分散式鎖之redis鎖及實現
阿新 • • 發佈:2018-11-12
分散式鎖有幾種常用的實現方式:zookeeper、memcached、redis、mysql。這裡介紹一下redis的實現方式,並在最後附上了一個Demo小工具:
眾所周知,reids鎖是通過setnx + expire的方式實現的,setnx保證只有在key不存在時才能set成功,expire保證鎖在非正常釋放的情況下不會形成死鎖。基本原理就是這個,但實際操作中我們需要注意幾個問題:
- setnx與expire是非原子性的,那麼如果setnx執行成功、但expire未執行,那麼鎖也就無法過期自動刪除了。解決方案:redis提供命令
set(key,1,30,nx)
一步到位設定超時時間。- 如果執行緒A先獲得了鎖,但是執行時間超過了鎖的過期時間,鎖自動釋放了,那麼執行緒B獲得鎖,不僅有可能導致執行結果出現併發不一致問題,如果A執行完了,那麼還會刪除掉B的鎖。解決方案:(1)使用一個請求標識作為鎖的value值,在刪除前判斷一下。(2)使用一個守護執行緒,不斷的更新鎖過期時間,保證執行過程中鎖不會釋放。
- 判斷value值與刪除鎖不是原子的。解決方案:使用lua指令碼,保證判斷與刪除原子操作。
public class RedisLockUtil {
private static final Jedis DEFAULT_CLIENT = RedisClient.getInstance();
private static final String LOCK_SUCCESS="OK";
private static final Long RELEASE_SUCCESS=1L;
private static final Long EXPIRE_SUCCESS=1L;
/**
* 加鎖
* @param client 可自己傳入redis-client,如果是null,則用預設的client
* @param lockId 當前鎖的key值
* @param requestId 當前加鎖的任務的請求標識,可以用執行緒id,或者分散式唯一id,
* 可以保證全域性唯一即可,作為當前鎖的value值使用,用作刪除或
* 延時是判斷是否是持有鎖的任務
* @param second 過期時間,避免鎖未正常釋放時形成死鎖
* @return true:加鎖成功,flase:加鎖失敗
*/
public static boolean lock(Jedis client, String lockId, String requestId,int second) {
if (client == null)
client = DEFAULT_CLIENT;
// client.setnx(lockId,"1");
// client.expire(lockId,60);
//加鎖和超時設定分為了兩步,沒法保證原子性,所以可以直接用下面的命令
String rst = client.set(lockId,requestId,"nx","ex",second);
if (LOCK_SUCCESS.equals(rst)) {
return true;
}
return false;
}
/**
* 釋放鎖
* @param client 可自己傳入redis-client,如果是null,則用預設的client
* @param lockId 當前鎖的key值
* @param requestId 請求標識,刪除前判斷是否是持有鎖的任務
* @return true:加鎖成功,flase:加鎖失敗
*/
public static boolean unlock(Jedis client, String lockId, String requestId) {
if (client == null)
client = DEFAULT_CLIENT;
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object rst = client.eval(luaScript, Collections.singletonList(lockId), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(rst)) {
return true;
}
return false;
}
/**
* 延遲加鎖時間
* @param client 可自己傳入redis-client,如果是null,則用預設的client
* @param lockId 當前鎖的key值
* @param requestId 請求標識,延時前判斷是否是持有鎖的任務
* @param second 申請延遲的時間
* @return true:加鎖成功,flase:加鎖失敗
*/
public static boolean expired(Jedis client, String lockId, String requestId, int second) {
if (client == null)
client = DEFAULT_CLIENT;
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('expire',KEYS[1],ARGV[2]) " +
"else return 0 end";
Object rst = client.eval(luaScript,Collections.singletonList(lockId), Arrays.asList(requestId,String.valueOf(second)));
if (EXPIRE_SUCCESS.equals(rst)) {
return true;
}
return false;
}
}
class RedisClient{
private static volatile Jedis client = null;
static Jedis getInstance(){
String host = "127.0.0.1";
int port = 6379;
synchronized (RedisClient.class) {
if (client == null) {
client = new Jedis(host,port);
}
}
return client;
}
}