1. 程式人生 > >分散式鎖的介紹及使用方案

分散式鎖的介紹及使用方案

一、分散式鎖

當應用伺服器數量超過1臺,對相同資料的訪問可能造成訪問衝突(特別是寫衝突)。單純使用關係資料庫比如MYSQL的應用可以藉助於事務來實現鎖,也可以使用版本號等實現樂觀鎖,最大的缺陷就是可用性降低(效能差)。對於GLEASY這種滿足大規模併發訪問請求的應用來說,使用資料庫事務來實現資料庫就有些捉襟見肘了。另外對於一些不依賴資料庫的應用,比如分散式檔案系統,為了保證同一檔案在大量讀寫操作情況下的正確性,必須引入分散式鎖來約束對同一檔案的併發操作。

二、對分散式鎖的要求
1.高效能(分散式鎖不能成為系統的效能瓶頸)
2.避免死鎖(拿到鎖的結點掛掉不會導致其它結點永遠無法繼續)
3.支援鎖重入

三、分散式鎖的解決方式

1、是否可以考慮採用ReentrantLock來實現,但是實際上去實現的時候是有問題的,ReentrantLock的lock和unlock要求必須是在同一執行緒進行,而分散式應用中,lock和unlock是兩次不相關的請求,因此肯定不是同一執行緒,因此導致無法使用ReentrantLock。

2、基於資料庫表做樂觀鎖,用於分散式鎖。

3、使用memcached的add()方法,用於分散式鎖。

4、使用memcached的cas()方法,用於分散式鎖。(不常用) 

5、使用redis的setnx()、expire()方法,用於分散式鎖。

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

7、使用redis的watch、multi、exec命令,用於分散式鎖。(不常用) 

8、使用zookeeper,用於分散式鎖。(不常用) 

1)基於資料庫資源表做樂觀鎖,用於分散式鎖

大多數是基於資料版本(version)的記錄機制實現的。何謂資料版本號?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表新增一個 “version”欄位來實現讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加1。在更新過程中,會對版本號進行比較,如果是一致的,沒有發生改變,則會成功執行本次操作;如果版本號不一致,則會更新失敗。

ABA問題:

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

假設我們現在我們對id=5780這條資料進行分配,那麼非分散式場景的情況下,我們一般先查詢出來state=1(未分配)的資料,然後從其中選取一條資料可以通過以下語句進行,如果可以更新成功,那麼就說明已經佔用了這個資源。 

update t_resource set state=2 wherestate=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 andversion=26

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

 樂觀鎖的缺點:

(1). 這種操作方式,使原本一次的update操作,必須變為2次操作: select版本號一次;update一次。增加了資料庫操作的次數。

(2). 如果業務場景中的一次業務流程中,多個資源都需要用保證資料一致性,那麼如果全部使用基於資料庫資源表的樂觀鎖,就要讓每個資源都有一張資源表,這個在實際使用場景中肯定是無法滿足的。而且這些都基於資料庫操作,在高併發的要求下,對資料庫連線的開銷一定是無法忍受的

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

2)使用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設定超時時間,即便發生了宕機的情況,也不會將資源一直佔用,可以避免死鎖的問題。

其缺點:

memcached採用列入LRU置換策略,所以如果記憶體不夠,可能導致快取中的鎖資訊丟失。

memcached無法持久化,一旦重啟,將導致資訊丟失。


3)用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設定。

 具體的使用步驟如下: 

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

2、expire()命令對lockkey設定超時時間,為的是避免死鎖問題。

3、執行完業務程式碼後,可以通過delete命令刪除key。

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

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

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

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

1、getset(key, "value1")  返回nil   此時key的值會被設定為value1

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

3. 依次類推!

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

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

 2、get(lockkey)獲取值oldExpireTime ,並將這個value值與當前的系統時間進行比較,如果小於當前系統時間,則認為這個鎖已經超時,可以允許別的請求重新獲取,轉向3。

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

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

 5、在獲取到鎖之後,當前執行緒可以開始自己的業務處理,當處理完畢後,比較自己的處理時間和對於鎖設定的超時時間,如果小於鎖設定的超時時間,則直接執行delete釋放鎖;如果大於鎖設定的超時時間,則不需要再鎖進行處理。

6、也可以使用Redis的jedis客戶端實現。

6)zookeeper

1、實現原理:

基於zookeeper瞬時有序節點實現的分散式鎖,其主要邏輯如下(該圖來自於IBM網站)。大致思想即為:每個客戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。

2、優點

鎖安全性高,zk可持久化

3、缺點

效能開銷比較高。因為其需要動態產生、銷燬瞬時節點來實現鎖功能。

4、實現

可以直接採用zookeeper第三方庫curator即可方便地實現分散式鎖。