Rails 鎖(未完成)

1. 引言

1.1 為什么需要鎖(并發(fā)控制)?

在多用戶環(huán)境中,在同一時(shí)間可能會(huì)有多個(gè)用戶更新相同的記錄,這會(huì)產(chǎn)生沖突。這就是著名的并發(fā)性問題。

典型的沖突有

  • 丟失更新
    一個(gè)事務(wù)的更新覆蓋了其它事務(wù)的更新結(jié)果,就是所謂的更新丟失。例如:用戶A把值從6改為2,用戶B把值從2改為6,則用戶A丟失了他的更新。
  • 臟讀
    當(dāng)一個(gè)事務(wù)讀取其它完成一半事務(wù)的記錄時(shí),就會(huì)發(fā)生臟讀取。例如:用戶A,B看到的值都是6,用戶B把值改為2,用戶A讀到的值仍為6。

1.2 鎖一般分為哪幾種?

  • 悲觀鎖(Pessimistic Lock)
    ++假定會(huì)發(fā)生并發(fā)沖突++,屏蔽一切可能違反數(shù)據(jù)完整性的操作。
    Java 中的 synchronized,如果某個(gè)資源定義為 synchronized,那么該資源的調(diào)用只能排隊(duì),一個(gè)使用完成后,另外一個(gè)才能開始使用。

  • 樂觀鎖(Optimistic Lock)
    ++假設(shè)不會(huì)發(fā)生并發(fā)沖突++,只在提交操作時(shí)++檢查++是否違反數(shù)據(jù)完整性。 樂觀鎖不能解決臟讀的問題。
    Java JUC中的atomic (原子) 包就是樂觀鎖的一種實(shí)現(xiàn),AtomicInteger 通過CAS(compare-and-swap)操作實(shí)現(xiàn)線程安全的自增。

2. 悲觀鎖

2.1 概述

悲觀鎖 ++假定其他用戶企圖訪問或者改變你正在訪問、更改的對象的概率是很高的++,因此在悲觀鎖的環(huán)境中,在你開始改變此對象之前就將該對象鎖住,并且++直到你提交了所作的更改之后才釋放鎖++。

悲觀的缺陷 是不論是頁鎖還是行鎖,加鎖的時(shí)間可能會(huì)很長,這樣可能會(huì)++長時(shí)間的鎖定一個(gè)對象,限制其他用戶的訪問++,也就是說++悲觀鎖的并發(fā)訪問性不好++。

悲觀鎖,主要依賴的是數(shù)據(jù)庫的排他鎖來實(shí)現(xiàn)

2.2 SQL 實(shí)現(xiàn)

實(shí)現(xiàn)悲觀鎖,需要以下兩步:

    1. 使用 transaction,當(dāng) commit/rollback 時(shí)釋放鎖。
    1. 使用 FOR UPDATE 請求對查詢的資源進(jìn)行加鎖。


我們使用 SQL 來做一個(gè)試驗(yàn) (基于PostgreSQL),試驗(yàn)假設(shè)的內(nèi)容:

  • 第一步,【在查詢窗口1】使用悲觀鎖對資源進(jìn)行加鎖。
  • 第二步,【在查詢窗口1】使用 sleep 模擬業(yè)務(wù)處理的等待。
-- 在窗口1先執(zhí)行以下代碼(自行保證數(shù)據(jù)存在):
BEGIN;
SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
SELECT pg_sleep(10);
COMMIT;
  • 第三步,【在查詢窗口2】在另外一個(gè)窗口請求對相同資源的訪問。分別測試了以下四個(gè)情況:

情景 1. 不使用悲觀鎖對資源進(jìn)行訪問:

SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88'
-- 結(jié)果:不需要等待窗口1完成,直接輸出結(jié)果

情景 2. 使用悲觀鎖進(jìn)行請求訪問:

SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
-- 結(jié)果:需要等待窗口1完成后,才輸出結(jié)果

情景 3. 使用悲觀鎖進(jìn)行請求訪問:

SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
-- 結(jié)果:需要等待窗口1完成后,才輸出結(jié)果
-- 注意:窗口1的記錄,它的serial_number是10027
SELECT * FROM charge_stations WHERE serial_number LIKE '1002%' FOR UPDATE;
-- 結(jié)果:需要等待窗口1完成后,才輸出結(jié)果

情景 4. 使用悲觀鎖進(jìn)行其它資源訪問:

-- 需要保證資源存在,才能達(dá)到測試的目的
SELECT * FROM charge_stations WHERE serial_number = '10000' FOR UPDATE;
-- 結(jié)果:不需要等待窗口1完成,直接輸出結(jié)果

==綜合可以得出,只有都在使用悲觀鎖,而且是對相同的資源,才會(huì)導(dǎo)致后面的訪問等待。==

2.3 Rails 的悲觀鎖實(shí)現(xiàn) (程序?qū)用媸褂帽^鎖)

鎖方法

相關(guān)方法有:lock、lock! 和 with_lock.
其中,lock 和 with_lock 都是封裝 lock! 而來。

https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

方法一:lock

為 ActiveRecord Relation 提供鎖請求。

用例:

Account.lock.find(1)
-- Output: select * from accounts where id=1 for update

完整的鎖應(yīng)用例子:

# 需要手動(dòng)啟動(dòng)事務(wù) 和 加鎖,不過 lock可以加參數(shù),設(shè)置不同的lock模式,如: 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'
Account.transaction do
  # select * from accounts where name = 'shugo' limit 1 for update
  shugo = Account.where(name: 'shugo').lock(true).first
  yuko = Account.where(name: 'yuko').lock(true).first
  shugo.balance -= 100
  shugo.save!
  yuko.balance += 100
  yuko.save!
end


方法二:with_lock

自動(dòng)啟動(dòng)事務(wù),和 請求加鎖

用例:

account = Account.find_by(name: 'shugo')
account.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  account.balance -= 100
  account.save!
end

3. 樂觀鎖

3.1 概述

樂觀鎖認(rèn)為其他用戶企圖改變你正在更改的對象的概率是很小的,因此++樂觀鎖直到你準(zhǔn)備提交所作的更改時(shí)才將對象鎖住++,當(dāng)你++讀取以及改變該對象時(shí)并不加鎖++。

可見樂觀鎖++加鎖的時(shí)間要比悲觀鎖短++,樂觀鎖可以++用較大的鎖粒度獲得較好的并發(fā)訪問性能++。

臟讀導(dǎo)致的失敗甚至要重置問題:如果第二個(gè)用戶恰好在第一個(gè)用戶提交更改之前讀取了該對象,那么當(dāng)他完成了自己的更改進(jìn)行提交時(shí),數(shù)據(jù)庫就會(huì)發(fā)現(xiàn)該對象已經(jīng)變化了,這樣,第二個(gè)用戶不得不重新讀取該對象并作出更改。這說明在樂觀鎖環(huán)境中,++會(huì)增加并發(fā)用戶讀取對象的次數(shù)++。

樂觀鎖是一種并發(fā)解決的思想,是通過程序?qū)用?+ 數(shù)據(jù)庫字段協(xié)同實(shí)現(xiàn)的。

3.2 SQL 實(shí)現(xiàn)

在數(shù)據(jù)庫層面,主要有兩種方式協(xié)助實(shí)現(xiàn)樂觀鎖:

  • 增加 version 字段
    Integer 類型,每次修改都增加1;修改當(dāng)前記錄時(shí)需要比較 version 與 讀取的記錄的 version 是否還保持一致。
  • 增加 timestamp 字段(名字可以叫 updated_time)
    每次記錄更新后,都更新該字段為當(dāng)前時(shí)間;修改當(dāng)前記錄時(shí)需要比較 timestamp 與 讀取的記錄的 timestamp 是否還保持一致。

3.3 Rails 的樂觀鎖實(shí)現(xiàn)

https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html

4. 鎖的應(yīng)用場景

從數(shù)據(jù)庫廠商的角度看,使用樂觀的頁鎖是比較好的,尤其在影響很多行的批量操作中可以放比較少的鎖,從而降低對資源的需求提高數(shù)據(jù)庫的性能。再考慮聚集索引。在數(shù)據(jù)庫中記錄是按照聚集索引的物理順序存放的。如果使用頁鎖,當(dāng)兩個(gè)用戶同時(shí)訪問更改位于同一數(shù)據(jù)頁上的相鄰兩行時(shí),其中一個(gè)用戶必須等待另一個(gè)用戶釋放鎖,這會(huì)明顯地降低系統(tǒng)的性能。interbase和大多數(shù)關(guān)系數(shù)據(jù)庫一樣,采用的是樂觀鎖,而且讀鎖是共享的,寫鎖是排他的。可以在一個(gè)讀鎖上再放置讀鎖,但不能再放置寫鎖;你不能在寫鎖上再放置任何鎖。鎖是目前解決多用戶并發(fā)訪問的有效手段。

綜上所述:在實(shí)際生產(chǎn)環(huán)境里邊,如果并發(fā)量不大且不允許臟讀,可以使用悲觀鎖解決并發(fā)問題;
但如果系統(tǒng)的并發(fā)非常大的話,悲觀鎖定會(huì)帶來非常大的性能問題,所以我們就要選擇樂觀鎖定的方法.

5. 并發(fā)鎖問題

6. 數(shù)據(jù)庫鎖

https://blog.csdn.net/puhaiyang/article/details/72284702

https://www.cnblogs.com/deliver/p/5730616.html

AASM lock。
https://github.com/aasm/aasm/blob/master/README.md

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容