身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

1 前言

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

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

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

2.初識鎖

1. 鎖的雙面性

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

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

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

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

凡事基本上都是雙面的,鎖可以在一定程度上保證數據的一致性,但是鎖也意味著維護和使用的複雜性,當然也伴隨著性能的損耗,我見過的最大的鎖可能就是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不均衡時造成線程飢餓,不過高併發大量請求的情況下還是可以的。

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

3. 單機鎖和分佈式鎖

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

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

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

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

3.分佈式鎖

1. 分佈式鎖的實現簡介

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


任何一個分佈式系統都無法同時滿足一致性Consistency、可用性Availability和分區容錯性Partition Tolerance三個方面,最多隻能同時滿足兩項。

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

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

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

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

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

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

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

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

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

在單Matster-Slave的Redis系統中,正常情況下Client向Master獲取鎖之後同步給Slave,如果Client獲取鎖成功之後Master節點掛掉,並且未將該鎖同步到Slave,之後在Sentinel的幫助下Slave升級為Master但是並沒有之前未同步的鎖的信息,此時如果有新的Client要在新Master獲取鎖,那麼將

可能出現兩個Client持有同一把鎖的問題,來看個圖來想下這個過程:

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

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

// 獲取鎖 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算法提出了一些質疑。

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

搜狗翻譯看一下:

1.我是劍橋大學計算機科學與技術系的高級研究助理和附屬講師,由勒弗烏爾姆信託早期職業獎學金和艾薩克牛頓信託基金資助。我致力於本地優先的協作軟件和分佈式系統安全

2.我也是劍橋科珀斯克里斯蒂學院計算機科學研究的研究員和主任,我在那裡從事本科教學。

3.2017年,我為奧雷利出版了一本名為《設計數據密集型應用》的書。它涵蓋了廣泛的數據庫和分佈式數據處理系統的體系結構,是該出版社最暢銷書之一。

4.我經常在會議上發言,我的演講錄音已經被觀看了超過15萬次。

5.我參與過各種開源項目,包括自動合併、Apache Avro和Apache Samza。

6.2007年至2014年間,我是一名工業軟件工程師和企業家。我共同創立了Rapportive(2012年被領英收購)和Go Test(2009年被紅門軟件收購)。

7.我創作了幾部音樂作品,包括《二月之死》(德語),這是唐克·德拉克特對該書的音樂戲劇改編,於2007年首映,共有150人參與。

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法


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

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

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

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

獲取鎖的客戶端在持有鎖時可能會暫停一段較長的時間,儘管鎖有一個超時時間,避免了崩潰的客戶端可能永遠持有鎖並且永遠不會釋放它,但是如果

客戶端的暫停持續的時間長於鎖的到期時間,並且客戶沒有意識到它已經到期,那麼它可能會繼續進行一些不安全的更改,換言之由於客戶端阻塞導致的持有的鎖到期而不自知

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

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

客戶端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發表文章之後世界各地的分佈式系統專家和愛好者都積極發表自己的看法,筆者在評論中發現了一個熟悉的名字:

身為一枚優秀的程序員必備的基於Redis的分佈式鎖和Redlock算法

文末點個關注,轉發下哦


分享到:


相關文章: