redis分佈式鎖

上一篇寫的是關於mongo分佈式鎖的bug的文章, 發現網上使用的mongo實現分佈式鎖方案有bug, 目前我還沒找到解決方案, 建議你們仍是使用redis來實現redis

具體思路仍是利用redis的setnx方法的安全性, 同一時刻永遠只有一個線程能set成功.spring

加鎖代碼以下:安全

public boolean lock(String lockKey, String requestId, Long expireTime) {

return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);

//若不支持上述原子操做,可以使用以下方式
// RedisScript redisScript = RedisScript.of(LOCK_LUA, Boolean.class);
// List<String> keys = new ArrayList<>();
// keys.add(lockKey);
// return (Boolean)stringRedisTemplate.execute(redisScript, new FastJsonRedisSerializer<>(Object.class),
// new FastJsonRedisSerializer<>(Object.class), keys, requestId, expireTime);

}

如註釋中所述,若不支持上述原子操做,可以使用LUA腳本方式實現,不過須要注意,若是redis是集羣模式,是不支持以上lock方法中註釋的代碼的,
須要
拿到原redis的connection來執行腳本,具體實現可參考下面unLock方法依葫蘆畫瓢.
低版本的redisTemplate是不支持setIfAbsent的同時設置過時時間的,
須要分兩步,先setIfAbsent,成功後再expire,不過這樣以來就不是同步的了,
也許在setIfAbsent成功後還沒來得及expire,系統dump了或redis掛了,那就形成死鎖了!!!

加鎖代碼以下:
/**
* 單機和集羣都適用,
* 但有個條件,單機模式時,必須排除掉io.lettuce包,確保鏈接使用的是Jedis實例
* @param key
* @param requestId
* @return
*/
public boolean unLock(String key,String requestId) {
// 釋放鎖的時候,有可能由於持鎖以後方法執行時間大於鎖的有效期,此時有可能已經被另一個線程持有鎖,因此不能直接刪除
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(requestId);

// 使用lua腳本刪除redis中匹配value的key,能夠避免因爲方法執行時間過長而redis鎖自動過時失效的時候誤刪其餘線程的鎖
// spring自帶的執行腳本方法中,集羣模式直接拋出不支持執行腳本的異常,因此只能拿到原redis的connection來執行腳本
Long result = stringRedisTemplate.execute((RedisCallback<Long>) connection -> {
Object nativeConnection = connection.getNativeConnection();
// 集羣模式和單機模式雖然執行腳本的方法同樣,可是沒有共同的接口,因此只能分開執行
// 集羣模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}

// 單機模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
});
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock error", e);
}
return false;
}
上述解鎖代碼註釋已經很詳細了,這個方法也是網上使用較多的, 加了部分本身的註釋.
有個問題值得注意,就是當你的redis是單機模式時,必須排除掉io.lettuce包,確保鏈接使用的是Jedis實例,
不然進入不了單機模式的刪鎖代碼,致使釋放鎖失效.

另,附上加鎖和解鎖的LUA腳本:
public static final String UNLOCK_LUA;

public static final String LOCK_LUA;

static {
StringBuilder unlockLua = new StringBuilder();
unlockLua.append("if redis.call('get', KEYS[1]) == ARGV[1] ");
unlockLua.append("then ");
unlockLua.append(" return redis.call('del', KEYS[1]) ");
unlockLua.append("else ");
unlockLua.append(" return 0 ");
unlockLua.append("end ");
UNLOCK_LUA = unlockLua.toString();

StringBuilder lockLua = new StringBuilder();
lockLua.append("if redis.call('setnx', KEYS[1], ARGV[1]) == 1 ");
lockLua.append("then ");
lockLua.append(" redis.call('expire', KEYS[1], ARGV[2]) return true ");
lockLua.append("else ");
lockLua.append(" return false ");
lockLua.append("end ");
LOCK_LUA = lockLua.toString();
}
以上代碼完美實現redis分佈式鎖機制,可放心使用.
相關文章
相關標籤/搜索