摘要
分散式鎖在很多應用場景下是非常有效的手段,比如當執行在多個機器上的不同程式需要訪問同一個競爭資源的時候,那麼就會涉及到程式對資源的加鎖和釋放,這樣才能保證資料的安全訪問。分散式鎖實現的方案有很多,比如基於ZooKeeper實現、或者基於Mysql實現等等,今天我們來一起看看如何基於Redis實現分散式鎖服務。
分散式鎖要點
對於分散式鎖的目標,我們必須首先明確三點:
- 任何一個時間點必須只能夠有一個客戶端擁有鎖。
- 不能夠有死鎖,也就是最終客戶端都能夠獲得鎖,儘管可能會經歷失敗。
- 錯誤容忍性要好,只要有大部分的Redis例項存活,客戶端就應該能夠獲得鎖。
一種簡單的方法
理解了上面我們列出的三個點,我們來分析一下一般的基於Redis實現的分散式鎖:
使用Redis實現鎖最簡單的辦法是建立一個key,且這個key通常有有限的存活時間,這一點可以利用Redis的過期時間特性,所以鎖最終會被釋放掉,當客戶端需要釋放資源的時候,客戶端delete這個key即可。
So far so good!但是有個單點問題,假如Redis master掛掉怎麼辦,因此我們需要加個slave,當master掛掉的時候可以切換到slave。這又帶來了新的問題,由於Redis的複製是非同步的,因此我們不能保證同時只有一個客戶端獲得鎖。
這個模型有很顯然的競態:
- Client在Master上面獲得了鎖。
- master在資料同步到slave之前掛掉了。
- slave升級成為master。
- Client B申請了同樣的資源的鎖,成功了!
在特定條件下這種情況是會發生的,當出現多個客戶端同時獲得鎖的時候,我們就認為可以這種鎖方案是不可靠的。
基於Redis單例的實現
為了後面更好的瞭解分散式鎖的實現,我們先來看看如何基於Redis單例實現鎖服務。我們可以用下面方法獲得鎖:
1 |
SET resource_name my_random_value NX PX 30000 |
上面的命令在只有當key不存在的時候會執行成功(NX選項),同時會設定過期時間為30000ms(PX選項)。key的值會被設定為my_random_value。這個值在多個客戶端和鎖中必須是唯一的,我們使用random value是為了方便安全地釋放鎖,看看下面的指令碼:
1 2 3 4 5 |
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end |
只有當key存在且值是預期的值的時候才會刪除key。這種方式可以避免誤刪除其他客戶端建立的鎖。例如,當客戶端獲取鎖之後執行一個很長時間的邏輯,一直過了鎖的過期時間,這個時候鎖會被自動釋放掉,而另外一個客戶端又獲取了這個鎖,前一個客戶端終於執行完了邏輯執行,回頭釋放鎖,刪除key,其實這個時候釋放的已經是另外一個客戶端持有的鎖了。使用DEL是不安全的,因為客戶端有可能誤刪其他客戶端持有的鎖。上面指令碼的方法的好處是每次獲得鎖的時候加上一個隨機的簽名,當釋放鎖的時候去看看是不是自己持有的鎖,這個時候就不會誤刪。
現在我們學會了如何在Redis單例上獲取鎖和釋放鎖,那麼接下來我們看看如何在Redis叢集上獲取鎖和釋放鎖。
基於Redlock演算法的實現
在分散式環境下,假設我們有N個master,這些節點都是獨立的,因此我們沒有配置複製策略。上面我們已經學會了如何在單機環境下獲取鎖和釋放鎖,我們假設的更具體一些,N=5,為了能獲取鎖,客戶端的步驟為:
- 獲取當前系統的時間,以毫秒為單位。
- 順序的獲取N個Redis例項上的鎖,在每個例項中都用同樣的key和value。在步驟2中,客戶端需要一個比過期時間小很多的超時時間,例如,如果自動過期時間為10s,那麼超時時間大概是5~50ms,這樣可以避免客戶端一直被阻塞,而不能繼續請求下一個例項。
- 客戶端每次都要計算已經過去了多長時間,使用的時間是否小於key自動過期的時間同時又獲取了至少3個例項的鎖。如果是,那麼我們認為客戶端此次獲取鎖成功。
- 如果鎖被獲取了,鎖的過期時間必須要減去獲取鎖花費的時間。
- 如果當前客戶端獲取鎖失敗,客戶端需要釋放所有之前獲取到的鎖。
總結
這篇文章主要介紹Redis實現分散式鎖的基本方法,然後分別介紹通過Redis單例和Redis叢集實現分散式鎖的方法。
參考文獻
《Redis官方文件》
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式