掃描下方海報二維碼,試聽課程:
html
(課程詳細大綱,請參見文末)程序員
===================================面試
本文來源:朱小廝的博客
redis
===================================
算法
在單實例JVM中,常見的處理併發問題的方法有不少,好比synchronized關鍵字進行訪問控制、volatile關鍵字、ReentrantLock等經常使用方法。數據庫
可是在分佈式環境中,上述方法卻不能在跨JVM場景中用於處理併發問題,當業務場景須要對分佈式環境中的併發問題進行處理時,須要使用分佈式鎖來實現。緩存
分佈式鎖,是指在分佈式的部署環境下,經過鎖機制來讓多客戶端互斥的對共享資源進行訪問。安全
目前比較常見的分佈式鎖實現方案有如下幾種:bash
基於數據庫,如MySQL網絡
基於緩存,如Redis
基於Zookeeper、etcd等。
在上一篇《基於數據庫實現分佈式鎖》中介紹瞭如何基於數據庫實現分佈式鎖,這裏介紹一下如何使用緩存(Redis)實現分佈式鎖。
使用Redis實現分佈式鎖最簡單的方案是使用命令SETNX。
SETNX(SET if Not eXist)的使用方式爲:SETNX key value,只在鍵key不存在的狀況下,將鍵key的值設置爲value,若鍵key存在,則SETNX不作任何動做。
SETNX在設置成功時返回,設置失敗時返回0。當要獲取鎖時,直接使用SETNX獲取鎖,當要釋放鎖時,使用DEL命令刪除掉對應的鍵key便可。
上面這種方案有一個致命問題,就是某個線程在獲取鎖以後因爲某些異常因素(好比宕機)而不能正常的執行解鎖操做,那麼這個鎖就永遠釋放不掉了。
爲此,咱們能夠爲這個鎖加上一個超時時間,第一時間咱們會聯想到Redis的EXPIRE命令(EXPIRE key seconds)。
可是這裏咱們不能使用EXPIRE來實現分佈式鎖,由於它與SETNX一塊兒是兩個操做,在這兩個操做之間可能會發生異常,從而仍是達不到預期的結果,示例以下:
// STEP 1SETNX key value// 若在這裏(STEP1和STEP2之間)程序忽然崩潰,則沒法設置過時時間,將有可能沒法釋放鎖// STEP 2EXPIRE key expireTime複製代碼
對此,正確的姿式應該是使用「SET key value [EX seconds] [PX milliseconds] [NX|XX]」這個命令。
從 Redis 2.6.12 版本開始, SET 命令的行爲能夠經過一系列參數來修改:
EX seconds :將鍵的過時時間設置爲 seconds 秒。執行 SET key value EX seconds 的效果等同於執行 SETEX key seconds value 。
PX milliseconds :將鍵的過時時間設置爲 milliseconds 毫秒。執行 SET key value PX milliseconds 的效果等同於執行 PSETEX key milliseconds value 。
NX :只在鍵不存在時, 纔對鍵進行設置操做。執行 SET key value NX 的效果等同於執行 SETNX key value 。
XX :只在鍵已經存在時, 纔對鍵進行設置操做。
舉例,咱們須要建立一個分佈式鎖,而且設置過時時間爲10s,那麼能夠執行如下命令:
SET lockKey lockValue EX 10 NX或者SET lockKey lockValue PX 10000 NX複製代碼
注意EX和PX不能同時使用,不然會報錯:ERR syntax error。
解鎖的時候仍是使用DEL命令來解鎖。
修改以後的方案看上去很完美,但實際上仍是會有問題。
試想一下,某線程A獲取了鎖而且設置了過時時間爲10s,而後在執行業務邏輯的時候耗費了15s,此時線程A獲取的鎖早已被Redis的過時機制自動釋放了。
在線程A獲取鎖並通過10s以後,改鎖可能已經被其它線程獲取到了。當線程A執行完業務邏輯準備解鎖(DEL key)的時候,有可能刪除掉的是其它線程已經獲取到的鎖。
因此最好的方式是在解鎖時判斷鎖是不是本身的,咱們能夠在設置key的時候將value設置爲一個惟一值uniqueValue(能夠是隨機值、UUID、或者機器號+線程號的組合、簽名等)。
當解鎖時,也就是刪除key的時候先判斷一下key對應的value是否等於先前設置的值,若是相等才能刪除key,僞代碼示例以下:
if uniqueKey == GET(key) { DEL key}複製代碼
這裏咱們一眼就能夠看出問題來:GET和DEL是兩個分開的操做,在GET執行以後且在DEL執行以前的間隙是可能會發生異常的。
若是咱們只要保證解鎖的代碼是原子性的就能解決問題了。
這裏咱們引入了一種新的方式,就是Lua腳本,示例以下:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end複製代碼
其中ARGV[1]表示設置key時指定的惟一值。
因爲Lua腳本的原子性,在Redis執行該腳本的過程當中,其餘客戶端的命令都須要等待該Lua腳本執行完才能執行。
下面咱們使用Jedis來演示一下獲取鎖和解鎖的實現,具體以下:
public boolean lock(String lockKey, String uniqueValue, int seconds){ SetParams params = new SetParams(); params.nx().ex(seconds); String result = jedis.set(lockKey, uniqueValue, params); if ("OK".equals(result)) { return true; } return false;}public boolean unlock(String lockKey, String uniqueValue){ String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue)); if (result.equals(1)) { return true; } return false;}複製代碼
如此就萬無一失了嗎?
顯然不是!
表面來看,這個方法彷佛很管用,可是這裏存在一個問題:在咱們的系統架構裏存在一個單點故障,若是Redis的master節點宕機了怎麼辦呢?
有人可能會說:加一個slave節點!在master宕機時用slave就好了!
可是其實這個方案明顯是不可行的,由於Redis的複製是異步的。
舉例來講:
線程A在master節點拿到了鎖。
master節點在把A建立的key寫入slave以前宕機了。
slave變成了master節點。
線程B也獲得了和A還持有的相同的鎖。(由於原來的slave裏面尚未A持有鎖的信息)
固然,在某些場景下這個方案沒有什麼問題,好比業務模型容許同時持有鎖的狀況,那麼使用這種方案也何嘗不可。
舉例說明,某個服務有2個服務實例A和B,初始狀況下A獲取了鎖而後對資源進行操做(能夠假設這個操做很耗費資源),B沒有獲取到鎖而不執行任何操做,此時B能夠看作是A的熱備。
當A出現異常時,B能夠「轉正」,當鎖出現異常時,好比Redis master宕機,那麼B可能會同時持有鎖而且對資源進行操做,若是操做的結果是冪等的(或者其它狀況),那麼也可使用這種方案。
這裏引入分佈式鎖可讓服務在正常狀況下避免重複計算而形成資源的浪費。
爲了應對這種狀況,antriez提出了Redlock算法。
Redlock算法的主要思想是:假設咱們有N個Redis master節點,這些節點都是徹底獨立的,咱們能夠運用前面的方案來對前面單個的Redis master節點來獲取鎖和解鎖
若是咱們整體上能在合理的範圍內或者N/2+1個鎖,那麼咱們就能夠認爲成功得到了鎖,反之則沒有獲取鎖(可類比Quorum模型)。
雖然Redlock的原理很好理解,可是其內部的實現細節非常複雜,要考慮不少因素
具體內容能夠參考:https://redis.io/topics/distlock。
Redlock算法也並不是是「銀彈」,他除了條件有點苛刻外,其算法自己也被質疑。
關於Redis分佈式鎖的安全性問題,在分佈式系統專家Martin Kleppmann和Redis的做者antirez之間就發生過一場爭論。這場爭論的內容大體以下:
Martin Kleppmann發表了一篇blog,名字叫」How to do distributed locking 「
地址爲:
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Martin在這篇文章中談及了分佈式系統的不少基礎性的問題(特別是分佈式計算的異步模型),對分佈式系統的從業者來講很是值得一讀。
Martin的那篇文章是在2016-02-08這一天發表的,但據Martin說,他在公開發表文章的一星期以前就把草稿發給了antirez進行review,並且他們之間經過email進行了討論。
不知道Martin有沒有意料到,antirez對於此事的反應很快,就在Martin的文章發表出來的次日,antirez就在他的博客上貼出了他對於此事的反駁文章,名字叫」Is Redlock safe?」,地址爲http://antirez.com/news/101。
這是高手之間的過招。antirez這篇文章也條例很是清晰,而且中間涉及到大量的細節。antirez認爲,Martin的文章對於Redlock的批評能夠歸納爲兩個方面(與Martin文章的先後兩部分對應):
帶有自動過時功能的分佈式鎖,必須提供某種fencing機制來保證對共享資源的真正的互斥保護。Redlock提供不了這樣一種機制。
Redlock構建在一個不夠安全的系統模型之上。它對於系統的記時假設(timing assumption)有比較強的要求,而這些要求在現實的系統中是沒法保證的。
antirez對這兩方面分別進行了反駁。
首先,關於fencing機制。antirez對於Martin的這種論證方式提出了質疑:
既然在鎖失效的狀況下已經存在一種fencing機制能繼續保持資源的互斥訪問了,那爲何還要使用一個分佈式鎖而且還要求它提供那麼強的安全性保證呢?
即便退一步講,Redlock雖然提供不了Martin所講的遞增的fencing token,但利用Redlock產生的隨機字符串(my_random_value)能夠達到一樣的效果。
這個隨機字符串雖然不是遞增的,但倒是惟一的,能夠稱之爲unique token。
而後,antirez的反駁就集中在第二個方面上:關於算法在記時(timing)方面的模型假設。
在咱們前面分析Martin的文章時也提到過,Martin認爲Redlock會失效的狀況主要有三種:
1. 時鐘發生跳躍;
2. 長時間的GC pause;
3. 長時間的網絡延遲。
antirez確定意識到了這三種狀況對Redlock最致命的實際上是第一點:時鐘發生跳躍。這種狀況一旦發生,Redlock是無法正常工做的。
而對於後兩種狀況來講,Redlock在當初設計的時候已經考慮到了,對它們引發的後果有必定的免疫力。
因此,antirez接下來集中精力來講明經過恰當的運維,徹底能夠避免時鐘發生大的跳動,而Redlock對於時鐘的要求在現實系統中是徹底能夠知足的。
神仙打架,咱們站旁邊看看就好。拋開這個層面而言,在理解Redlock算法時要理解「各個節點徹底獨立」這個概念。
Redis自己有幾種部署模式:單機模式、主從模式、哨兵模式、集羣模式。
好比採用集羣模式部署,若是須要5個節點,那麼就須要部署5個Redis Cluster集羣。
很顯然,這種要求每一個master節點都獨立的Redlock算法條件有點苛刻,使用它所須要耗費的資源比較多,並且對每一個節點都請求一次鎖所帶來的額外開銷也不可忽視。除非有實實在在的業務應用需求,或者有資源能夠複用。
使用Redis分佈式鎖並不能作到萬無一失。通常而言,Redis分佈式鎖的優點在於性能,而若是要考慮到可靠性,那麼Zookeeper、etcd這類的組件會比Redis要高。
固然,在合適的環境下使用基於數據庫實現的分佈式鎖會更合適
不過就以可靠性而言,沒有任何組件是徹底可靠的,程序員的價值不只僅在於表象地如何靈活運用這些組件,而在於如何基於這些不可靠的組件構建一個可靠的系統。
仍是那句老話,選擇何種方案,合適最重要。
參考資料:
https://redis.io/topics/distlock
https://www.jianshu.com/p/7e47a4503b87
http://ifeve.com/redis-lock/
https://www.cnblogs.com/linjiqin/p/8003838.html
http://zhangtielei.com/posts/blog-redlock-reasoning.html
http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html
《21天互聯網Java進階面試訓練營(分佈式篇)》詳細目錄,掃描圖片末尾的二維碼,試聽課程