分佈式鎖-經常使用技術方案

分佈式鎖的解決方式

一、是否能夠考慮採用ReentrantLock來實現,可是實際上去實現的時候是有問題的,ReentrantLock的lock和unlock要求必須是在同一線程進行,而分佈式應用中,lock和unlock是兩次不相關的請求,所以確定不是同一線程,所以致使沒法使用ReentrantLock。redis

二、基於數據庫表作樂觀鎖,用於分佈式鎖。數據庫

三、使用memcached的add()方法,用於分佈式鎖。緩存

四、使用memcached的cas()方法,用於分佈式鎖。(不經常使用) 服務器

五、使用redis的setnx()、expire()方法,用於分佈式鎖。併發

六、使用redis的setnx()、get()、getset()方法,用於分佈式鎖。分佈式

七、使用redis的watch、multi、exec命令,用於分佈式鎖。(不經常使用) memcached

八、使用zookeeper,用於分佈式鎖。(不經常使用) 高併發

於數據庫資源表作樂觀鎖,用於分佈式鎖

大多數是基於數據版本(version)的記錄機制實現的。何謂數據版本號?即爲數據增長一個版本標識,在基於數據庫表的版本解決方案中,通常是經過爲數據庫表添加一個 「version」字段來實現讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加1。在更新過程當中,會對版本號進行比較,若是是一致的,沒有發生改變,則會成功執行本次操做;若是版本號不一致,則會更新失敗。優化

ABA問題:spa

假設咱們有一張資源表,以下圖所示: t_resource , 其中有6個字段id, resoource,  state, add_time, update_time, version,分別表示表主鍵、資源、分配狀態(1未分配  2已分配)、資源建立時間、資源更新時間、資源數據版本號。

假設咱們如今咱們對id=5780這條數據進行分配,那麼非分佈式場景的狀況下,咱們通常先查詢出來state=1(未分配)的數據,而後從其中選取一條數據能夠經過如下語句進行,若是能夠更新成功,那麼就說明已經佔用了這個資源。 

update t_resource set state=2 where state=1 and id=5780。(相似於CAS操做)返回影響行數0即失敗,1即成功。

若是在分佈式場景中,因爲數據庫的update操做是原子是原子的,其實上邊這條語句理論上也沒有問題,可是這條語句若是在典型的「ABA」狀況下,咱們是沒法感知的。好比銀行帳戶存款或者扣款的過程當中,這種狀況是比較恐怖的。

樂觀鎖解決:

a. 先執行select操做查詢當前數據的數據版本號,好比當前數據版本號是26:

 select id, resource, state,version from t_resource  where state=1 and id=5780;

 b. 執行更新操做:

 update t_resoure set state=2, version=27, update_time=now() where resource=xxxxxx and state=1 and version=26

 c. 若是上述update語句真正更新影響到了一行數據,那就說明佔位成功。若是沒有更新影響到一行數據,則說明這個資源已經被別人佔位了。

 樂觀鎖的缺點:

(1). 這種操做方式,使本來一次的update操做,必須變爲2次操做: select版本號一次;update一次。增長了數據庫操做的次數。

(2). 若是業務場景中的一次業務流程中,多個資源都須要用保證數據一致性,那麼若是所有使用基於數據庫資源表的樂觀鎖,就要讓每一個資源都有一張資源表,這個在實際使用場景中確定是沒法知足的。並且這些都基於數據庫操做,在高併發的要求下,對數據庫鏈接的開銷必定是沒法忍受的

 (3)樂觀鎖機制每每基於系統中的數據存儲邏輯,所以可能會形成髒數據被更新到數據庫中。在系統設計階段,咱們應該充分考慮到這些狀況出現的可能性,並進行相應調整,如將樂觀鎖策略在數據庫存儲過程當中實現,對外只開放基於此存儲過程的數據更新途徑,而不是將數據庫表直接對外公開。 

使用memcached的add()方法

對於使用memcached的add()方法作分佈式鎖,這個在互聯網公司是一種比較常見的方式,並且基本上能夠解決本身手頭上的大部分應用場景。在使用這個方法以前,只要能搞明白memcached的add()和set()的區別,而且知道爲何能用add()方法作分佈式鎖就好。若是key是已經存在的set是更新原來的數據,而add則不會。

memcache::add 方法:add方法用於向memcache服務器添加一個要緩存的數據。

memcache::set 方法:set方法用於設置一個指定key的緩存內容,set方法是add方法和replace方法的集合體

mmecache::replace方法: replace方法用於替換一個指定key的緩存內容,若是key不存在則返回false

比較:

方法 當key存在 當key不存在
add false true
replace 替換(true) false
set 替換(true) true

避免死鎖問題:

若是使用memcached的add()命令對資源佔位成功了咱們須要在add()的使用指定當前添加的這個key的有效時間,若是不指定有效時間,正常狀況下,你能夠在執行完本身的業務後,使用delete方法將這個key刪除掉,也就是釋放了佔用的資源。可是,若是在佔位成功後,memecached或者本身的業務服務器發生宕機了,那麼這個資源將沒法獲得釋放。因此經過對key設置超時時間,即使發生了宕機的狀況,也不會將資源一直佔用,能夠避免死鎖的問題。

使用redis的setnx()、expire()方法

對於使用redis的setnx()、expire()來實現分佈式鎖,這個方案相對於memcached()的add()方案,redis佔優點的是,其支持的數據類型更多,而memcached只支持String一種數據類型。

 首先說明一下setnx()命令,setnx的含義就是SET if Not Exists,其主要有兩個參數 setnx(key, value)。該方法是原子的,若是key不存在,則設置當前key成功,返回1;若是當前key已經存在,則設置當前key失敗,返回0。可是要注意的是setnx命令不能設置key的超時時間,只能經過expire()來對key設置。

 具體的使用步驟以下: 

一、setnx(lockkey, 1)  若是返回0,則說明佔位失敗;若是返回1,則說明佔位成功

二、expire()命令對lockkey設置超時時間,爲的是避免死鎖問題。

三、執行完業務代碼後,能夠經過delete命令刪除key。

 這個方案實際上是能夠解決平常工做中的需求的,但從技術方案的探討上來講,可能還有一些能夠完善的地方。好比,若是在第一步setnx執行成功後,在expire()命令執行成功前,發生了宕機的現象,那麼就依然會出現死鎖的問題,因此若是要對其進行完善的話,可使用redis的setnx()、get()和getset()方法來實現分佈式鎖。   

使用redis的setnx()、get()、getset()方法

這個方案的背景主要是在setnx()和expire()的方案上針對可能存在的死鎖問題,作了一版優化。

getset()命令?這個命令主要有兩個參數 getset(key,newValue)。該方法是原子的,對key設置newValue這個值,而且返回key原來的舊值。假設key原來是不存在的,那麼屢次執行這個命令,會出現下邊的效果:

一、getset(key, "value1")  返回nil   此時key的值會被設置爲value1

2. getset(key, "value2")  返回value1   此時key的值會被設置爲value2

3. 依次類推!

 介紹完要使用的命令後,具體的使用步驟以下:

 一、setnx(lockkey, 當前時間+過時超時時間) ,若是返回1,則獲取鎖成功;若是返回0則沒有獲取到鎖,轉向2。

 二、get(lockkey)獲取值oldExpireTime ,並將這個value值與當前的系統時間進行比較,若是小於當前系統時間,則認爲這個鎖已經超時,能夠容許別的請求從新獲取,轉向3。

 三、計算newExpireTime=當前時間+過時超時時間,而後getset(lockkey, newExpireTime) 會返回當前lockkey的值currentExpireTime。

 四、判斷currentExpireTime與oldExpireTime 是否相等,若是相等,說明當前getset設置成功,獲取到了鎖。若是不相等,說明這個鎖又被別的請求獲取走了,那麼當前請求能夠直接返回失敗,或者繼續重試。

 五、在獲取到鎖以後,當前線程能夠開始本身的業務處理,當處理完畢後,比較本身的處理時間和對於鎖設置的超時時間,若是小於鎖設置的超時時間,則直接執行delete釋放鎖;若是大於鎖設置的超時時間,則不須要再鎖進行處理。

相關文章
相關標籤/搜索