在 course-se 的提交服務中,爲了限制同一用戶在規定時間(5秒)內,沒法進行二次提交,開發人員實現了基於 Redis 的分佈式鎖。一般,咱們稱該業務場景爲節流(Throttle)。javascript
在閱讀此部分代碼時,我一開始尋思着徹底可使用一個 Map 維護各個用戶及其剩餘時間的關係,何須使用 Redis 。後來,通過了仔細思考,我忽略了服務端服務是以集羣(Cluster) 的形式進行部署的,同一個用戶的請求可能被轉發至不一樣的 Node 進程,所以咱們須要實現分佈式鎖服務,而不是進程內的鎖服務。java
爲了提供分佈式鎖服務,咱們須要確保如下幾點:node
course-se 的現有實現,是創建 Redis 服務是單節點的前提上,咱們分別探討現有實現如何確保互斥訪問和避免死鎖。git
在實現中,咱們經過 Lock
類的 acquire
函數實現互斥操做,該函數調用了 getset
原子指令判斷當前 key 是否已被設置:若未被設置,則設置值,表示可獲取鎖;不然,表示鎖已被佔用,不可獲取鎖。在對應 key 對應的值爲空時,客戶端獲取鎖,並設置值的過時時間,避免出現因客戶端未手動解鎖形成的死鎖問題,實現的核心代碼以下。github
async acquire() {
const key = this.genKey();
// 若對應值爲空,表示可獲取鎖,則設置該值,獲取鎖;
// 若對應值不爲空,表示鎖已被佔用,不可獲取。
const val = await this.tryExec('getset', key, '1');
// 設置值的過時時間,避免出現因客戶端未手動解鎖形成的死鎖問題。
await this.tryExec('expire', key, this.expireAfter);
return val === null;
}
genKey() {
const { curUser: { user_id }, asgn: { asgn_id }, ce: { isExam } } = this.paramData;
return `submit-asgn:${user_id}-${asgn_id}-${Number(isExam)}`;
}
複製代碼
其實,上述代碼是存在問題的,咱們假象如下的場景:現有服務節點 node1 和 node2 ,node1 接收了一個用戶請求,成功調用 getset
獲取到鎖,但在準備設置過時時間(假設5s)時,node1 意外退出了,隨後 node2 接收到一樣的用戶請求,在調用 getset
沒法獲取鎖後,其執行了 expire
命令,爲 node1 的鎖進行續期,若在到期時間內,該用戶一直髮送同一請求,則致使該鎖沒法被釋放,形成死鎖。redis
爲了解決上述的潛在問題,咱們須要使用 SET key value NX PX expireAfter
原子指令,替換上述 getset
和 expire
命令組合,咱們可根據命令操做結果是否爲空來判斷鎖的佔用狀況。算法
async acquire() {
const key = this.genKey();
// 若 ret 不爲空,表示已獲取鎖,不然表示鎖已被佔用。
// px 參數保證了值在必定時間後會過時,避免了死鎖。
const ret = await this.tryExec('set', key, '1', 'nx', 'px', this.expireAfter);
return ret === null;
}
複製代碼
函數 release
可用於鎖的釋放,具體代碼以下。npm
async release() {
return this.tryExec('del', this.genKey());
}
複製代碼
上述釋放鎖的代碼也是存在潛在問題的,設想這樣的場景:node1 已獲取到鎖(其過時時間爲 5s),但因爲各類緣由,node1 花了 6s 的時間才完成業務,那麼在第 5 秒時,鎖已過時,若此時 node2 獲取到了該鎖,則在第 6 秒時,node1 手動調用的 release
將會釋放 node2 獲取到的鎖,進而給其餘節點提供了獲取鎖的可能性。bash
解決上述問題的方法是:在釋放鎖以前,判斷該鎖是不是本身獲取的。至於具體的實現,咱們能夠在加鎖的時候,把當前節點的惟一標識符設置爲 key 對應的 value ,並在釋放鎖以前,檢查 key 對應的 value 是否爲本身的惟一標識符。異步
但在現有的 Redis 指令集中,咱們並不能實如今 get
和 del
的原子操做,所以咱們只能使用 Lua 腳本。
// 定義 Lua 腳本實現 get 和 del 的原子操做。
this.redis.defineCommand('lua_unlock', {
numberOfKeys: 1,
lua : ` local remote_value = redis.call("get",KEYS[1]) if (not remote_value) then return 0 elseif (remote_value == ARGV[1]) then return redis.call("del",KEYS[1]) else return -1 end`
});
複製代碼
對於單節點鎖的實現,若是 Redis 是以主從方式進行部署的,會發生錯誤,這是由於主從同步是異步的,當主庫發生異常時,從庫還未獲取到鎖的信息,則可能致使多個進程持有鎖,考慮如下場景:
針對此狀況,咱們可以使用 Redlock 算法,實現魯棒性更優的分佈式鎖。假設如今有 個 Redis Master 節點,節點與節點之間徹底獨立,沒有使用分佈式協調算法,在這種狀況下客戶端獲取鎖的流程以下:
初步來看,Redlock 算法流程仍是很清晰的,在實際生產中,咱們可以使用 redlock
包提供強魯棒的分佈式鎖服務。
npm install --save redlock
複製代碼
關於 redlock
的具體使用方法可查看其說明文檔。