Java鎖?分散式鎖?樂觀鎖?行鎖?

Java工程師攻略發表於2019-04-23

Tomcat的鎖

Tomcat是這個系統的核心組成部分, 每當有使用者請求過來,Tomcat就會從執行緒池裡找個執行緒來處理,有的執行登入,有的檢視購物車,有的下訂單,看著屬下們盡心盡職地工作,完成人類的請求,Tomcat就很有成就感。

與此同時,它也很得意,所有的業務邏輯盡在掌握。MySQL算啥!不就是一個儲存資料的地方嗎? Redis算啥!不就是一個加快速度的快取嗎?

沒有他們,我也能找到替代品,而我不可替代的, Tomcat經常這麼想。

昨天MySQL偶然說起隔壁機器入駐了一個叫做Node.js的傢伙,居然只用一個執行緒來執行JavaScript程式碼,實現各種業務邏輯,JavaScript也能到後端來?還用回撥? 這不是胡鬧嗎?不過得小心,別被他把業務都給搶走了。

想到此處,Tomcat立刻去檢視各個執行緒活幹得怎麼樣,有沒有人故意偷懶。

執行緒0x9527和0x7954又在吵架了,原因非常簡單,他們倆都去做扣減庫存的操作:讀取庫存,修改庫存,寫回資料庫。

執行緒的併發執行導致三個操作交織在了一起,最後資料出現了不一致。

Java鎖?分散式鎖?樂觀鎖?行鎖?
Tomcat說:“你們怎麼搞的,為什麼要把庫存讀出來,直接update 庫存不行嗎? 讓MySQL老頭兒去保證正確性。要學會甩鍋啊!”

0x7954回答道:“沒辦法,張大胖的程式碼就是這麼寫的,好像是業務要求的,扣減庫存之前要檢查庫存夠不夠。”

Tomcat一陣牙疼, 不由得想起了Redis的處理辦法, 對於每個讀寫快取的請求,Redis都把他們給排成了隊,用一個執行緒挨個去處理,肯定沒有這個併發的問題了。

可是自己這裡不行啊,訪問資料庫是極慢的操作,如果只用一個執行緒,一個個地處理請求,所有的請求都得等待,人類會急死的。

沒辦法,Tomcat扔給他們倆一個Java物件:“這是一把鎖,以後誰先搶到誰才能執行扣減庫存的三個操作。”

“如果搶不到怎麼辦?”

“阻塞等待,別人釋放了鎖,JVM自然會喚醒你,然後再去搶! 什麼時候搶到,什麼時候執行。”

分散式的鎖

張大胖覺得有點不對勁, 這幾天程式執行怎麼有點兒慢了呢?

他還以為是機器效能不夠,就申請了幾臺新機器,又安裝了幾個Tomcat,組成了一個叢集。

Java鎖?分散式鎖?樂觀鎖?行鎖?
這下可好,三個Tomcat, 每個Tomcat都有一把鎖來控制對庫存的訪問。

在Tomcat這個JVM程式內部,同一個時刻只有一個幸運兒執行緒可以扣減庫存,可是現在有三個Tomcat,出現了三個幸運兒。

這三個幸運兒在扣減庫存的時候,仍然會出現0x7954和0x9527那樣的錯誤,只不過現在他們互不知曉,連吵架的機會都沒有了。

三個Tomcat都覺得頭大,在這個分散式的環境中,多個程式在執行,原來那種程式內的鎖已經失效,當務之急是找一個客觀、公正、獨立的第三方來實現鎖的功能。

MySQL提議: “到我這裡來找鎖啊!”

“你那裡能提供一個鎖服務? 暴露出來讓我們使用? ” Tomcat A問道。

“不不,不是一個鎖服務,我給你們一個資料庫表,這個表中的欄位lock_name有個唯一性約束。”

Java鎖?分散式鎖?樂觀鎖?行鎖?
“你的意思是,我們的執行緒每次想獲得鎖的時候,都去資料庫插入一條資料? ” Tomcat A 反映很快。

insert into locks(lock_name,...) values('stock',...);

“對啊,我的唯一性約束只能保證一個成功,其他的都失敗,就相當於獲得鎖了。 當然那個執行緒的操作完成以後,需要釋放鎖。”

delete from locks where lock_name='stock'

Java鎖?分散式鎖?樂觀鎖?行鎖?
這倒是一個簡單的辦法, 但也是一個重量級的辦法:每次獲得鎖都得訪問一次資料庫!

假設來自TomcatA的0x9527捷足先登,插入了一條資料,獲得了鎖, 那來自Tomcat B的0x7954操作肯定失敗,這時候0x7954該怎麼辦? 能阻塞等待TomcatB來喚醒他嗎? 不行,因為連TomcatB 都不知道0x9527什麼時候操作完成, 除非MySQL來通知各個Tomcat, 這是肯定不行的。

那0x7954@TomcatB只能做一件事情:等待一會兒,然後重試! 如此迴圈下去,直到獲得鎖為止。

可是如果0x9527獲得了鎖,在執行的過程中TomcatA 掛掉了,那資料庫記錄一直存在,無人刪除,那鎖就永遠也無法釋放了! 還得弄一個清理者, 清理那些過期沒釋放的鎖, 這實在是太麻煩了。

Redis

這時候Redis說道:“千萬別上MySQL的賊船!他的辦法太笨重了,不就是找個第三方來儲存鎖的資訊嗎? 用我的快取多好!”

“Redis這小子操作的是記憶體,速度會快很多!” Tomcat B說道。

“對,MySQL不是給你們提供了一張表讓你們插入資料嗎? 我這裡不用那麼麻煩,你們Tomcat的執行緒,都可以嘗試到我的快取中設定一個值,比如stock_lock=true, 誰先設定成功,誰就獲得了鎖,可以去扣減庫存。”

Java鎖?分散式鎖?樂觀鎖?行鎖?
“ 如果有多個執行緒去設定,你能保證只有一個成功,別的都失敗嗎? ”

Redis拍拍胸脯: “絕對保證!”

(碼農翻身老劉注:其實就是setnx命令了)

MySQL撇撇嘴:“和我的方案本質上是一樣的。人家Tomcat 的執行緒對庫存做了修改以後,也還得去解鎖,去刪除這個stock_lock。”

Redis說:“我這裡還能設定過期時間,如果Tomcat A上執行緒獲得了鎖,然後Tomcat A掛掉了, 到了過期時間,我就可以自動把這個stock_lock刪除,別的執行緒又可以獲得鎖了!”

“嗯,是比MySQL先進,並且速度更快,我們還是用這個鎖吧。” 三個Tomcat都表示同意。

定期自動釋放的問題

“且慢,這個自動刪除過期的鎖有問題啊 !” MySQL突然反擊。

“什麼問題?” Redis沒想到資料庫老頭兒還想負隅頑抗。

“假設Tomcat A上的0x9527獲得了鎖, 去執行扣減庫存的操作,然後由於某種原因被阻塞了,阻塞的時間超過了過期時間,鎖被你釋放掉了,最終還是會出現不一致!”

Java鎖?分散式鎖?樂觀鎖?行鎖?
“你這是吹毛求疵,絕對是小概率事件!” Redis叫道! “再說了,用你的資料庫方案,也得定期清理那些鎖,道理是一樣的。”

行鎖

第二天, MySQL高興得去找Tomcat:“兄弟們,我昨天晚上和Quartz(一個著名的定時執行框架)聊了半宿,他告訴了我一個新的用資料庫實現分散式鎖的辦法, 行鎖。”

Java鎖?分散式鎖?樂觀鎖?行鎖?

“看到沒有, 通過新增一個for udpate ,這個SQL語句會把這一行給鎖定,就是獲得了鎖! 只要事務一提交,這個行鎖就自動釋放了。”

“那沒有獲得鎖的別的執行緒呢? ”

自然是阻塞住了,等到別的執行緒釋放了行鎖,它可以自動去獲取,程式碼中都不用迴圈重試,你看,之前的方案都做不到這一點吧。” MySQL說道。

“那要是有個執行緒遲遲不釋放行鎖,會發生什麼問題?” Tomcat最關心這個。

“那其他執行緒都會等待,並且佔用著資料庫連線不釋放,嗯,如果連線被佔用得過多,連線池就要出問題了......” MySQL底氣不足了,這可是個致命的問題。

“哈哈,看你出的什麼餿注意!還是用我的鎖吧!” Redis笑道。

“那人家Quartz為什麼可以用?”MySQL不死心。

“估計Quartz業務單一,並且鎖釋放得很快,不會出問題吧。”

CAS

正在這時,Node.js悄悄地走過來, 把資料庫老頭兒拉走了:“前輩,別給他們一般見識,不就是扣減庫存嗎,用啥分散式鎖!, 我們們這麼做:”

#old_num = 先獲取現有的庫存數量

#new_num = #old_num - 10

update stock set stock_num = #new_num where product_id=#product_id and stock_num = #old_num

MySQL眼前一亮, 是啊,每次把這個#old_num 作為條件傳進去呼叫update語句,如果能成功,說明在這段時間內沒有別的執行緒更新庫存;

如果不成功,那就重新執行這三條語句,直到成功為止, 就這個麼簡單, 完全不用鎖,真是太爽了。

過了幾天,Tomcat他們也聽說了這個方案, 驚訝地說:“這不就是我們Java常用的Compare And Set(CAS)嗎?”

總結

與此同時,張大胖開始做總結:分散式鎖和程式內的鎖本質上是一樣的。

1.  要互斥,同一時刻只能被一臺機器上的一個執行緒獲得。

2.  最好支援阻塞,然後喚醒,這樣那些等待的執行緒不用迴圈重試。

3.  最好可以重入(本文沒有涉及,參見《程式設計世界的那把鎖》)

4.  獲得鎖和釋放鎖速度要快

5.  對於分散式鎖,需要找到一個集中的“地方”(資料庫,Redis, Zookeeper等)來儲存鎖,這個地方最好是高可用的。

6. 考慮到“不可靠的”分散式環境, 分散式鎖需要設定過期時間

7.  CAS的思想很重要。

讀者福利

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:277763288

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

Java鎖?分散式鎖?樂觀鎖?行鎖?

相關文章