数据库 并发更新之乐观锁和悲观锁
文章目錄
- 1. 問題引出
- 2. 數(shù)據(jù)庫悲觀鎖解決并發(fā)更新
- 3. 數(shù)據(jù)庫樂觀鎖解決并發(fā)更新
- 4. 樂觀鎖 CAS 的 ABA 問題
- 5. 拓展思考
- 5.1. 悲觀鎖和排他鎖、樂觀鎖和 CAS 分別有什么區(qū)別
- 5.2. 悲觀鎖和樂觀鎖適用場景
- 5.3. 樂觀鎖是否必須加版本號或時間戳字段
1. 問題引出
假設現(xiàn)在有一張 item 商品表,quantity 字段表示該商品的數(shù)量。
這時候有一個用戶下了訂單,購買一件商品。那么我們可以用以下 SQL 來實現(xiàn)這個邏輯
UPDATE item SET quantity = quantity - 1 WHERE id = 1;這個實現(xiàn)在一般情況下是沒有問題的,但是現(xiàn)在的后端應用都是在多線程或者多進程環(huán)境下運行,在高并發(fā)情況下就有可能發(fā)生問題
假設現(xiàn)在有 A 和 B 兩個用戶同時下單,后端服務會分配 2 個不同的線程去處理請求,這里分別用線程 A 和 B 來表示。
| 查詢商品 id = 1,此時 quantity = 100 | |
| 查詢商品 id = 1,此時 quantity = 100 | |
| 用戶A下單,更新 quantity = 99 | |
| 用戶B下單,更新 quantity = 99 |
在線程 A 還沒更新數(shù)量之前,B 就去把商品數(shù)量查出來了,并發(fā)更新導致數(shù)據(jù)不一致,業(yè)務上就體現(xiàn)為超賣。
那么這個問題該如何解決呢?答案就是加鎖。鎖可以在不同的層面加。如果是單實例應用,直接加本地鎖,例如 Java 應用可以使用 synchronized。如果是分布式應用,可以通過 Redis、ZooKeeper、Etcd 加分布式鎖
這種情況是數(shù)據(jù)庫并發(fā)更新導致的,能不能直接在數(shù)據(jù)庫層面解決呢?答案是可以的,可以利用數(shù)據(jù)庫鎖機制來解決并發(fā)更新問題。方案有悲觀鎖和樂觀鎖,本文對這2種解決方案展開說明
2. 數(shù)據(jù)庫悲觀鎖解決并發(fā)更新
MySQL 的 InnoDB 引擎提供了以下兩種行鎖機制。在查詢記錄時,使用以下 SQL,可以給對應行加上共享鎖和排他鎖。
-- 共享鎖(S) SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 排他鎖(X) SELECT * FROM table_name WHERE ... FOR UPDATE其中 SELECT ... FOR UPDATE 是悲觀鎖的具體實現(xiàn)。并發(fā)更新可以通過該機制保證數(shù)據(jù)一致性
需要注意的是,鎖在 autocommit=0 狀態(tài)下使用才有意義,因為鎖會在 commit 之后自動釋放。默認情況下 MySQL 單行語句就是一個事務,加鎖語句執(zhí)行完,鎖立即就被釋放了,也就沒意義了
下面給出 SELECT ... FOR UPDATE 解決并發(fā)更新的示例
-- A和B開啟事務 BEGIN; -- A查詢,加上排他鎖 SELECT * FROM item WHERE id = 1 FOR UPDATE;-- B查詢加鎖,由于鎖被A占用,所以阻塞SELECT * FROM item WHERE id = 1 FOR UPDATE; -- A更新 UPDATE item SET quantity = quantity - 1 WHERE id = 1; -- A提交 COMMIT;-- B成功查詢出記錄,繼續(xù)執(zhí)行更新UPDATE item SET quantity = quantity - 1 WHERE id = 1;-- B提交COMMIT;3. 數(shù)據(jù)庫樂觀鎖解決并發(fā)更新
樂觀鎖本質上不加鎖,是一種 CAS 無鎖機制。所謂 CAS,就是在更新的時候,檢查該實際值是不是和期望值一樣,一樣就更新成功,不一樣就更新失敗
下面給出 CAS 解決并發(fā)更新的示例
-- A 查出來 quantity = 100 SELECT * FROM item WHERE id = 1;-- B 查出來 quantity = 100SELECT * FROM item WHERE id = 1; -- A 更新 quantity,同時加上 where 條件檢查 quantity 是不是期望值。發(fā)現(xiàn)是,更新成功 UPDATE item SET quantity = quantity - 1 WHERE id = 1 AND quantity = 100;-- B 更新 quantity,發(fā)現(xiàn) quantity 不是期望值,更新失敗UPDATE item SET quantity = quantity - 1 WHERE id = 1 AND quantity = 100;CAS 存在更新失敗的情況。如何判斷更新是否失敗呢?這也很簡單,UPDATE 語句返回值代表更新的行數(shù),直接判斷返回值是不是 0 即可,0 就是失敗。
現(xiàn)在我們可以判斷更新失敗了,那如何解決呢?這個得具體業(yè)務具體解決了。如果業(yè)務容許這種錯誤發(fā)現(xiàn),可以給用戶一個錯誤提示,比如:
// 查詢記錄 doQuery();// CAS 更新 if (doCasUpdate() == 0) {doError("提示系統(tǒng)繁忙,請重試"); }如果業(yè)務不容許失敗,這時候可以加一個死循環(huán)進行重試
while (true) {// 查詢記錄doQuery();// CAS 更新if (doCasUpdate() > 0) {break;} }4. 樂觀鎖 CAS 的 ABA 問題
我們繼續(xù)以商品這個場景舉例,假設現(xiàn)在有3個操作同時進行,分別是 A、B 用戶同時下單,C 用戶添加商品數(shù)據(jù)
| 查詢 quantity = 100 | 查詢 quantity = 100 | |
| 更新 quantity = 99 | ||
| 更新 quantity = 99+1= 100 | ||
| 更新 quantity = 99 |
用戶 B 下單減庫存本來應該失敗的,但是在 C 用戶的干預下,更新商品數(shù)量成功了,因為 quantity 在中間階段又被更新回預期值 100
這就是 ABA 問題。一個變量一開始是A,被修改為B,又被修改為A,這在程序看來數(shù)據(jù)是沒有變化的。但實際上此A非彼A。
這個情況對業(yè)務有沒有影響呢?在這個商品數(shù)量場景下確實是沒有影響的。但是有的業(yè)務可能是會有影響的。這時候需要單獨引入一個版本號或時間戳字段來解決
SELECT * FROM item WHERE id = 1; UPDATE item SET quantity = quantity - 1, version = version + 1 WHERE id = 1 AND version = 預期版本號5. 拓展思考
5.1. 悲觀鎖和排他鎖、樂觀鎖和 CAS 分別有什么區(qū)別
悲觀鎖和樂觀鎖都是抽象概念,而且都是針對并發(fā)更新場景提出的,物理上不存在對應的鎖。
悲觀鎖,去查數(shù)據(jù)的時候都悲觀地認為別人會修改,所以每次查數(shù)據(jù)時直接上鎖。排他鎖是悲觀鎖的一種實現(xiàn)方案
樂觀鎖,相對悲觀鎖而言,查數(shù)據(jù)時認為一般不會被修改,所以只在更新數(shù)據(jù)時檢測沖突。CAS 是樂觀鎖的一種具體實現(xiàn)
5.2. 悲觀鎖和樂觀鎖適用場景
寫多讀少用悲觀鎖,讀多寫少用樂觀鎖
舉個例子,假設有10萬并發(fā),其中有幾個是更新操作,其它都是讀操作,這時候就特別適合使用樂觀鎖。對于更新操作,由于請求數(shù)較少,CAS 沖突概率就小,大部分都是成功的。對于讀操作,由于沒有加鎖,就沒有性能響應
假設有10萬并發(fā),有幾個是讀操作,其它都是寫操作。如果使用樂觀鎖,CAS 沖突概率極大,大部分都是更新失敗。如果還有循環(huán)不停地進行 CAS 操作,一個是應用的 CPU 開銷過大,一個是給數(shù)據(jù)庫帶來過多的并發(fā),嚴重影響性能。這時候就使用悲觀鎖,直接上鎖。
5.3. 樂觀鎖是否必須加版本號或時間戳字段
如果 CAS 業(yè)務上存在 ABA 問題,那么就得加版本號或時間戳字段。
如果不存在 ABA 問題的話,直接通過業(yè)務字段本身來檢測沖突即可,沒有必要再引入額外字段
總結
以上是生活随笔為你收集整理的数据库 并发更新之乐观锁和悲观锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 时区设定_设置MySQL默认
- 下一篇: linux cmake编译源码,linu