1. 程式人生 > >基於Redis的分散式鎖和Redlock演算法

基於Redis的分散式鎖和Redlock演算法

1 前言

前面寫了4篇Redis底層實現和工程架構相關文章,感興趣的讀者可以回顧一下:

  • Redis面試熱點之底層實現篇-1
  • Redis面試熱點之底層實現篇-2
  • Redis面試熱點之工程架構篇-1
  • Redis面試熱點之工程架構篇-2

今天開始來和大家一起學習一下Redis實際應用篇,會寫幾個Redis的常見應用。

在我看來Redis最為典型的應用就是作為分散式快取系統,其他的一些應用本質上並不是殺手鐗功能,是基於Redis支援的資料型別和分散式架構來實現的,屬於小而美的應用。

結合筆者的日常工作,今天和大家一起研究下基於Redis的分散式鎖和Redlock演算法的一些事情。

圖片來自網路

2.初識鎖

1. 鎖的雙面性

現在我們寫的程式基本上都有一定的併發性,要麼單臺多進執行緒、要麼多臺機器叢集化,在僅讀的場景下是不需要加鎖的,因為資料是一致的,在讀寫混合或者寫場景下如果不加以限制和約束就會造成寫混亂資料不一致的情況。

如果業務安全和正確性無法保證,再多的併發也是無意義的。

這個不由得讓我想起一個趣圖:

圖片來自網路

高併發多半是考驗你們公司的基礎架構是否強悍,合理正確地使用鎖才是個人能力的體現。

凡事基本上都是雙面的,鎖可以在一定程度上保證資料的一致性,但是鎖也意味著維護和使用的複雜性,當然也伴隨著效能的損耗,我見過的最大的鎖可能就是CPython直譯器的全域性直譯器鎖GIL了。

沒辦法 好可怕 那個鎖 不像話--《說鎖就鎖》

鎖使用不當不但解決不了資料混亂問題,甚至會帶來諸如死鎖等更多問題,通俗地說死鎖現象:

幾年前會出現這樣的場景:在異地需要買火車票回老家,但是身份證丟了無法購票,補辦身份證又需要本人坐火車回老家戶籍管理處,就這樣生活太難。

2. 無鎖化程式設計

既然鎖這麼難以把控,那不得不思考有沒有無鎖的高併發。

無鎖程式設計也是一個非常有意思的話題,後續可以寫一篇聊聊這個話題,本次就只提一下,要開啟思路,不要被困在凡是併發必須加鎖的思維定勢。

在某些特定場景下會選擇一種並行轉序列的思路,從而儘量避免鎖的使用,舉個栗子:

Post請求:http://abc.def/setdata?reqid=abc123789def&dbname=bighero

假如有一個上述的post請求的URI部分是個覆蓋寫操作,reqid=abc123789def,服務部署在多臺機器,在大前端將流量轉發到Nginx之後根據reqid進行雜湊,Nginx的配置大概是這樣的:

upstream myservice
{
  #根據引數進行Hash分配
  hash $urlkey;
  server localhost:5000;
  server localhost:5001;
  server localhost:5002;
}

經過Nginx負載均衡相同reqid的請求將被轉發到一臺機器上,當然你可能會說如果叢集的機器動態調整呢?我只能說不要考慮那麼多那麼充分,工程化去設計即可。

然而轉發到一臺機器仍然無法保證序列處理,因為單機仍然是多執行緒的,我們仍然需要將所有的reqid資料放到同一個執行緒處理,最終保證執行緒內序列,這個就需要藉助於執行緒池的管理者Disper按照reqid雜湊取模來進行多執行緒的負載均衡。

經過Nginx和執行緒內負載均衡,最終相同的reqid都將線上程內序列處理,有效避免了鎖的使用,當然這種設計可能在reqid不均衡時造成執行緒飢餓,不過高併發大量請求的情況下還是可以的。

只描述不畫圖 就等於沒說:

3. 單機鎖和分散式鎖

鎖依據使用範圍可簡單分為:單機鎖和分散式鎖。

Linux提供系統級單機鎖,這類鎖可以實現執行緒同步和互斥資源的共享,單機鎖實現了機器內部執行緒之間對共享資源的併發控制。

在分散式部署高併發場景下,經常會遇到資源的互斥訪問的問題,最有效最普遍的方法是給共享資源或者對共享資源的操作加一把鎖。

分散式鎖是控制分散式系統之間同步訪問共享資源的一種方式,用於在分散式系統中協調他們之間的動作。

3.分散式鎖

1. 分散式鎖的實現簡介

分散式CAP理論告訴我們需要做取捨:

任何一個分散式系統有三大特性:一致性Consistency、可用性Availability和分割槽容錯性Partition Tolerance,但是由於網路分割槽不受人為控制,在網路發生分割槽時,我們必須在可用性和一致性二者中選擇之一。

在網際網路領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只保證最終一致性。在很多場景中為了保證資料的最終一致性,需要很多的技術方案來支援,比如分散式事務、分散式鎖等。

分散式鎖一般有三種實現方式:

  • 基於資料庫
    在資料庫中建立一張表,表裡包含方法名等欄位,並且在方法名欄位上面建立唯一索引,執行某個方法需要使用此方法名向表中插入資料,成功插入則獲取鎖,執行結束則刪除對應的行資料釋放鎖
  • 基於快取資料庫Redis
    Redis效能好並且實現方便,但是單節點的分散式鎖在故障遷移時產生安全問題,Redlock是Redis的作者 Antirez 提出的叢集模式分散式鎖,基於N個完全獨立的Redis節點實現分散式鎖的高可用
  • 基於ZooKeeper
    ZooKeeper 是以 Paxos 演算法為基礎的分散式應用程式協調服務,為分散式應用提供一致性服務的開源元件

2. 分散式鎖需要具備的條件

分散式鎖在應用於分散式系統環境相比單機鎖更為複雜,本文講述基於Redis的分散式鎖實現,該鎖需要具備一些特性:

  • 互斥性
    在任意時刻,只有一個客戶端能持有鎖 其他嘗試獲取鎖的客戶端都將失敗而返回或阻塞等待
  • 健壯性
    一個客戶端持有鎖的期間崩潰而沒有主動釋放鎖,也需要保證後續其他客戶端能夠加鎖成功,就像C++的智慧指標來避免記憶體洩漏一樣
  • 唯一性
    加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給釋放了,自己持有的鎖也不能被其他客戶端釋放
  • 高可用
    不必依賴於全部Redis節點正常工作,只要大部分的Redis節點正常執行,客戶端就可以進行加鎖和解鎖操作

3. 基於單Redis節點的分散式鎖

本文的重點是基於多Redis節點的Redlock演算法,不過在展開這個演算法之前,有必要提一下單Redis節點分散式鎖原理以及演進,因為Redlock演算法是基於此改進的。

最初分散式鎖藉助於setnx和expire命令,但是這兩個命令不是原子操作,如果執行setnx之後獲取鎖但是此時客戶端掛掉,這樣無法執行expire設定過期時間就導致鎖一直無法被釋放,因此在2.8版本中Antirez為setnx增加了引數擴充套件,使得setnx和expire具備原子操作性。

在單Matster-Slave的Redis系統中,正常情況下Client向Master獲取鎖之後同步給Slave,如果Client獲取鎖成功之後Master節點掛掉,並且未將該鎖同步到Slave,之後在Sentinel的幫助下Slave升級為Master但是並沒有之前未同步的鎖的資訊,此時如果有新的Client要在新Master獲取鎖,那麼將可能出現兩個Client持有同一把鎖的問題,來看個圖來想下這個過程:

為了保證自己的鎖只能自己釋放需要增加唯一性的校驗,綜上基於單Redis節點的獲取鎖和釋放鎖的簡單過程,使用lua指令碼實現如下:

// 獲取鎖 unique_value作為唯一性的校驗
SET resource_name unique_value NX PX 30000

// 釋放鎖 比較unique_value是否相等 避免誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

這就是基於單Redis的分散式鎖的幾個要點。

4.Redlock演算法過程

Redlock演算法是Antirez在單Redis節點基礎上引入的高可用模式。

在Redis的分散式環境中,我們假設有N個完全互相獨立的Redis節點,在N個Redis例項上使用與在Redis單例項下相同方法獲取鎖和釋放鎖。

現在假設有5個Redis主節點(大於3的奇數個),這樣基本保證他們不會同時都宕掉,獲取鎖和釋放鎖的過程中,客戶端會執行以下操作:

  • 1.獲取當前Unix時間,以毫秒為單位
  • 2.依次嘗試從5個例項,使用相同的key和具有唯一性的value獲取鎖
    當向Redis請求獲取鎖時,客戶端應該設定一個網路連線和響應超時時間,這個超時時間應該小於鎖的失效時間,這樣可以避免客戶端死等
  • 3.客戶端使用當前時間減去開始獲取鎖時間就得到獲取鎖使用的時間。當且僅當從半數以上的Redis節點取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功
  • 4.如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間,這個很重要
  • 5.如果因為某些原因,獲取鎖失敗(沒有在半數以上例項取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis例項上進行解鎖,無論Redis例項是否加鎖成功,因為可能服務端響應訊息丟失了但是實際成功了,畢竟多釋放一次也不會有問題

上述的5個步驟是Redlock演算法的重要過程,也是面試的熱點,有心的讀者還是記錄一下吧!

5.Redlock演算法是否安全的爭論

1.關於馬丁·克萊普曼博士

2016年2月8號分散式系統的專家馬丁·克萊普曼博士(Martin Kleppmann)在一篇文章How to do distributed locking 指出分散式鎖設計的一些原則並且對Antirez的Redlock演算法提出了一些質疑。

筆者找到了馬丁·克萊普曼博士的個人網站以及一些簡介,一起看下:

用搜狗翻譯看一下:

1.我是劍橋大學電腦科學與技術系的高階研究助理和附屬講師,由勒弗烏爾姆信託早期職業獎學金和艾薩克牛頓信託基金資助。我致力於本地優先的協作軟體和分散式系統安全。
2.我也是劍橋科珀斯克里斯蒂學院電腦科學研究的研究員和主任,我在那裡從事本科教學。
3.2017年,我為奧雷利出版了一本名為《設計資料密集型應用》的書。它涵蓋了廣泛的資料庫和分散式資料處理系統的體系結構,是該出版社最暢銷書之一。
4.我經常在會議上發言,我的演講錄音已經被觀看了超過15萬次。
5.我參與過各種開源專案,包括自動合併、Apache Avro和Apache Samza。
6.2007年至2014年間,我是一名工業軟體工程師和企業家。我共同創立了Rapportive(2012年被領英收購)和Go Test(2009年被紅門軟體收購)。
7.我創作了幾部音樂作品,包括《二月之死》(德語),這是唐克·德拉克特對該書的音樂戲劇改編,於2007年首映,共有150人蔘與。

大牛就是大牛,能教書、能出書、能寫開源軟體、能創業、能寫音樂劇,優秀的人哪方面也優秀,服氣了。

圖片來自網路

2.馬丁博士文章的主要觀點

馬丁·克萊普曼在文章中談及了分散式系統的很多基礎問題,特別是分散式計算的非同步模型,文章分為兩大部分前半部分講述分散式鎖的一些原則,後半部分針對Redlock提出一些看法:

  • Martin指出即使我們擁有一個完美實現的分散式鎖,在沒有共享資源參與進來提供某種fencing柵欄機制的前提下,我們仍然不可能獲得足夠的安全性
  • Martin指出,由於Redlock本質上是建立在一個同步模型之上,對系統的時間有很強的要求,本身的安全性是不夠的

針對fencing機制馬丁給出了一個時序圖:

獲取鎖的客戶端在持有鎖時可能會暫停一段較長的時間,儘管鎖有一個超時時間,避免了崩潰的客戶端可能永遠持有鎖並且永遠不會釋放它,但是如果客戶端的暫停持續的時間長於鎖的到期時間,並且客戶沒有意識到它已經到期,那麼它可能會繼續進行一些不安全的更改,換言之由於客戶端阻塞導致的持有的鎖到期而不自知。

針對這種情況馬丁指出要增加fencing機制,具體來說是fencing token隔離令牌機制,同樣給出了一張時序圖:

客戶端1獲得鎖並且獲得序號為33的令牌,但隨後它進入長時間暫停,直至鎖超時過期,客戶端2獲取鎖並且獲得序號為34的令牌,然後將其寫入傳送到儲存服務。隨後,客戶端1復活並將其寫入傳送到儲存服務,然而儲存伺服器記得它已經處理了具有較高令牌號的寫入34,因此它拒絕令牌33的請求。
Redlock演算法並沒有這種唯一且遞增的fencing token生成機制,這也意味著Redlock演算法不能避免由於客戶端阻塞帶來的鎖過期後的操作問題,因此是不安全的。

這個觀點筆者覺得並沒有徹底解決問題,因為如果客戶端1的寫入操作是必須要執行成功的,但是由於阻塞超時無法再寫入同樣就產生了一個錯誤的結果,客戶端2將可能在這個錯誤的結果上進行操作,那麼任何操作都註定是錯誤的。

 

3.馬丁博士對Redlock的質疑

馬丁·克萊普曼指出Redlock是個強依賴系統時間的演算法,這樣就可能帶來很多不一致問題,他給出了個例子一起看下:

假設多節點Redis系統有五個節點A/B/C/D/E和兩個客戶端C1和C2,如果其中一個Redis節點上的時鐘向前跳躍會發生什麼?

  • 客戶端C1獲得了對節點A、B、c的鎖定,由於網路問題,法到達節點D和節點E
  • 節點C上的時鐘向前跳,導致鎖提前過期
  • 客戶端C2在節點C、D、E上獲得鎖定,由於網路問題,無法到達A和B
  • 客戶端C1和客戶端C2現在都認為他們自己持有鎖

分散式非同步模型:
上面這種情況之所以有可能發生,本質上是因為Redlock的安全性對Redis節點系統時鐘有強依賴,一旦系統時鐘變得不準確,演算法的安全性也就無法保證。

馬丁其實是要指出分散式演算法研究中的一些基礎性問題,好的分散式演算法應該基於非同步模型,演算法的安全性不應該依賴於任何記時假設。

分散式非同步模型中程序和訊息可能會延遲任意長的時間,系統時鐘也可能以任意方式出錯。這些因素不應該影響它的安全性,只可能影響到它的活性,即使在非常極端的情況下,演算法最多是不能在有限的時間內給出結果,而不應該給出錯誤的結果,這樣的演算法在現實中是存在的比如Paxos/Raft,按這個標準衡量Redlock的安全級別是達不到的。

4.馬丁博士文章結論和基本觀點

馬丁表達了自己的觀點,把鎖的用途分為兩種:

  • 效率第一
    使用分散式鎖只是為了協調多個客戶端的一些簡單工作,鎖偶爾失效也會產生其它的不良後果,就像你收發兩份相同的郵件一樣,無傷大雅
  • 正確第一
    使用分散式鎖要求在任何情況下都不允許鎖失效的情況發生,一旦發生失效就可能意味著資料不一致、資料丟失、檔案損壞或者其它嚴重的問題,就像給患者服用重複劑量的藥物一樣,後果嚴重

最後馬丁出了如下的結論:

  • 為了效率而使用分散式鎖
    單Redis節點的鎖方案就足夠了Redlock則是個過重而昂貴的設計
  • 為了正確而使用分散式鎖
    Redlock不是建立在非同步模型上的一個足夠強的演算法,它對於系統模型的假設中包含很多危險的成分

馬丁認為Redlock演算法是個糟糕的選擇,因為它不倫不類:出於效率選擇來說,它過於重量級和昂貴,出於正確性選擇它又不夠安全。

6.Antirez的反擊

馬丁的那篇文章是在2016.2.8發表之後Antirez反應很快,他發表了"Is Redlock safe?"進行逐一反駁,文章地址如下:http://antirez.com/news/101

Antirez認為馬丁的文章對於Redlock的批評可以概括為兩個方面:

  • 帶有自動過期功能的分散式鎖,必須提供某種fencing柵欄機制來保證對共享資源的真正互斥保護,Redlock演算法提供不了這樣一種機制
  • Redlock演算法構建在一個不夠安全的系統模型之上,它對於系統的記時假設有比較強的要求,而這些要求在現實的系統中是無法保證的

Antirez對這兩方面分別進行了細緻地反駁。

關於fencing機制

Antirez提出了質疑:既然在鎖失效的情況下已經存在一種fencing機制能繼續保持資源的互斥訪問了,那為什麼還要使用一個分散式鎖並且還要求它提供那麼強的安全性保證呢?

退一步講Redlock雖然提供不了遞增的fencing token隔離令牌,但利用Redlock產生的隨機字串可以達到同樣的效果,這個隨機字串雖然不是遞增的,但卻是唯一的。

關於記時假設

Antirez針對演算法在記時模型假設集中反駁,馬丁認為Redlock失效情況主要有三種:

  • 1.時鐘發生跳躍
  • 2.長時間的GC pause
  • 3.長時間的網路延遲

後兩種情況來說,Redlock在當初之處進行了相關設計和考量,對這兩種問題引起的後果有一定的抵抗力。
時鐘跳躍對於Redlock影響較大,這種情況一旦發生Redlock是沒法正常工作的。
Antirez指出Redlock對系統時鐘的要求並不需要完全精確,只要誤差不超過一定範圍不會產生影響,在實際環境中是完全合理的,通過恰當的運維完全可以避免時鐘發生大的跳動。

7.馬丁的總結和思考

分散式系統本身就很複雜,機制和理論的效果需要一定的數學推導作為依據,馬丁和Antirez都是這個領域的專家,對於一些問題都會有自己的看法和思考,更重要的是很多時候問題本身並沒有完美的解決方案。

這次爭論是分散式系統領域非常好的一次思想的碰撞,很多網友都發表了自己的看法和認識,馬丁博士也在Antirez做出反應一段時間之後再次發表了自己的一些觀點:

For me, this is the most important point: I don’t care who is right or wrong in this debate — I care about learning from others’ work, so that we can avoid repeating old mistakes, and make things better in future. So much great work has already been done for us: by standing on the shoulders of giants, we can build better software.
By all means, test ideas by arguing them and checking whether they stand up to scrutiny by others. That’s part of the learning process. But the goal should be to learn, not to convince others that you are right. Sometimes that just means to stop and think for a while.

簡單翻譯下就是:
對馬丁而言並不在乎誰對誰錯,他更關心於從他人的工作中汲取經驗來避免自己的錯誤重複工作,正如我們是站在巨人的肩膀上才能做出更好的成績。

另外通過別人的爭論和檢驗才更能讓自己的想法經得起考驗,我們的目標是相互學習而不是說服別人相信你是對的,所謂一人計短,思考辯駁才能更加接近真理。

在Antirez發表文章之後世界各地的分散式系統專家和愛好者都積極發表自己的看法,筆者在評論中發現了一個熟悉的名字:

8.巨人的肩膀

  • https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
  • http://antirez.com/news/101
  • http://zhangtielei.com/posts/blog-redlock-reasoning.html
  • http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html

鐵蕾大神的兩篇文章寫的非常好,本文從中做了很多參考,也是鐵蕾大神的文章讓筆者瞭解到這場精彩的華山論劍,感興趣的可以直接搜尋閱讀參考3和4。

9.關於我