基於Redis的分佈式鎖和Redlock算法

本文首發於: 基於Redis的分佈式鎖和Redlock算法
微信公衆號:後端技術指南針

1 前言html

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

今天開始來和你們一塊兒學習一下Redis實際應用篇,會寫幾個Redis的常見應用。面試

在我看來Redis最爲典型的應用就是做爲分佈式緩存系統,其餘的一些應用本質上並非殺手鐗功能,是基於Redis支持的數據類型和分佈式架構來實現的,屬於小而美的應用。redis

結合筆者的平常工做,今天和你們一塊兒研究下基於Redis的分佈式鎖和Redlock算法的一些事情算法

                      
                                                                圖片來自網絡

2.初識鎖

1. 鎖的雙面性數據庫

如今咱們寫的程序基本上都有必定的併發性,要麼單臺多進線程、要麼多臺機器集羣化,在僅讀的場景下是不須要加鎖的,由於數據是一致的,在讀寫混合或者寫場景下若是不加以限制和約束就會形成寫混亂數據不一致的狀況。編程

若是業務安全和正確性沒法保證,再多的併發也是無心義的。後端

這個不禁得讓我想起一個趣圖:緩存

                                   

                                                      圖片來自網絡

高併發多半是考驗大家公司的基礎架構是否強悍,合理正確地使用鎖纔是我的能力的體現。安全

凡事基本上都是雙面的,鎖能夠在必定程度上保證數據的一致性,可是鎖也意味着維護和使用的複雜性,固然也伴隨着性能的損耗,我見過的最大的鎖可能就是CPython解釋器的全局解釋器鎖GIL了。

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

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

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

2. 無鎖化編程

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

無鎖編程也是一個很是有意思的話題,後續能夠寫一篇聊聊這個話題,本次就只提一下,要打開思路,不要被困在凡是併發必須加鎖的思惟定勢

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

Post請求: abc.def/setdata?req…

假若有一個上述的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?"進行逐一反駁,文章地址以下: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.巨人的肩膀

鐵蕾大神的兩篇文章寫的很是好,本文從中作了不少參考,也是鐵蕾大神的文章讓筆者瞭解到這場精彩的華山論劍,感興趣的能夠直接搜索閱讀參考3和4。

9.關於我

                           
相關文章
相關標籤/搜索