分布式锁-常用技术方案
分布式鎖的解決方式
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,用于分布式鎖。(不常用)?
基于數據庫資源表做樂觀鎖,用于分布式鎖
大多數是基于數據版本(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 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設置。
?具體的使用步驟如下:?
1、setnx(lockkey, 1) ?如果返回0,則說明占位失敗;如果返回1,則說明占位成功
2、expire()命令對lockkey設置超時時間,為的是避免死鎖問題。
3、執行完業務代碼后,可以通過delete命令刪除key。
?這個方案其實是可以解決日常工作中的需求的,但從技術方案的探討上來說,可能還有一些可以完善的地方。比如,如果在第一步setnx執行成功后,在expire()命令執行成功前,發生了宕機的現象,那么就依然會出現死鎖的問題,所以如果要對其進行完善的話,可以使用redis的setnx()、get()和getset()方法來實現分布式鎖。 ??
使用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釋放鎖;如果大于鎖設置的超時時間,則不需要再鎖進行處理。
?
轉載于:https://www.cnblogs.com/wade-luffy/p/5812335.html
總結
以上是生活随笔為你收集整理的分布式锁-常用技术方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转发- css(display,floa
- 下一篇: Spark 运行模式 standalon