關於redis在cluster模式化下的 分布式鎖的探索


背景

       redis作為一個內存數據庫,在分布式的服務的大環境下,占的比重越來越大啦,下面我們和大家一起探討一下如何使用redis實現一個分布式鎖 

說明

      一個分布式鎖至少要滿足下面幾個條件

     1:互斥性

              多個客戶端競爭的時候,只能有一個客戶端能獲取鎖

      2:安全性

              誰創建,誰銷毀,客戶端A創建了分布式鎖,只能有A來銷毀

     3:容錯性

            某個redis節點掛啦,不會影響客戶端創建或者銷毀分布式鎖

     4:避免死鎖

            客戶端A創建了分布式鎖因程序異常未釋放,不會造成其他客戶端再也無法申請到鎖

       下面我們基於上面四個基本准則一起來設計分布式鎖,主要有2個方法,①嘗試獲取鎖,②釋放鎖

   嘗試獲取鎖

   這一段代碼中有很多容易犯錯的地方

public boolean trylock(String lockKey,String lockValue,Long lockWaitTimeout,Long lockExpirseTimeout){
int timeout = lockWaitTimeout.intValue();
while (timeout >= 0){
String expireTimeout = String.valueOf(lockExpirseTimeout/1000);
List<String> keys = new ArrayList<String>();
keys.add(lockKey);

List<String> args = new ArrayList<String>();
args.add(lockValue);
args.add(expireTimeout);

//①使用lua腳本,setnx創建鎖,並設置過期時間,這里網上大多數教程都是直接將value值設置為過期時間,人工判斷,我在這里通過lua腳本給加一個過期時間
/**
      //偽代碼
// 如果當前鎖不存在,返回加鎖成功,
      if (jedis.setnx(lockKey, expiresStr) == 1) {
// 若在這里程序突然崩潰,則無法設置過期時間,將發生死鎖,建議將2個命令通過lua腳本一起執行,保證原則性
          jedis.expire(lockKey, expireTime);
return true;
      }
*/
// 如果當前鎖不存在,返回加鎖成功
String lockLuaScript = setNxLuaScript();
Long exeResult = exeLuaScript(lockLuaScript,keys,args,Long.class);
if (exeResult!=null && exeResult.intValue() == 1){
return true;
}

// 如果鎖存在,獲取鎖的過期時間
String lockTimeStr = get(lockKey);
if (lockTimeStr != null && Long.parseLong(lockTimeStr) < System.currentTimeMillis()){
// 鎖已過期,獲取上一個鎖的過期時間,並設置現在鎖的過期時間
String oldLockTimeStr = getAndSet(lockKey,lockValue);
// 考慮多線程並發的情況,只有一個線程的設置值和當前值相同,它才有權利加鎖
if (oldLockTimeStr != null && oldLockTimeStr.equals(lockTimeStr)){
//大多數網上源代碼中是木有這一行代碼的,此行是為了解決高並發情況下,getSet雖然只有一個設置成功,但是value值可能會被覆蓋,所以重新設置一下
set(lockKey,lockValue,Long.valueOf(expireTimeout),TimeUnit.SECONDS);
return true;
}
}
int sleepTime=new Random().nextInt(10)*100;
timeout -= sleepTime;
try {
log.info("獲取redis分布式鎖失敗,sleep:{}ms后重新獲取",sleepTime);
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 其他情況,一律返回加鎖失敗
return false;
}

private String setNxLuaScript(){
StringBuffer luascript = new StringBuffer();
luascript.append(" if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then ");
luascript.append(" redis.call('expire',KEYS[1],ARGV[2]) return 1");
luascript.append(" else return 0 end");
return luascript.toString();
}

釋放鎖

/**
   *網上很多代碼都木有考慮,誰創建,誰銷毀這個准則
* 通過獲取lockKey的值和當初設定oldValue是否一致,來決定客戶端是否有權利來釋放鎖,由於這是2個命令,考慮高並發情況,所以通過lua腳本,將2個命令放在一起執行,保證原子性
*/
public void unlock(String lockKey,String oldValue){
String luascript = delLuaScript();
List<String> keys = new ArrayList<String>();
keys.add(lockKey);
List<String> args = new ArrayList<String>();
args.add(oldValue);
exeLuaScript(luascript,keys,args,Long.class);
}


private String delLuaScript(){
StringBuffer luascript = new StringBuffer();
luascript.append(" if redis.call('exists',KEYS[1]) == 1 and redis.call('get',KEYS[1]) == ARGV[1] then");
luascript.append(" redis.call('del',KEYS[1]) return 1");
luascript.append(" else return 0 end");
return luascript.toString();
}

//執行lua腳本命令

public <T> T exeLuaScript(String luaScript, List<String> keys, List<String> args,Class<T> clz){
 T t = (T)redisTemplate.execute(new RedisCallback<T>(){
@Override
public T doInRedis(RedisConnection redisConnection) throws DataAccessException {

Object nativeConnection = redisConnection.getNativeConnection();
if (nativeConnection instanceof JedisCluster) {
return (T)((JedisCluster) nativeConnection).eval(luaScript.toString(), keys, args);
} // 單機模式
else if (nativeConnection instanceof Jedis) {

return (T) ((Jedis) nativeConnection).eval(luaScript.toString(), keys, args);
}
return null;
}
});

if(t == null){
throw new RuntimeException("redis model doesn't support luascript");
}
return t;
}

上述代碼目前依然存在的問題
①:當業務耗時時間大於分布式鎖的過期時間lockExpirseTimeout,會造成同時有2個客戶端獲取到了分布式鎖
②:容錯性問題還有待解決
 
  1.  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM