背景
樂觀鎖在并發(fā)控制中有非常廣泛的使用,在并發(fā)更新數(shù)據(jù)時避免了互斥鎖的使用,更新沖突較少時有著良好的性能表現(xiàn)。
在Rails中也集成了樂觀鎖的功能,由無所不能的ActiveRecord實現(xiàn)假颇。使用的方式及其簡單,只需要在對應的model
中加入一個lock_version
字段:
class CreateOrders < ActiveRecord::Migration[5.1]
def change
create_table :orders do |t|
t.integer :lock_version, default: 0
t.string :name
t.integer :leave_count,default: 0
end
end
end
在model
數(shù)據(jù)更新的時候就會自動檢測數(shù)據(jù)版本
骨稿,只有持有最新的lock_version
數(shù)據(jù)的更新操作能成功笨鸡。
# p1 p2 持有同樣的數(shù)據(jù)版本
p1 = Order.find(1)
p2 = Order.find(1)
p1.name = "zhangsan"
p1.save # 成功, lock_version字段值會自動增加
p2.name = "cuihua"
p2.save # Raises an ActiveRecord::StaleObjectError
當持有舊版本的更新操作會得到一個ActiveRecord::StaleObjectError
異常坦冠。具體可以查看官方文檔形耗。
提出疑問
包括官方文檔在內(nèi)的眾多資料只是提供了如何在Rails中使用樂觀鎖的方法,只是反復提到Rails會自動檢測數(shù)據(jù)版本是否過期
辙浑,具體實現(xiàn)只字未提激涤。作為一名低端的搬磚工人,我對此感到非常失落判呕。即使是搬磚倦踢,也要知道搬的磚是怎么燒出來的。(主旨點明侠草,本文完)
所以辱挥,不想被拖拉機替代的,接下來我們一起探尋它是如何實現(xiàn)的边涕。這里有兩個問題需要思考:
- 文檔中說的
自動檢測
是如何實現(xiàn)的晤碘? - 異常由誰產(chǎn)生,數(shù)據(jù)庫還是ActiveRecord功蜓?
稍微注意會發(fā)現(xiàn)這兩個問題的答案異常簡單:
pry(main)> p1 = Order.find(1)
pry(main)> p2 = Order.find(1)
pry(main)> p1.leave_count = 9
=> 9
pry(main)> p1.save
(0.2ms) BEGIN
SQL (0.5ms) UPDATE `orders` SET `leave_count` = 9, `updated_at` = '2018-07-15 06:47:28', `lock_version` = 1 WHERE `orders`.`id` = 1 AND `orders`.`lock_version` = 0
(2.3ms) COMMIT
=> true
pry(main)> p2.leave_count = 9
=> 9
pry(main)> p2.save
(0.3ms) BEGIN
SQL (0.4ms) UPDATE `orders` SET `leave_count` = 9, `updated_at` = '2018-07-15 06:47:53', `lock_version` = 1 WHERE `orders`.`id` = 1 AND `orders`.`lock_version` = 0
(0.1ms) ROLLBACK
ActiveRecord::StaleObjectError: Attempted to update a stale object: Order.
from /home/dog/.rvm/gems/ruby-2.5.1@study/gems/activerecord-5.1.6/lib/active_record/locking/optimistic.rb:95:in `_update_row'
ActiveRecord會創(chuàng)建一個巧妙的SQL:
UPDATE `orders` SET `leave_count` = 9, `updated_at` = '2018-07-15 06:47:28', `lock_version` = 1 WHERE `orders`.`id` = 1 AND `orders`.`lock_version` = 0
UPDATE
本質(zhì)上是先SELECT
到對應條件的數(shù)據(jù)园爷,再執(zhí)行數(shù)據(jù)更新。如果當前持有的lock_version
過期了式撼,對應的數(shù)據(jù)行不會查詢到童社,也就不會有更新操作,數(shù)據(jù)庫會返回更新數(shù)據(jù)行為0端衰,也不會產(chǎn)生異常叠洗。
通過查看源碼,發(fā)現(xiàn)異常是由ActiveRecord拋出:
# 有刪減
def _update_row(attribute_names, attempted_action = "update")
return super unless locking_enabled?
affected_rows = self.class._update_record(
attributes_with_values(attribute_names),
self.class.primary_key => id_in_database,
locking_column => previous_lock_value
)
if affected_rows != 1
raise ActiveRecord::StaleObjectError.new(self, attempted_action)
end
end
疑問揭曉旅东,非常簡單巧妙。
進一步思考
這種實現(xiàn)是利用了數(shù)據(jù)庫更新時的原子性十艾,例如在MySQL中會有行鎖
抵代,這是一個悲觀鎖。那么這樣還能叫樂觀鎖嗎忘嫉? 翻一翻樂觀鎖的定義:
Optimistic concurrency control (OCC) is a concurrency control method applied to transactional systems such as relational database management systems and software transactional memory. OCC assumes that multiple transactions can frequently complete without interfering with each other. While running, transactions use data resources without acquiring locks on those resources. Before committing, each transaction verifies that no other transaction has modified the data it has read. If the check reveals conflicting modifications, the committing transaction rolls back and can be restarted.[1] Optimistic concurrency control was first proposed by H.T. Kung and John T. Robinson.
大意是在并發(fā)控制時不會有鎖產(chǎn)生荤牍,在提交時會去檢測數(shù)據(jù)是否已經(jīng)被修改案腺,如沒有則直接更新提交,否則就回滾康吵。這是一種理念劈榨,看看具體的一種實現(xiàn)CAS(比較交換)
CAS 的全稱為compare and swap
, 可以這樣理解: A(目標數(shù)據(jù)的地址)晦嵌, currentVersion(位于A的數(shù)據(jù)的最新版本號)同辣,holdVersion(更新者持有的數(shù)據(jù)版本號), B(新數(shù)據(jù))惭载。 如果holdVersion == currentVersion旱函,就將A地址的數(shù)據(jù)更新為B,否則更新失敗描滔。
這樣來看棒妨,ActiveRecord中的實現(xiàn)滿足CAS的理念,可以說是非常簡潔完美的實現(xiàn)含长。
ActiveRecord中確實沒有產(chǎn)生鎖券腔,但是它確實是依賴于數(shù)據(jù)庫更新時的鎖,也就是說有鎖的參與拘泞,這個怎么理解颅眶?(不是無鎖嗎)
實際上,幾乎所有CAS都是由CPU指令實現(xiàn)田弥,由CPU保證執(zhí)行的原子性涛酗,如果是單核CPU的話,指令反正可以理解為是一條一條順序執(zhí)行的偷厦,不會有沖突商叹。
但是在多CPU的情況下呢? 如何保證指令中比較
和交換
等步驟的原子性只泼? 實際上剖笙,經(jīng)查閱資料,這種情況下CPU硬件級別也會有一個鎖请唱,保證CAS指令執(zhí)行的原子性弥咪,還是有鎖的參與。 不過層級不一樣十绑,這是更加底層的實現(xiàn)聚至, 越底層的鎖,開銷越小本橙,上層并不知曉扳躬。
所以,對于樂觀鎖的理解,需要分層來看贷币。 在ActiveRecord這種應用層來說击胜,它所的實現(xiàn)的就是樂觀鎖。只要當前層級的實現(xiàn)中沒有鎖役纹,且滿足樂觀鎖的理念偶摔,那么它就可以認為是樂觀鎖,盡管它底層可能依賴的是悲觀鎖促脉。
有沒有徹徹底底的樂觀鎖呢辰斋?
使用場景
從上面可以知道,當數(shù)據(jù)版本失效時去更新嘲叔,會得到一個異常亡呵。這在代碼中需要寫一個異常捕獲來捕捉這個特定的異常,以便進一步進行選擇是重試還是直接返回失敗硫戈。
如果數(shù)據(jù)會頻繁更新锰什,則數(shù)據(jù)沖突的可能性加大,可能會頻繁重試丁逝。當業(yè)務邏輯讀多寫少汁胆,或?qū)χ卦嚥幻舾校抑卦嚨拇鷥r較小時霜幼,樂觀鎖也許是一種較好的選擇嫩码。