關(guān)于分布式的鎖,大概有幾個(gè)熱點(diǎn)問(wèn)題
關(guān)鍵點(diǎn)一:原子命令加鎖。
對(duì)于 Redis 的加鎖操作先set key,再設(shè)置 key 的過(guò)期時(shí)間,這樣的話不是原子性操作。不是原子操作會(huì)帶來(lái)什么問(wèn)題,就不用我說(shuō)了吧?
而在 2.6.12 版本后,可以通過(guò)向 Redis 發(fā)送下面的命令,實(shí)現(xiàn)原子性的加鎖操作:
SET key random_value NX PX 30000
我們常用的redis客戶(hù)端,Jedis、Lettuce都實(shí)現(xiàn)了這一命令方法,比如jedis的setnx(),Lettuce的setIfAbsent()。
關(guān)鍵點(diǎn)二:設(shè)置值的時(shí)候,放的是random_value。而不是你隨便扔個(gè)“OK”進(jìn)去。
先解釋一下上面的命令中的幾個(gè)參數(shù)的含義:
random_value:是由客戶(hù)端生成的一個(gè)隨機(jī)字符串,它要保證在足夠長(zhǎng)的一段時(shí)間內(nèi)在所有客戶(hù)端的所有獲取鎖的請(qǐng)求中都是唯一的。 比如我們使用UUID。
NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時(shí)候才能 set 成功。這保證了只有第一個(gè)請(qǐng)求的客戶(hù)端才能獲得鎖,而其它客戶(hù)端在鎖被釋放之前都無(wú)法獲得鎖。
PX 30000:表示這個(gè)鎖有一個(gè) 30 秒的自動(dòng)過(guò)期時(shí)間。當(dāng)然,這里 30 秒只是一個(gè)例子,客戶(hù)端可以選擇合適的過(guò)期時(shí)間。
那么為什么要使用 PX 30000 去設(shè)置一個(gè)超時(shí)時(shí)間?是怕進(jìn)程 A 不講道理啊,鎖沒(méi)等釋放呢,萬(wàn)一崩了,直接原地把鎖帶走了,導(dǎo)致系統(tǒng)中誰(shuí)也拿不到鎖。
就算這樣,還是不能保證萬(wàn)無(wú)一失。如果進(jìn)程 A 又不講道理,操作鎖內(nèi)資源超過(guò)筆者設(shè)置的超時(shí)時(shí)間,那么就會(huì)導(dǎo)致其他進(jìn)程拿到鎖,等進(jìn)程 A 回來(lái)了,回手就是把其他進(jìn)程的鎖刪了。這就引出了下一個(gè)關(guān)鍵點(diǎn)。
再解釋一下為什么 value 需要設(shè)置為一個(gè)隨機(jī)字符串。這也是第三個(gè)關(guān)鍵點(diǎn)。
關(guān)鍵點(diǎn)三:value 的值設(shè)置為隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時(shí)候需要檢查 key 是否存在,且 key 對(duì)應(yīng)的值是否和我指定的值一樣,是一樣的才能釋放鎖。所以可以看到這里有獲取、判斷、刪除三個(gè)操作。
釋放鎖偽代碼
String uuid = xxxx;
// 偽代碼,具體實(shí)現(xiàn)看項(xiàng)目中用的連接工具
// 有的提供的方法名為set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
// unlock
if(uuid.equals(redisTool.get('key')){
redisTool.del('key');
}
}
這回看起來(lái)是不是穩(wěn)了?相反,這回的問(wèn)題更明顯了,在 Finally 代碼塊中,Get 和 Del 并非原子操作,還是有進(jìn)程安全問(wèn)題。
進(jìn)程安全問(wèn)題會(huì)在什么場(chǎng)景下出現(xiàn)?我就先不回答了,感興趣的可以在留言中交流下
為了保障原子性,我們需要用 lua 腳本。
那么刪除鎖的正確姿勢(shì)之一,就是可以使用 Lua 腳本,通過(guò) Redis 的 eval/evalsha 命令來(lái)。
下面簡(jiǎn)單看下一個(gè)釋放鎖有問(wèn)題的代,相信也是很多人使用最多的方法
/**
* 獲取鎖
* @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);
}
}
/**
* 獲取鎖,如果沒(méi)有獲取到會(huì)再去嘗試幾次
*
* @param key 鎖鍵值
* @param expireSecond 鎖過(guò)期時(shí)間
* @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);
}
實(shí)現(xiàn)Redis鎖
網(wǎng)上大佬用jedis寫(xiě)的鎖簡(jiǎn)單修改下使用lettuce實(shí)現(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) {
// 釋放鎖的時(shí)候,有可能因?yàn)槌宙i之后方法執(zhí)行時(shí)間大于鎖的有效期,此時(shí)有可能已經(jīng)被另外一個(gè)線程持有鎖,所以不能直接刪除
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;
}
}