背景
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個客戶端獲取到了分布式鎖
②:容錯性問題還有待解決