為了防止分布式系統(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ù)中其它具有唯一性標識的數(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----------