關(guān)于分布式的鎖刹泄,大概有幾個熱點問題
關(guān)鍵點一:原子命令加鎖震糖。
對于 Redis 的加鎖操作先set key,再設(shè)置 key 的過期時間屏鳍,這樣的話不是原子性操作。不是原子操作會帶來什么問題局服,就不用我說了吧钓瞭?
而在 2.6.12 版本后,可以通過向 Redis 發(fā)送下面的命令淫奔,實現(xiàn)原子性的加鎖操作:
SET key random_value NX PX 30000
我們常用的redis客戶端山涡,Jedis、Lettuce都實現(xiàn)了這一命令方法唆迁,比如jedis的setnx()鸭丛,Lettuce的setIfAbsent()。
關(guān)鍵點二:設(shè)置值的時候唐责,放的是random_value鳞溉。而不是你隨便扔個“OK”進(jìn)去。
先解釋一下上面的命令中的幾個參數(shù)的含義:
random_value:是由客戶端生成的一個隨機(jī)字符串妒蔚,它要保證在足夠長的一段時間內(nèi)在所有客戶端的所有獲取鎖的請求中都是唯一的穿挨。 比如我們使用UUID。
NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時候才能 set 成功肴盏。這保證了只有第一個請求的客戶端才能獲得鎖科盛,而其它客戶端在鎖被釋放之前都無法獲得鎖。
PX 30000:表示這個鎖有一個 30 秒的自動過期時間菜皂。當(dāng)然贞绵,這里 30 秒只是一個例子,客戶端可以選擇合適的過期時間恍飘。
那么為什么要使用 PX 30000 去設(shè)置一個超時時間榨崩?是怕進(jìn)程 A 不講道理啊,鎖沒等釋放呢章母,萬一崩了母蛛,直接原地把鎖帶走了,導(dǎo)致系統(tǒng)中誰也拿不到鎖乳怎。
就算這樣彩郊,還是不能保證萬無一失。如果進(jìn)程 A 又不講道理蚪缀,操作鎖內(nèi)資源超過筆者設(shè)置的超時時間秫逝,那么就會導(dǎo)致其他進(jìn)程拿到鎖,等進(jìn)程 A 回來了询枚,回手就是把其他進(jìn)程的鎖刪了违帆。這就引出了下一個關(guān)鍵點。
再解釋一下為什么 value 需要設(shè)置為一個隨機(jī)字符串金蜀。這也是第三個關(guān)鍵點痹仙。
關(guān)鍵點三:value 的值設(shè)置為隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時候需要檢查 key 是否存在末融,且 key 對應(yīng)的值是否和我指定的值一樣撩笆,是一樣的才能釋放鎖。所以可以看到這里有獲取抒线、判斷班巩、刪除三個操作。
釋放鎖偽代碼
String uuid = xxxx;
// 偽代碼嘶炭,具體實現(xiàn)看項目中用的連接工具
// 有的提供的方法名為set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
// unlock
if(uuid.equals(redisTool.get('key')){
redisTool.del('key');
}
}
這回看起來是不是穩(wěn)了抱慌?相反,這回的問題更明顯了眨猎,在 Finally 代碼塊中抑进,Get 和 Del 并非原子操作,還是有進(jìn)程安全問題睡陪。
進(jìn)程安全問題會在什么場景下出現(xiàn)寺渗?我就先不回答了匿情,感興趣的可以在留言中交流下
為了保障原子性,我們需要用 lua 腳本信殊。
那么刪除鎖的正確姿勢之一炬称,就是可以使用 Lua 腳本,通過 Redis 的 eval/evalsha 命令來涡拘。
下面簡單看下一個釋放鎖有問題的代玲躯,相信也是很多人使用最多的方法
/**
* 獲取鎖
* @param key
* @param expireSecond
* @return
*/
public boolean lock(String key, int expireSecond) {
Jedis Jedis = null;
try {
Jedis = jedisPool.getResource();
Long result = Jedis.setnx(key, "1");
if(result == 1){
Jedis.expire(key, expireSecond);
return true;
}
return false;
} catch (Exception ex) {
logger.error("lock-setnx error:", ex);
returnBrokenResource(Jedis);
throw new TraweServiceException("獲取鎖出現(xiàn)異常:" + ex.getMessage(), ex);
} finally {
returnResource(Jedis);
}
}
/**
* 獲取鎖,如果沒有獲取到會再去嘗試幾次
*
* @param key 鎖鍵值
* @param expireSecond 鎖過期時間
* @param tryCount 嘗試次數(shù)
* @return 是否獲得鎖
*/
@SuppressWarnings("static-access")
public boolean tryLock(String key, int expireSecond, int tryCount) {
if (lock(key, expireSecond)) {
return true;
}
for (int i=0; i<tryCount; i++) {
int sleepMills = RandomUtils.nextInt(20, 200);
try {
Thread.currentThread().sleep(sleepMills);
} catch (InterruptedException e) {
logger.error(e, e);
}
if (lock(key, expireSecond)) {
return true;
}
}
return false;
}
/**
* 釋放鎖
* @param key
*/
public void unlock(String key) {
del(key);
}
實現(xiàn)Redis鎖
網(wǎng)上大佬用jedis寫的鎖簡單修改下使用lettuce實現(xiàn)
@Component
public class RedisDistributedLock {
@Autowired
@Resource(name="redisTemplateMaster")
private RedisTemplate<Object,Object> redisTemplate;
private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);
// 如果獲取鎖失敗鳄乏,按照傳入的重試次數(shù)進(jìn)行重試
while((!result) && retryTimes-- > 0){
try {
logger.debug("lock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key , expire);
}
return result;
}
private boolean setRedis(final String key, final long expire ) {
try{
RedisCallback<Boolean> callback = (connection) -> {
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid);
return connection.set(key.getBytes(Charset.forName("UTF-8")), uuid.getBytes(Charset.forName("UTF-8")), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
};
return (Boolean)redisTemplate.execute(callback);
} catch (Exception e) {
logger.error("redis lock error.", e);
}
return false;
}
public boolean releaseLock(String key) {
// 釋放鎖的時候跷车,有可能因為持鎖之后方法執(zhí)行時間大于鎖的有效期,此時有可能已經(jīng)被另外一個線程持有鎖橱野,所以不能直接刪除
try {
RedisCallback<Boolean> callback = (connection) -> {
String value = lockFlag.get();
return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN ,1, key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")));
};
return (Boolean)redisTemplate.execute(callback);
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的數(shù)據(jù)朽缴,避免內(nèi)存溢出
lockFlag.remove();
}
return false;
}
}