前言
Redis分布式鎖其實網(wǎng)上已經(jīng)一大把了拴孤,我寫這篇博客的目的是想發(fā)表下自己的淺見荧琼。
我們知道在單進程中,通過語言內(nèi)置的鎖可以保證數(shù)據(jù)的一致性带迟,隨著軟件的不斷擴大,很多應用已經(jīng)是多進程的方式部署,那么為了保證數(shù)據(jù)的一致性,我們需要一個分布式鎖囱桨。標題說的是Redis分布式鎖仓犬,那么我下面主要講的rendis鎖,而且不會涉及到redis單點故障的問題舍肠。
常見的分布鎖
搜索一下常見的分布式鎖說的都是通過redis和zookeeper來實現(xiàn)搀继。
-
redis
setnx 通過設置一個共有的key,而且通過設置過期時間來避免死鎖
-
zk
通過設置一個共有的節(jié)點,通過zk的watch,觀察節(jié)點的變化進行通知
鎖的需求
獨占: 一個時間點只能一個線程訪問
可重入: 同一個線程只要當前持有鎖翠语,可以重復進入
避免死鎖: 多個線程訪問不會出現(xiàn)死鎖的情況
鎖釋放: 進程故障能自動釋放鎖叽躯,只有持有鎖的進程能釋放鎖
嘗試自己實現(xiàn)一個
實現(xiàn) copy from importnew Redis 分布式鎖的正確實現(xiàn)方式( Java 版 )
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
private static final Long RELEASE_SUCCESS = 1L;
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
上面的鎖實現(xiàn)存在一個問題,假如程序員A不小心調(diào)用了releaseDistributedLock就會把某個已經(jīng)持有鎖的?線程的鎖釋放掉。相信大家都能想到用ThreadLocal方式避免給未持有鎖的線程釋放了肌括。 那我就試著將這代碼改改点骑。(偽代碼)
public class RedisToolv2 {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private final ThreadLocal<Boolean> hasLock = new ThreadLocal<>();
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
boolean locked = false;
if (LOCK_SUCCESS.equals(result)) {
locked = true;
}
hasLock.set(locked);
return locked;
}
private static final Long RELEASE_SUCCESS = 1L;
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
boolean locked = Optional.ofNull(hasLock.get()).orElse(false) ;
boolean unloked = false;
if(locked){
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
unloked = true;
}
}
return false;
}
}
上面的方式雖然說解決了其他線程誤刪鎖的情況,但是分布式鎖其實還有很多細節(jié)都地方需要實現(xiàn),當一個線程已經(jīng)擁有此鎖了畔况,如果還有大量其他(本應用或其他應用)的線程在請求鎖鲸鹦,無疑給redis造成很大的壓力(當然這個得看應用的規(guī)模,小規(guī)模是不會出現(xiàn)這個情況)跷跪。
假設我們同一個應用中同一個時間點只有線程在請求分布式鎖馋嗜,那么相對于Redis的壓力是不是就會下降很多? 那么我們在代碼中加入一個同步鎖是不是就解決了問題呢?首先我想到的是synchronized可以重入吵瞻,可以使用遞歸的方式調(diào)用葛菇。但是如果是遞歸,如果長時間拿不到鎖橡羞,那么有可能會引發(fā)StackOverflowError眯停。額, 那這不能實現(xiàn)卿泽,沒招了咩莺债? 其實jdk里面帶了一個 ReentrantLock,這個鎖其實跟synchronized差不多,也是可以重入的签夭,而且有一個 tryLock(long timeout, TimeUnit unit)的方法齐邦,在遞歸調(diào)用的時候可以避免Stack過大。 (思路有了第租,我相信代碼也是很輕易就寫出來了)
拋個磚
那么這個鎖持有的時間該如何計算呢措拇?
還有就是這么實現(xiàn)這個鎖是不是就完美了呢?慎宾?
Redis有個叫鍵空間通知(keyspace notification) 的功能丐吓,是不是可以用這種方式來實現(xiàn)一個類似zookeeper的鎖呢 ?
最后
各位看官還有什么好的思路歡迎來交流呀趟据。