背景
樂觀鎖在并發(fā)控制中有非常廣泛的使用,在并發(fā)更新數(shù)據(jù)時避免了互斥鎖的使用,更新沖突較少時有著良好的性能表現(xiàn)。
在Rails中也集成了樂觀鎖的功能,由無所不能的ActiveRecord實現(xiàn)。使用的方式及其簡單,只需要在對應(yīng)的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
當(dāng)持有舊版本的更新操作會得到一個ActiveRecord::StaleObjectError異常。具體可以查看官方文檔。
提出疑問
包括官方文檔在內(nèi)的眾多資料只是提供了如何在Rails中使用樂觀鎖的方法,只是反復(fù)提到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到對應(yīng)條件的數(shù)據(jù),再執(zhí)行數(shù)據(jù)更新。如果當(dāng)前持有的lock_version過期了,對應(yīng)的數(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這種應(yīng)用層來說,它所的實現(xiàn)的就是樂觀鎖。只要當(dāng)前層級的實現(xiàn)中沒有鎖,且滿足樂觀鎖的理念,那么它就可以認為是樂觀鎖,盡管它底層可能依賴的是悲觀鎖。
有沒有徹徹底底的樂觀鎖呢?
使用場景
從上面可以知道,當(dāng)數(shù)據(jù)版本失效時去更新,會得到一個異常。這在代碼中需要寫一個異常捕獲來捕捉這個特定的異常,以便進一步進行選擇是重試還是直接返回失敗。
如果數(shù)據(jù)會頻繁更新,則數(shù)據(jù)沖突的可能性加大,可能會頻繁重試。當(dāng)業(yè)務(wù)邏輯讀多寫少,或?qū)χ卦嚥幻舾?,且重試的代價較小時,樂觀鎖也許是一種較好的選擇。