基於Redis實現分散式鎖實戰

来源:https://www.cnblogs.com/lfs2640666960/archive/2018/07/13/9307827.html
-Advertisement-
Play Games

背景在很多互聯網產品應用中,有些場景需要加鎖處理,比如:秒殺,全局遞增ID,樓層生成等等。大部分的解決方案是基於DB實現的,Redis為單進程單線程模式,採用隊列模式將併發訪問變成串列訪問,且多客戶端對Redis的連接並不存在競爭關係。其次Redis提供一些命令SETNX,GETSET,可以方便實現 ...


背景
在很多互聯網產品應用中,有些場景需要加鎖處理,比如:秒殺,全局遞增ID,樓層生成等等。大部分的解決方案是基於DB實現的,Redis為單進程單線程模式,採用隊列模式將併發訪問變成串列訪問,且多客戶端對Redis的連接並不存在競爭關係。其次Redis提供一些命令SETNX,GETSET,可以方便實現分散式鎖機制。

Redis命令介紹
使用Redis實現分散式鎖,有兩個重要函數需要介紹

SETNX命令(SET if Not eXists)
語法:
SETNX key value
功能:
當且僅當 key 不存在,將 key 的值設為 value ,並返回1;若給定的 key 已經存在,則 SETNX 不做任何動作,並返回0。

GETSET命令
語法:
GETSET key value
功能:
將給定 key 的值設為 value ,並返回 key 的舊值 (old value),當 key 存在但不是字元串類型時,返回一個錯誤,當key不存在時,返回nil。

GET命令
語法:
GET key
功能:
返回 key 所關聯的字元串值,如果 key 不存在那麼返回特殊值 nil 。

DEL命令
語法:
DEL key [KEY …]
功能:
刪除給定的一個或多個 key ,不存在的 key 會被忽略。

兵貴精,不在多。分散式鎖,我們就依靠這四個命令。但在具體實現,還有很多細節,需要仔細斟酌,因為在分散式併發多進程中,任何一點出現差錯,都會導致死鎖,hold住所有進程。

加鎖實現

SETNX 可以直接加鎖操作,比如說對某個關鍵詞foo加鎖,客戶端可以嘗試
SETNX foo.lock <current unix time>

如果返回1,表示客戶端已經獲取鎖,可以往下操作,操作完成後,通過
DEL foo.lock

命令來釋放鎖。
如果返回0,說明foo已經被其他客戶端上鎖,如果鎖是非堵塞的,可以選擇返回調用。如果是堵塞調用調用,就需要進入以下個重試迴圈,直至成功獲得鎖或者重試超時。理想是美好的,現實是殘酷的。僅僅使用SETNX加鎖帶有競爭條件的,在某些特定的情況會造成死鎖錯誤。

處理死鎖

在上面的處理方式中,如果獲取鎖的客戶端端執行時間過長,進程被kill掉,或者因為其他異常崩潰,導致無法釋放鎖,就會造成死鎖。所以,需要對加鎖要做時效性檢測。因此,我們在加鎖時,把當前時間戳作為value存入此鎖中,通過當前時間戳和Redis中的時間戳進行對比,如果超過一定差值,認為鎖已經時效,防止鎖無限期的鎖下去,但是,在大併發情況,如果同時檢測鎖失效,並簡單粗暴的刪除死鎖,再通過SETNX上鎖,可能會導致競爭條件的產生,即多個客戶端同時獲取鎖。

C1獲取鎖,並崩潰。C2和C3調用SETNX上鎖返回0後,獲得foo.lock的時間戳,通過比對時間戳,發現鎖超時。
C2 向foo.lock發送DEL命令。
C2 向foo.lock發送SETNX獲取鎖。
C3 向foo.lock發送DEL命令,此時C3發送DEL時,其實DEL掉的是C2的鎖。
C3 向foo.lock發送SETNX獲取鎖。

此時C2和C3都獲取了鎖,產生競爭條件,如果在更高併發的情況,可能會有更多客戶端獲取鎖。所以,DEL鎖的操作,不能直接使用在鎖超時的情況下,幸好我們有GETSET方法,假設我們現在有另外一個客戶端C4,看看如何使用GETSET方式,避免這種情況產生。

C1獲取鎖,並崩潰。C2和C3調用SETNX上鎖返回0後,調用GET命令獲得foo.lock的時間戳T1,通過比對時間戳,發現鎖超時。
C4 向foo.lock發送GESET命令,
GETSET foo.lock <current unix time>
並得到foo.lock中老的時間戳T2

如果T1=T2,說明C4獲得時間戳。
如果T1!=T2,說明C4之前有另外一個客戶端C5通過調用GETSET方式獲取了時間戳,C4未獲得鎖。只能sleep下,進入下次迴圈中。

現在唯一的問題是,C4設置foo.lock的新時間戳,是否會對鎖產生影響。其實我們可以看到C4和C5執行的時間差值極小,並且寫入foo.lock中的都是有效時間錯,所以對鎖並沒有影響。
為了讓這個鎖更加強壯,獲取鎖的客戶端,應該在調用關鍵業務時,再次調用GET方法獲取T1,和寫入的T0時間戳進行對比,以免鎖因其他情況被執行DEL意外解開而不知。以上步驟和情況,很容易從其他參考資料中看到。客戶端處理和失敗的情況非常複雜,不僅僅是崩潰這麼簡單,還可能是客戶端因為某些操作被阻塞了相當長時間,緊接著 DEL 命令被嘗試執行(但這時鎖卻在另外的客戶端手上)。也可能因為處理不當,導致死鎖。還有可能因為sleep設置不合理,導致Redis在大併發下被壓垮。最為常見的問題還有

GET返回nil時應該走那種邏輯?

第一種走超時邏輯
C1客戶端獲取鎖,並且處理完後,DEL掉鎖,在DEL鎖之前。C2通過SETNX向foo.lock設置時間戳T0 發現有客戶端獲取鎖,進入GET操作。
C2 向foo.lock發送GET命令,獲取返回值T1(nil)。
C2 通過T0>T1+expire對比,進入GETSET流程。
C2 調用GETSET向foo.lock發送T0時間戳,返回foo.lock的原值T2
C2 如果T2=T1相等,獲得鎖,如果T2!=T1,未獲得鎖。

第二種情況走迴圈走setnx邏輯
C1客戶端獲取鎖,並且處理完後,DEL掉鎖,在DEL鎖之前。C2通過SETNX向foo.lock設置時間戳T0 發現有客戶端獲取鎖,進入GET操作。
C2 向foo.lock發送GET命令,獲取返回值T1(nil)。
C2 迴圈,進入下一次SETNX邏輯

兩種邏輯貌似都是OK,但是從邏輯處理上來說,第一種情況存在問題。當GET返回nil表示,鎖是被刪除的,而不是超時,應該走SETNX邏輯加鎖。走第一種情況的問題是,正常的加鎖邏輯應該走SETNX,而現在當鎖被解除後,走的是GETST,如果判斷條件不當,就會引起死鎖,很悲催,我在做的時候就碰到了,具體怎麼碰到的看下麵的問題

GETSET返回nil時應該怎麼處理?

C1和C2客戶端調用GET介面,C1返回T1,此時C3網路情況更好,快速進入獲取鎖,並執行DEL刪除鎖,C2返回T2(nil),C1和C2都進入超時處理邏輯。
C1 向foo.lock發送GETSET命令,獲取返回值T11(nil)。
C1 比對C1和C11發現兩者不同,處理邏輯認為未獲取鎖。
C2 向foo.lock發送GETSET命令,獲取返回值T22(C1寫入的時間戳)。
C2 比對C2和C22發現兩者不同,處理邏輯認為未獲取鎖。

此時C1和C2都認為未獲取鎖,其實C1是已經獲取鎖了,但是他的處理邏輯沒有考慮GETSET返回nil的情況,只是單純的用GET和GETSET值就行對比,至於為什麼會出現這種情況?一種是多客戶端時,每個客戶端連接Redis的後,發出的命令並不是連續的,導致從單客戶端看到的好像連續的命令,到Redis server後,這兩條命令之間可能已經插入大量的其他客戶端發出的命令,比如DEL,SETNX等。第二種情況,多客戶端之間時間不同步,或者不是嚴格意義的同步。

時間戳的問題

我們看到foo.lock的value值為時間戳,所以要在多客戶端情況下,保證鎖有效,一定要同步各伺服器的時間,如果各伺服器間,時間有差異。時間不一致的客戶端,在判斷鎖超時,就會出現偏差,從而產生競爭條件。
鎖的超時與否,嚴格依賴時間戳,時間戳本身也是有精度限制,假如我們的時間精度為秒,從加鎖到執行操作再到解鎖,一般操作肯定都能在一秒內完成。這樣的話,我們上面的CASE,就很容易出現。所以,最好把時間精度提升到毫秒級。這樣的話,可以保證毫秒級別的鎖是安全的。

分散式鎖的問題

1:必要的超時機制:獲取鎖的客戶端一旦崩潰,一定要有過期機制,否則其他客戶端都降無法獲取鎖,造成死鎖問題。
2:分散式鎖,多客戶端的時間戳不能保證嚴格意義的一致性,所以在某些特定因素下,有可能存在鎖串的情況。要適度的機制,可以承受小概率的事件產生。
3:只對關鍵處理節點加鎖,良好的習慣是,把相關的資源準備好,比如連接資料庫後,調用加鎖機制獲取鎖,直接進行操作,然後釋放,儘量減少持有鎖的時間。
4:在持有鎖期間要不要CHECK鎖,如果需要嚴格依賴鎖的狀態,最好在關鍵步驟中做鎖的CHECK檢查機制,但是根據我們的測試發現,在大併發時,每一次CHECK鎖操作,都要消耗掉幾個毫秒,而我們的整個持鎖處理邏輯才不到10毫秒,玩客沒有選擇做鎖的檢查。
5:sleep學問,為了減少對Redis的壓力,獲取鎖嘗試時,迴圈之間一定要做sleep操作。但是sleep時間是多少是門學問。需要根據自己的Redis的QPS,加上持鎖處理時間等進行合理計算。
6:至於為什麼不使用Redis的muti,expire,watch等機制,可以查一參考資料,找下原因。

7:想要深入系統瞭解分散式技術的話,我在這裡給大家推薦一個架構方面的交流學習群:650385180,裡面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分散式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,相信對於已經工作和遇到技術瓶頸的碼友,在這個群里會有你需要的內容。

鎖測試數據

未使用sleep
第一種,鎖重試時未做sleep。單次請求,加鎖,執行,解鎖時間 


可以看到加鎖和解鎖時間都很快,當我們使用

ab -n1000 -c100 'http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t'
AB 併發100累計1000次請求,對這個方法進行壓測時。 


我們會發現,獲取鎖的時間變成,同時持有鎖後,執行時間也變成,而delete鎖的時間,將近10ms時間,為什麼會這樣?
1:持有鎖後,我們的執行邏輯中包含了再次調用Redis操作,在大併發情況下,Redis執行明顯變慢。
2:鎖的刪除時間變長,從之前的0.2ms,變成9.8ms,性能下降近50倍。
在這種情況下,我們壓測的QPS為49,最終發現QPS和壓測總量有關,當我們併發100總共100次請求時,QPS得到110多。當我們使用sleep時

使用Sleep時

單次執行請求時

我們看到,和不使用sleep機制時,性能相當。當時用相同的壓測條件進行壓縮時 

獲取鎖的時間明顯變長,而鎖的釋放時間明顯變短,僅是不採用sleep機制的一半。當然執行時間變成就是因為,我們在執行過程中,重新創建資料庫連接,導致時間變長的。同時我們可以對比下Redis的命令執行壓力情況 

上圖中細高部分是為未採用sleep機制的時的壓測圖,矮胖部分為採用sleep機制的壓測圖,通上圖看到壓力減少50%左右,當然,sleep這種方式還有個缺點QPS下降明顯,在我們的壓測條件下,僅為35,並且有部分請求出現超時情況。不過綜合各種情況後,我們還是決定採用sleep機制,主要是為了防止在大併發情況下把Redis壓垮,很不行,我們之前碰到過,所以肯定會採用sleep機制。

文章轉載自CSDN:https://blog.csdn.net/ugg/article/details/41894947

參考資料

http://www.worlduc.com/FileSystem/18/2518/590664/9f63555e6079482f831c8ab1dcb8c19c.pdf
http://redis.io/commands/setnx
http://www.blogjava.net/caojianhua/archive/2013/01/28/394847.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 控制反轉是應用於軟體工程領域中的,在運行時被裝配器對象來綁定耦合對象的一種編程技巧,對象之間耦合關係在編譯時通常是未知的。在傳統編程方式中,業務邏輯的流程是應用程式中早已被設定好關聯關係的對象來決定的。在使用控制反轉的情況下,業務邏輯的流程是由對象關係圖來決定的,該對象關係圖有裝配器負責實例化,這種 ...
  • 在大容量,高負荷的web系統中,對資料庫進行一系列拆分,可有效提升資料庫容量和性能。在初學程式的早期,程式員通常都喜歡按傳統資料庫設計模式,設計為單庫和單一功能表的結構,這樣的結構在數據量和併發量達到一定程度之後,會出現嚴重性能問題和維護問題。在出現問題的時候才著手進行優化,會非常痛苦,所以應該在系 ...
  • List、Set、數據結構、Collections 初次學習,涉及到List集合,Set集合和數據結構方面的一些知識,有錯誤還請批評指正 數據結構 數據存儲的常用結構有:棧、隊列、數組、鏈表和紅黑樹。 棧 先進後出(FILO). 隊列 先進先出(FIFO). 數組 有序的元素序列,以索引訪問.查詢快 ...
  • datetime是模塊,datetime模塊還包含一個datetime類,通過from datetime import datetime導入的才是datetime這個類。 strptime(): 用戶輸入的日期和時間是字元串,要處理日期和時間,首先必須把str轉換為datetime。轉換方法是通過d ...
  • 本文主要記錄了SpringBoot中AOP註解式攔截與方法規則攔截的基本使用。 ...
  • 本文內容: servlet的介紹 servlet的基礎使用介紹 HttpServlet ServletConfig ServletContext Cookie Session 數據域對象 servlet的介紹: Servlet是sun公司提供的一門用於開發動態web資源的技術。 servlet程式運... ...
  • 工作中偶然發現Scala構造方法中的參數,無論是否有val/var修飾都可以順利編譯運行,如下: 那麼兩者的區別在哪裡呢?對於case class呢?其區別又在哪裡?其應用場景又在哪裡呢?下麵就辨析一下如下幾個類的區別 單純的從代碼中來看,發現不了什麼區別,只是簡單的多了一個val的修飾符。為了一探 ...
  • 在配置項目組件的過程中, 瞭解Tomcat載入組件順序很有必要。 例如某些框架如Quartz的集群功能需要資料庫的支持, 資料庫的載入肯定要在框架組件載入之前。 經過查閱和Debug發現, web.xml組件載入順序為:context-param -> listener -> filter -> s ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...