隨著分布式系統(tǒng)的流行外盯,分布式鎖的需求也越來越強(qiáng)。網(wǎng)上很多基于Redis實(shí)現(xiàn)的分布式鎖翼雀,但是大大小小都有些問題饱苟。本文基于Redis給出實(shí)現(xiàn)及一些問題的分析。
基于Redis單節(jié)點(diǎn)(主從架構(gòu))的實(shí)現(xiàn)
獲取鎖
SET key_name random_value NX PX expire_time
public boolean lock(String key, long expireTime, TimeUnit timeUnit) {
String lockKey = LOCK_PREFIX + key;
return redisTemplate.execute(
(RedisCallback<Boolean>) connection ->
connection.set(lockKey.getBytes(), UUID.randomUUID().toString().replaceAll("-", "").getBytes(), Expiration.from(expireTime, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT)
);
}
- key_name:鎖的名稱
- random_value:客戶端生成的隨機(jī)字符串
- NX:只有當(dāng)不存在此key_name時才能操作成功
- PX:設(shè)置過期時間
- expire_time:過期時間值狼渊,單位:毫秒
執(zhí)行業(yè)務(wù)代碼
執(zhí)行業(yè)務(wù)的具體處理操作箱熬。
釋放鎖
釋放鎖采用lua腳本去執(zhí)行。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
- KEYS[1]:就是前面獲取鎖時的key_name
- ARGV[1:前面獲取鎖時的random_value
Redis單節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖注意事項(xiàng)
- 鎖要加上過期時間,防止成功獲取鎖的客戶端由于各種原因?qū)е聼o法與Redis進(jìn)行通信城须,無法釋放鎖护锤,導(dǎo)致其他客戶端也無法獲取到鎖,產(chǎn)生了死鎖酿傍。
- set NX 操作與 PX 操作要在同一條命令里執(zhí)行烙懦,避免set NX后由于其他原因,導(dǎo)致無法執(zhí)行PX操作赤炒,無法給鎖設(shè)置過期時間氯析。
- 必須給鎖設(shè)置一個隨機(jī)字符串,它保證了客戶端在釋放鎖時莺褒,釋放的一定是自己獲得的那把鎖掩缓。
- 釋放鎖的操作必須使用lua腳本實(shí)現(xiàn)。釋放鎖其實(shí)包含三步遵岩,'GET'你辣、判斷和'DEL'。使用lua腳本可以保證這三個操作的原子性尘执,客戶端自己操作這三個步驟不具有原子性舍哄。
這里解釋下第三點(diǎn)為什么要這么做,考慮的是可能會出現(xiàn)以下情況:
- 客戶端1獲取鎖成功
- 客戶端1在成功獲取鎖后誊锭,執(zhí)行業(yè)務(wù)邏輯時表悬,在某個地方阻塞了(比如說IO操作)很長一段時間
- 鎖的過期時間到了,鎖自動釋放了
- 客戶端2獲取到了同一個鎖的資源
- 客戶端1從阻塞中恢復(fù)過來丧靡,并且釋放掉了客戶端2持有的鎖蟆沫。
使用random_value,客戶端會判斷redis保存的那把鎖還是不是自己持有的那把鎖温治,如果是則釋放鎖饭庞,不是,則釋放失敗熬荆。
以上幾個問題在使用時只要稍加注意舟山,還是可以避免掉的。但是有一個問題惶看,由于Redis單節(jié)點(diǎn)無法解決的捏顺。
failover(故障轉(zhuǎn)移)引起的問題,以下簡述一下發(fā)生的過程纬黎。
- 客戶端1從master節(jié)點(diǎn)獲取了鎖
- master宕機(jī)了,并且存儲的key尚未復(fù)制到slaver節(jié)點(diǎn)
- slaver升級為master
- 客戶端2從新的master節(jié)點(diǎn)獲得了同一個鎖
由于Redis單節(jié)點(diǎn)存在一些問題劫窒,而且實(shí)際生產(chǎn)過程中本今,一般采用Redis集群保證高可用。Redis作者提出了Redlock的算法來實(shí)現(xiàn)Redis多節(jié)點(diǎn)下的分布式鎖。
Redis集群下的分布式鎖
獲取鎖
- 獲取當(dāng)前系統(tǒng)時間(毫秒數(shù))
- 按順序向Redis所有節(jié)點(diǎn)執(zhí)行獲取鎖的操作冠息,這個獲取鎖的操作和單節(jié)點(diǎn)時一致挪凑,包含隨機(jī)字符串、過期時間等逛艰。為了保證不受某個不可用節(jié)點(diǎn)的影響躏碳,Redis還增加了一個超時時間,它遠(yuǎn)小于鎖的有效時間(幾十毫秒級)散怖」矫啵客戶端向某個節(jié)點(diǎn)獲取鎖失敗,應(yīng)立即向其他節(jié)點(diǎn)獲取鎖
- 計算整個獲取鎖的過程總共消耗了多少時間镇眷,計算方法是用當(dāng)前時間減去第一步獲取的時間咬最,如果客戶端從大多數(shù)節(jié)點(diǎn)(>N/2+1)都獲取到了鎖,并且獲取鎖的總消耗時間小于鎖的有效時間欠动,這時才認(rèn)為獲取鎖成功永乌,否則認(rèn)為失敗
- 如果獲取鎖成功了,那么這個鎖的有效時間需要重新計算具伍,它等于最初的鎖的有效時間減去獲取鎖的過程消耗時間
- 如果鎖最終獲取失敗了翅雏,那么客戶端應(yīng)該向所有節(jié)點(diǎn)發(fā)送釋放鎖的操作
執(zhí)行客戶端代碼
釋放鎖
客戶端向所有節(jié)點(diǎn)發(fā)送釋放鎖的操作,包括獲取鎖失敗的節(jié)點(diǎn)人芽。