Redis分布式鎖在高并發(fā)場景的實現(xiàn)方式

為了防止分布式系統(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í)行。使用腳本的好處如下:

  1. 減少網(wǎng)絡(luò)開銷:可以將多個請求通過腳本的形式一次發(fā)送,減少網(wǎng)絡(luò)時延。
  2. 原子操作:Redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他請求插入。因此在腳本運行過程中無需擔(dān)心會出現(xiàn)競態(tài)條件,無需使用事務(wù)。
  3. 復(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----------

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容