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)悲觀鎖,需要以下兩步:
- 使用 transaction,當(dāng) commit/rollback 時(shí)釋放鎖。
- 使用 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