為了防止分布式系統(tǒng)中的多個進程之間相互干擾,需要一種分布式協(xié)調(diào)技術(shù)來對這些進程進行調(diào)度县忌。而這個分布式協(xié)調(diào)技術(shù)的核心就是來實現(xiàn)這個分布式鎖烘贴。
Redis加鎖
原理很簡單,set 一個 鎖-key礼患,如果成功則說明加鎖成功,反之則失敗失受。
為了確保分布式鎖可用讶泰,我們至少要確保鎖的實現(xiàn)同時滿足以下幾個條件:
互斥性咏瑟。在任意時刻拂到,只有一個客戶端能持有鎖。
不會發(fā)生死鎖码泞。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖兄旬,也能保證后續(xù)其他客戶端能加鎖。
解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端领铐,客戶端自己不能把別人加的鎖給解了悯森。
基于以上條件,采用set擴展參數(shù)绪撵,保證原子性操作:SET lock-key "lock-client" EX 10086 NX
lock-key "lock-client"
指定 加鎖client瓢姻,解鎖時用于判斷。
EX 10010
指定過期時間
NX
只在鍵不存在時音诈,才對鍵進行設(shè)置操作幻碱。效果等同于SETNX
命令。
只不過早期版本redis不支持set的擴展參數(shù)细溅,這就需要用到 lua
腳本了褥傍。
加鎖可以在高版本借助set命令實現(xiàn)原子操作,但解鎖就不可以了喇聊,依然得用到lua腳本恍风。
Redis+Lua
Redis在2.6版本推出了 lua 腳本功能,允許開發(fā)者使用Lua語言編寫腳本傳到Redis中執(zhí)行誓篱。使用腳本的好處如下:
- 減少網(wǎng)絡(luò)開銷:可以將多個請求通過腳本的形式一次發(fā)送朋贬,減少網(wǎng)絡(luò)時延。
- 原子操作:Redis會將整個腳本作為一個整體執(zhí)行窜骄,中間不會被其他請求插入兄世。因此在腳本運行過程中無需擔(dān)心會出現(xiàn)競態(tài)條件,無需使用事務(wù)啊研。
- 復(fù)用:客戶端發(fā)送的腳本會永久存在redis中御滩,這樣其他客戶端可以復(fù)用這一腳本,而不需要使用代碼完成相同的邏輯党远。
Redis 解鎖
需要在獲得 lock-key 后判斷加鎖對象是否為當(dāng)前client削解,是,則解鎖沟娱。Lua 腳本如下:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
執(zhí)行方式:eval氛驮;
eval 參數(shù)列表:eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
,參數(shù)解析:
eval 代表執(zhí)行Lua語言的命令济似;
lua-script 代表Lua語言腳本矫废;
key-num 表示參數(shù)中有多少個key,需要注意的是Redis中key是從1開始的砰蠢,如果沒有key的參數(shù)蓖扑,那么寫0;
[key1 key2 key3…] 是key作為參數(shù)傳遞給Lua語言台舱,也可以不填律杠,但是需要和key-num的個數(shù)對應(yīng)起來;
[value1 value2 value3 …] 這些參數(shù)傳遞給Lua語言,他們是可填可不填的柜去。
eval執(zhí)行示例:eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-val
灰嫉;
完整解鎖執(zhí)行腳本:
eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock-key client-val
為什么不優(yōu)先考慮使用 Redis 事務(wù)
簡單提兩句這個事情,redis 本身有提供事務(wù)功能嗓奢,即保證一系列復(fù)合操作是原子性執(zhí)行讼撒。不過事務(wù)有兩個問題:
1、Redis事務(wù)不支持Rollback(重點)
2股耽、基于上面1點椿肩,對于事務(wù)中已成功執(zhí)行的操作,無法回滾豺谈。
其實解鎖操作郑象,用事務(wù)倒是無所謂,因為是先get到key值茬末,比較后再刪除厂榛,即便第二步操作失敗,第一步的get也沒有實際影響丽惭;
但如果加鎖時击奶,使用set、expire可能會有問題责掏,比如set后未設(shè)置過期時間前進程異常掛掉柜砾,導(dǎo)致鎖沒有過期時間產(chǎn)生死鎖。所以加鎖盡量使用高版本(redis2.6及以上版本)的set附加expire參數(shù)執(zhí)行吧换衬。
參考樣例-PHP版
// 加鎖操作
function lock($timeout = 3) {
// 加鎖的key
$mtkey = 'lock:your_lock_key';
// 隨機生成id用于解鎖操作痰驱,也可用自己業(yè)務(wù)中其它具有唯一性標(biāo)識的數(shù)值
$mtid = uniqid(mt_rand(1000, 9999));
// 獲取鎖的超時時間
$end = time() + $timeout;
while (time() <= $end) {
// NX: 不存在時設(shè)置;PX:過期時間(毫秒)瞳浦;
if ($redis->set($mtkey, $mtid, array('NX', 'PX' => 1000))) {
return $mtid;
}
usleep(1000);
}
return '';
}
// 解操操作(將自己設(shè)置的鎖刪除)
function unLock($mtkey = 'lock:your_lock_key', $mtid) {
$script = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
LUA;
/**
* eval 第一個參數(shù)是要執(zhí)行的LUA腳本內(nèi)容
* 第二個參數(shù)是傳遞的參數(shù)
* 第三個參數(shù)是指傳遞的參數(shù)中前X個是放到LUA中的 KEYS 表担映,剩余的則放到LUA中的 ARGV 表
* LUA中的“表”類似數(shù)組,索引以1開始叫潦。
*/
$redis->eval($script, array($mtkey, $mtid), 1);
}
----------End----------