Redis分布式鎖

Redis 單線程:網(wǎng)絡(luò)IO和鍵值對讀寫是由一個線程完成的(網(wǎng)絡(luò)請求模塊和數(shù)據(jù)操作模塊是單線程的)

1,使用set加鎖。

1)set key value [EX seconds] [PX milliseconds] [NX|XX]
NX: set if not exists
XX: set if already exists
EX: seconds
PX: milliseconds
set zq 3 EX 10000 NX 支持設(shè)置EX失效時間,NX表示key不存在時才設(shè)置。
2)在刪除key時,將內(nèi)容對比(確定是否是自身線程擁有的鎖),一致才進行刪除。
3)支持可重入:循環(huán)+唯一id

2,MyRedisLock

/**
 * RedisLock 分布式鎖
 */
public class MyRedisLock {

  public static final String RELEASE_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

  private static final int DEFAULT_LOCK_EXPIRE_TIME_MILLIS = 10 * 1000;//默認(rèn)的lock key的過期時間
  private static final int DEFAULT_TRY_LOCK_TIMEOUT_MILLIS = 5 * 1000;//嘗試加鎖的超時時間
  private static final int CHECK_RELEASE_MAX_INTERVAL_MILLIS = 100;//檢查過期的間隔

  private Jedis jedis;//節(jié)點客戶端
  private String lockKey;//加鎖的key
  private String lockId;//加鎖的value
  private volatile long lockHoldingTime = 0;//鎖的持有時間

  public MyRedisLock(Jedis jedis, String lockKey, String lockId){
    this.jedis = jedis;
    this.lockKey = lockKey;
    this.lockId = lockId;
  }

  /**
   * 主動釋放鎖
   */
  public synchronized boolean release(){
    Object result = jedis.eval(RELEASE_LUA_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(lockId));
    lockHoldingTime = 0;
    return result != null && (Integer)result > 0;
  }

  /**
   * 判斷是否加鎖成功
   */
  public boolean isAcquired(){
    return lockHoldingTime >= System.currentTimeMillis();
  }

  /**
   * 阻塞嘗試使用默認(rèn)超時
   */
  public synchronized boolean acquire() throws InterruptedException{
    return acquire(DEFAULT_TRY_LOCK_TIMEOUT_MILLIS);
  }

  /**
   * 阻塞嘗試加鎖
   */
  public synchronized boolean acquire(int lockAcquireTimeoutMillis) throws InterruptedException {
    if (lockAcquireTimeoutMillis <= 0) {
      throw new IllegalArgumentException("lockAcquireTimeoutMillis <= 0");
    }

    boolean isAcquired = false;
    long startTimeNanos = System.nanoTime();
    try {
      int retry = 0;
      while (lockAcquireTimeoutMillis > 0) {
        String reply = jedis.set(lockKey, lockId, "NX", "PX", DEFAULT_LOCK_EXPIRE_TIME_MILLIS);
        if ("OK".equals(reply)) {
          lockHoldingTime = System.currentTimeMillis() + DEFAULT_LOCK_EXPIRE_TIME_MILLIS;
          isAcquired = true;
          break;
        }

        //支持可重入特性
        if (lockId.equals(jedis.get(lockKey))) {
          isAcquired = true;
          break;
        }

        // exponential backoff sleep starting at 16 milliseconds and maximum 100 milliseconds
        int sleep = 8 << ++retry;
        if (sleep > CHECK_RELEASE_MAX_INTERVAL_MILLIS || sleep < 0) {
          sleep = CHECK_RELEASE_MAX_INTERVAL_MILLIS;
        }
        lockAcquireTimeoutMillis -= sleep;
        Thread.sleep(sleep);
      }
      return isAcquired;
    } finally {
      long elapsed = System.nanoTime() - startTimeNanos;
      System.out.println("lockKey:" + lockKey + ", lockId:" + lockId + ", elapsed:" + elapsed);
    }
  }

}

3,eval執(zhí)行l(wèi)ua腳本

1)eval script numkeys key [key ...] arg [arg ...] eval 腳本 key的數(shù)量 key列表 附加參數(shù)列表
KEYS key數(shù)組:KEYS[1]獲取第一個key;
ARGV 參數(shù)數(shù)組:ARGV[1]獲取第一個附加參數(shù)。
2)eval可以返回多個數(shù)據(jù)
eval "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
3)腳本中使用redis.call()調(diào)用redis命令
eval "return redis.call('set','testEval','evalValue')" 0
eval "return redis.call('get', 'testEval')" 0
4)redis原子操作,分布式解鎖。
如果調(diào)用redis.get(${Key})獲取到的值,和附加參數(shù)中ARGV[1]相等,則刪除${Key},返回1,否則返回0

image.png

4,使用上述方案,潛在問題

1)超時時間設(shè)置
過長:如果線程執(zhí)行時掛掉(如機器宕機、程序掛掉)等,未釋放鎖。則會導(dǎo)致其他線程的【鎖等待時間】過長?!竞侠碓u估業(yè)務(wù)執(zhí)行時間,寧大勿小】
過短:如果線程還在執(zhí)行中,鎖超時被釋放了,則有可能會造成其他線程獲取到鎖,造業(yè)務(wù)故障。(不可取)
2)使用Redisson(Watch Dogs)實現(xiàn)鎖續(xù)期
internalLockLeaseTime默認(rèn)key的超時時間30s
鎖續(xù)期定時任務(wù)間隔internalLockLeaseTime / 3

5,Redisson實現(xiàn)原理

1)原子性設(shè)計:加鎖、解鎖、續(xù)約等都是通過Lua腳本中發(fā)送給redis
2)存儲結(jié)構(gòu)與加鎖腳本
key為唯一id+threadId,值為調(diào)用次數(shù)

image.png

image.png

image.png

3)線程阻塞與解除阻塞
redisson_lock__channel:{my-redisson-key}
image.png

4)Watch Dog鎖續(xù)期
leaseTime=-1(使用默認(rèn)的加鎖時間為 30s),才會生效
image.png

image.png

1.刪除鎖;重入鎖則減1
2.使用redis廣播鎖釋放消息,向redisson_lock__channel發(fā)送UNLOCK_MESSAGE
3.取消 Watch Dog。RedissonLock.EXPIRATION_RENEWAL_MAP中線程id刪除,并取消掉netty的定時任務(wù)
image.png

5)redis分布式鎖的最大缺陷
master-slave架構(gòu)中,當(dāng)A寫入master成功后,在slave異步復(fù)制過程中,發(fā)生了主從切換,導(dǎo)致B在新的master上也能加鎖成功。多個客戶端加鎖成功,可能會造成臟數(shù)據(jù)產(chǎn)生

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 Redis作為cache服務(wù)器,支持多種數(shù)據(jù)結(jié)構(gòu),String、List、Hash、Set、Zset。多種數(shù)...
    小小小碼農(nóng)閱讀 1,078評論 0 1
  • 給時光以生命,而不是給生命以時光 《愛呀》金玟岐幻海流心: 真正深愛過卻分手的人,永遠無法再做朋友,也無法灑脫的去...
    幻境流心閱讀 767評論 0 1
  • 此書作者例舉了他的兩個爸爸對金錢的不同觀念對他造成的影響。 窮爸爸主張墨守成規(guī),規(guī)規(guī)矩矩,努力學(xué)習(xí)讀書做個工薪階級...
    Berrywang閱讀 366評論 0 0
  • 年少 年少的我們都太過倔強 明明受了傷 卻將委屈都自己扛 年少的我們都太過天真 比太陽月亮都準(zhǔn)時 站在他會經(jīng)過的路...
    南婉Miss閱讀 258評論 0 4

友情鏈接更多精彩內(nèi)容