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