NodeJS 基於redis的分散式鎖的實現(Redlock演算法)

炎灸紋舞發表於2018-12-10

1. 前言

開發時,碰到互斥問題,需要保證在分散式環境下,避免重複性操作修改使用者狀態,如:使用者訂單狀態,購票時,修改票的餘額等

2. 分散式鎖的條件

  • 分散式鎖需要滿足下列條件
    • 鎖需要有充足的可訪問的儲存空間
    • 鎖必須被唯一標識
    • 鎖至少要有兩種狀態
  • 同時,要保證
    • 安全特性:互斥訪問,永遠只有一個client能拿到鎖
    • 避免死鎖:client最後可以拿到鎖,不會出現死鎖,即使原本上鎖的client出現問題無法解鎖
    • 容錯性:容錯,只要大多數redis節點能夠正常工作,客戶端端都能獲取和釋放鎖。

3.Redis單節點上鎖的實現

  • 使用下列語句獲取一個不存在的key,如果可以已存在則建立失敗,確保key值唯一, 加上過期時間,確保系統錯誤後及時解鎖,避免死鎖
 SET key value NX PX 30000
複製程式碼

但是這種方法在主庫錯誤時,會發生錯誤,redis主從同步是非同步,主庫錯誤時,從庫若還沒有鎖的資訊,則會導致多個程式持有鎖

3. Redlock演算法(官方文件

在分散式版本的演算法裡我們假設我們有N個Redis master節點,這些節點都是完全獨立的,我們不用任何複製或者其他隱含的分散式協調演算法。我們已經描述瞭如何在單節點環境下安全地獲取和釋放鎖。因此我們理所當然地應當用這個方法在每個單節點裡來獲取和釋放鎖。在我們的例子裡面我們把N設成5,這個數字是一個相對比較合理的數值,因此我們需要在不同的計算機或者虛擬機器上執行5個master節點來保證他們大多數情況下都不會同時當機。一個客戶端需要做如下操作來獲取鎖:

  • 獲取當前時間(單位是毫秒)。
  • 輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。
  • 客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。
  • 如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。
  • 如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。

4.使用示例

  • 首先安裝redis庫與redlock庫(官方庫,在git上名叫node-redlock),在node專案中,直接使用npm或者yarn下載安裝即可,redis庫我使用的是ioredis(直接安裝redis庫也可)
  npm i --save ioredis
  npm i --save redlock
複製程式碼
/** 請求鎖 */
Redlock.prototype.lock(resource, ttl, ?callback) 
// resource鎖的名稱
// ttl鎖的有效期
// callback 回撥函式,當使用promise的寫法的時候,可以填寫這個引數
/** 釋放鎖 */
Redlock.prototype.unlock(lock, ?callback)
/** 延長鎖的有效時間 */
Lock.prototype.extend(lock, ttl, ?callback)
// 都支援callback、promise、yield寫法
複製程式碼
  • 程式碼示例
const ioredis = require('ioredis')
const Redlock = require('redlock')
const client = new ioredis(REDIS_SERVER)
const redlock = new Redlock([client])
co(function* () {
   while (true) {
       let lock = null
       try {
           lock = yield redlock.lock('lock', 1000) // 這種寫法取不到鎖時會直接丟擲錯誤
       } catch (error) {
           lock = null
       }
       yield sleep(30 * 1000)
       // 處理邏輯
       lock.unlock()
   }
})
複製程式碼

相關文章