天天看點

再聊分布式鎖

[toc]

今天我們來聊聊分布式鎖。

首先,我們看這樣一個場景:客戶下單的時候,我們調用庫存中心進行減庫存,那我們一般的操作都是

這種通過設定庫存的修改方式,我們知道在并發量高的時候會存在資料庫的丢失更新,比如a,b目前兩個事務,查詢出來的庫存都是5,a買了3個單子要把庫存設定為2,而b買了1個單子要把庫存設定為4,那這個時候就會出現a會覆寫b的更新,是以我們更多的都是會加個條件

即樂觀鎖的方式來處理,當然也可以通過版本号來處理樂觀鎖,都是一樣的,但是這是更新一個表,如果我們牽扯到多個表呢,我們希望和這個單子關聯的所有的表同一時間隻能被一個線程來處理更新,多個線程按照不同的順序去更新同一個單子關聯的不同資料,出現死鎖的機率比較大。對于非敏感的資料,我們也沒有必要去都加樂觀鎖處理,我們的服務都是多機器部署的,要保證多程序多線程同時隻能有一個程序的一個線程去處理,這個時候我們就需要用到分布式鎖。

分布式鎖的實作方式有很多,我們今天分别通過資料庫,zk,redis以及tair的實作邏輯

更新一個單子關聯的所有的資料,先查詢出這個單子,并加上排他鎖,在進行一系列的更新操作

這種處理需要主要依靠排他鎖來阻塞其他線程,不過這個需要注意幾點:

查詢的資料一定要在資料庫裡存在,如果不存在的話,資料庫會加gap鎖,而gap鎖之間是相容的,這種如果兩個線程都加了gap鎖,另一個再更新的話會出現死鎖。不過一般能更新的資料都是存在的

後續的處理流程需要盡可能的時間短,即在更新的時候提前準備好資料,保證事務處理的時間足夠的短,流程足夠的短,因為開啟事務是一直占着連接配接的,如果流程比較長會消耗過多的資料庫連接配接的。

通過在一張表裡建立唯一鍵來擷取鎖,比如執行saveStore這個方法

其中method_name是個唯一鍵,通過這種方式也可以做到,解鎖的時候直接删除改行記錄就行。不過這種方式,鎖就不會是阻塞式的,因為插入資料是立馬可以得到傳回結果的。

那針對以上資料庫實作的兩種分布式鎖,存在什麼樣的優缺點呢

簡單,友善,快速實作

基于資料庫,開銷比較大,性能可能會存在影響

基于資料庫的目前讀來實作,資料庫會在底層做優化,可能用到索引,可能不用到索引,這個依賴于查詢計劃的分析

使用zk來實作,代碼網上比較多,我這裡大緻說下步驟,我們重點看redis的實作。

先有一個鎖跟節點,lockRootNode,這可以是一個永久的節點

用戶端擷取鎖,先在lockRootNode下建立一個順序的瞬時節點,保證用戶端斷開連接配接,節點也自動删除

調用lockRootNode父節點的getChildren()方法,擷取所有的節點,并從小到大排序,如果建立的最小的節點是目前節點,則傳回true,擷取鎖成功,否則,關注比自己序号小的節點的釋放動作(exist watch),這樣可以保證每一個用戶端隻需要關注一個節點,不需要關注所有的節點,避免羊群效應。

如果有節點釋放操作,重複步驟3

隻需要删除步驟2中建立的節點即可

使用zk的分布式鎖存在什麼樣的優缺點呢?

用戶端如果出現當機故障的話,鎖可以馬上釋放

可以實作阻塞式鎖,通過watcher監聽,實作起來也比較簡單

叢集模式,穩定性比較高

一旦網絡有任何的抖動,zk就會認為用戶端已經當機,就會斷掉連接配接,其他用戶端就可以擷取到鎖。當然zk有重試機制,這個就比較依賴于其重試機制的政策了

自己沒有做過測試,網上看到的說是性能上不如緩存

分布式鎖介紹這塊,我們重點看下redis的分布式鎖的實作。

我們先舉個例子,比如現在我要更新産品的資訊,産品的唯一鍵就是productId

這是一個簡單的實作,存在的問題

可能會導緻目前線程的鎖誤被其他線程釋放,比如a線程擷取到了鎖正在執行,但是由于内部流程處理逾時或者gc導緻鎖過期,這個時候b線程擷取到了鎖,a和b線程處理的是同一個productId,b還在處理的過程中,這個時候a處理完了,a去釋放鎖,可能就會導緻a把b擷取的鎖釋放了。

不能實作可重入

用戶端如果第一次已經設定成功,但是由于逾時傳回失敗,此後用戶端嘗試會一直失敗

針對以上問題我們改進下

v傳requestId,然後我們在釋放鎖的時候判斷一下,如果是目前requestId,那就可以釋放,否則不允許釋放

加入count的鎖計數,在擷取鎖的時候查詢一次,如果是目前線程已經持有的鎖,那鎖技術加1,直接傳回true

這種實作基本解決了誤釋放和可重入的問題。

這裡說明幾點:

引入count實作重入的話,看業務需要,并且在釋放鎖的時候,其實也可以直接就把鎖删除了,一次釋放搞定,不需要在通過count數量釋放多次,看業務需要吧

關于要考慮設定鎖逾時,是以需要在設定鎖的時候查詢一次,可能會有性能的考量,看具體業務吧

目前擷取鎖失敗的等待時間是在代碼裡面設定的,可以提出來,修改下等待的邏輯即可

之前在網上還看到有這種實作方式,就是擷取到鎖之後要檢查下鎖的過期時間,如果鎖過期了要重新設定下時間,大緻代碼如下

這種實作存在的問題,過度依賴目前伺服器的時間了,如果在大量的并發請求下,都判斷出了鎖過期,而這個時候再去設定鎖的時候,最終是會隻有一個線程,但是可能會導緻不同伺服器根據自身不同的時間覆寫掉最終擷取鎖的那個線程設定的時間。

參考

<a href="http://www.cnblogs.com/luxiaoxun/p/4889764.html">http://www.cnblogs.com/luxiaoxun/p/4889764.html</a>

<a href="http://blog.csdn.net/abccheng/article/details/72420996">http://blog.csdn.net/abccheng/article/details/72420996</a>