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

1 前言前端

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

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

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

身爲一枚優秀的程序員必備的基於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算法是否安全的爭論

相關文章
相關標籤/搜索