事務
事務是一組不可分割的操作集合,這些操作要么同時成功提交,要么同時失敗回滾。
acid事物的四大特性
原子性
最小工作單元,要么同時成功,要么同時失敗。
例如A轉賬300給B,A賬戶-300與B賬戶+300必須滿足操作原子性,避免出現A已轉賬但B未收到的一致性問題。
一致性
事務操作的起點和終點必須是從一個一致性狀態到另一個一致性狀態,也就是數據庫的數據變化必須符合預定義期望變化。(不會出現一個數據庫修改成功、一個失敗的情況)
例如在轉賬案例中事務開始時的賬戶總額等于事務結束時的賬戶金額。(并不是一定相等,數據變化符合業務預定義期望即可)
隔離性
并發的事務是相互隔離的。
例如多個并發轉賬事務,每個轉賬操作的數據是相互獨立的,不會出現數據混亂的情況。
持久性
一旦事務提交,其結果就是永久的,不會因系統崩潰丟失。
事務提交后會將數據持久化到硬盤,例如在裝張案例中,變更后賬戶數據持久化在硬盤,數據庫崩潰依然被保留。
并發事務問題
臟讀
事務A讀取到事務B未提交的修改。
不可重復讀
同一事務內多次讀取同一數據時因為其他事物在此期間提交了數據修改導致結果不同。
幻讀
同一事務內對一張表的查詢結果集不同,因為其他事務在此期間插入刪除了數據。
select * 結果集行數不同。
select count()/sum() 等聚合函數,查詢內容可能不同。
例如,事務A查詢name=張三不存在,事務B插入張三,事務A按照張三不存在的業務邏輯插入張三但無法插入。
隔離級別
讀未提交(RU)
允許事務讀取其他事物未提交的修改(臟讀)。
并發性能最高。
讀已提交(RC)
不允許事務讀取其他事物未提交的修改(臟讀)。
無法避免不可重復讀現象。
可重復讀(RR)
不會出現臟讀和不可重復讀問題。
無法避免幻讀問題。
MySQL默認隔離級別。
串行化(S)
完全避免所有并發問題。
并發性能最低。
如何選擇隔離級別
隔離級別越高,并發性能越低。
讀未提交(RU):僅適用對數據準確性要求極低,并發性能要求極高的場景,如監控數據,日志采集,瞬時數據不影響整體的場景,但實際實際生產環境下中還是極少使用,規避臟讀風險。
讀已提交(RC):適用大部分普通業務場景,也是大部分數據庫的默認隔離級別。例如用戶信息頁,用戶A修改提交后,用戶B刷新就能看到用戶A提交的修改內容,但不會看到用戶A未提交的內容。
RC下不可重復讀問題:
🛒 場景一:庫存扣減(并發搶購)
業務邏輯: 用戶下單時,需要檢查并扣減商品庫存(例如商品A,初始庫存10件)。
事務A (用戶1下單):
BEGIN;(RC隔離級別)SELECT stock FROM products WHERE id = 'A';// 返回 10 (庫存充足)(基于查詢結果10,決定繼續下單邏輯... 生成訂單、計算價格等,耗時幾毫秒/秒)
事務B (用戶2下單): (幾乎與事務A同時發生)
BEGIN;(RC隔離級別)SELECT stock FROM products WHERE id = 'A';// 也返回 10 (庫存充足)UPDATE products SET stock = stock - 1 WHERE id = 'A';// 扣減1件,庫存變為9COMMIT;// 用戶2下單成功,庫存更新為9并生效
事務A 繼續執行:
(執行完其他邏輯后)
UPDATE products SET stock = stock - 1 WHERE id = 'A';// 此時基于 *當前已提交數據* (stock=9) 扣減,庫存變為8COMMIT;// 用戶1下單成功
問題:
兩個用戶都成功下單購買了商品A。
最終庫存變為 8,這符合物理扣減。
不可重復讀在哪里?
事務A 在步驟2讀取
stock=10。在它執行后續邏輯時,事務B 修改并提交了庫存(變為9)。
當事務A 執行更新操作(步驟2.2)時,它沒有基于自己最初讀到的10去減1,而是基于最新已提交值9去減1。雖然最終庫存正確(8),但事務A在邏輯判斷(庫存是否充足)后,執行更新操作時依賴的數據(庫存值)已經發生了變化(10 -> 9)。這就是一次“不可重復讀”(在同一個事務A內,如果它再次執行
SELECT stock...,結果會是9,而不是最初的10)。
潛在風險:
超賣風險: 如果初始庫存只有1件,多個事務都讀到1(認為充足),然后都去扣減1(事務B扣成0并提交,事務A再基于0扣減就會變成-1)。這就是經典的并發超賣問題!雖然RC下避免了臟讀(不會讀到事務B未提交的扣減),但因為不可重復讀,兩個事務都基于“過時”的充足判斷進行了扣減,導致庫存為負。解決超賣通常需要額外的并發控制(如樂觀鎖、悲觀鎖、Redis分布式鎖等),而不僅僅是依賴隔離級別。
🕒 場景二:預約系統(時間段占用檢查)
業務邏輯: 用戶預約某個資源(如會議室A在10:00-11:00時段)。
事務A (用戶1預約):
BEGIN;(RC隔離級別)SELECT COUNT(*) FROM bookings WHERE room = 'A' AND start_time < '11:00' AND end_time > '10:00';// 返回 0 (表示10:00-11:00空閑)(用戶1填寫預約信息,點擊確認... 耗時幾秒)
事務B (用戶2預約): (幾乎與事務A同時發生,且操作更快)
BEGIN;(RC隔離級別)SELECT ...// 同樣返回0 (空閑)INSERT INTO bookings (room, start_time, end_time, user) VALUES ('A', '10:00', '11:00', 'user2');// 插入預約記錄COMMIT;// 用戶2預約成功
事務A 繼續執行:
(用戶1點擊確認)
INSERT INTO bookings (room, start_time, end_time, user) VALUES ('A', '10:00', '11:00', 'user1');// 嘗試插入(可能成功也可能失敗,取決于唯一性約束)
COMMIT;
問題:
事務A和事務B都檢查了同一時間段,都認為它是空閑的(SELECT返回0)。
事務B更快地插入記錄并提交。
事務A隨后也嘗試插入記錄。
不可重復讀在哪里?
事務A在步驟2執行SELECT查詢,得知會議室A在10:00-11:00空閑。
在它執行插入操作之前,事務B已經插入并提交了占用該時間段的記錄。
當事務A執行插入操作時,它所依賴的“空閑”狀態(SELECT的結果)已經不再成立(因為事務B的插入已提交)。事務A在邏輯判斷(是否空閑)后,執行插入操作時依賴的數據狀態(時間段是否被占用)已經發生了變化。如果表上有
(room, start_time, end_time)的唯一約束,事務A的插入會失敗(主鍵/唯一鍵沖突)。如果沒有唯一約束,則會產生雙重預訂!
潛在風險:
雙重預訂: 最嚴重的后果!同一個時間段被預約給了兩個用戶,導致沖突和用戶投訴。解決雙重預訂通常需要更嚴格的并發控制,如對目標時間段加行鎖(SELECT FOR UPDATE)或使用樂觀鎖(版本號)。
可重復讀(RR):適用同一事務內涉及一個以上對同一數據的查詢,業務要求不能使兩次查詢結果不一致。
幻讀問題典型案例
假設存在一張
goods表,存儲商品庫存信息,初始數據如下:id name stock 1 手機 10 2 電腦 5 現在有兩個并發事務:事務 A 負責查詢并修改庫存小于 10 的商品,事務 B 負責插入一條新的庫存小于 10 的商品記錄。
步驟 1:事務 A 啟動并首次查詢
事務 A 開始,執行查詢 “庫存小于 10 的商品”:
-- 事務 ABEGIN;-- 第一次查詢:查詢庫存 < 10 的商品SELECT * FROM goods WHERE stock < 10;
此時結果為:
id name stock 2 電腦 5 步驟 2:事務 B 插入新數據并提交
事務 B 啟動,插入一條新商品記錄(庫存 8,符合
stock < 10),并提交事務:-- 事務 BBEGIN;-- 插入一條新商品,庫存 8(符合 stock < 10)INSERT INTO goods (name, stock) VALUES ('平板', 8);COMMIT;此時表中數據變為:
id name stock 1 手機 10 2 電腦 5 3 平板 8 步驟 3:事務 A 再次查詢并嘗試修改
事務 A 再次執行相同的查詢:
-- 事務 A-- 第二次查詢:再次查詢庫存 < 10 的商品SELECT * FROM goods WHERE stock < 10;
在 RR 隔離級別下,由于 MVCC 的可重復讀特性,事務 A 第二次查詢的結果仍為:
id name stock 2 電腦 5 但此時如果事務 A 嘗試修改 “所有庫存 < 10 的商品”(例如批量增加庫存):
-- 事務 A-- 嘗試修改所有庫存 < 10 的商品UPDATE goods SET stock = stock + 2 WHERE stock < 10;COMMIT;
執行后,事務 A 查看最終數據時會發現:新插入的 “平板”(id=3)的庫存也被修改為 10(8+2)。 這就是幻讀:事務 A 兩次查詢都沒看到 “平板”,但修改操作卻影響了它,仿佛數據 “憑空出現” 并被修改。
在RR級別下,不可重復讀場景能被解決,但依然會出現更新操作前判斷失效的情況,update是當前讀會直接讀取最新數據修改,依然會出現同時判斷成功的超賣問題。
場景一:庫存扣減
RR下的行為:
1.事務A開始并創建快照,執行
SELECT stock...讀取的始終是快照中的庫存值(如10)2.事務B開始并執行扣減庫存,此時數據庫中stock值為9
3.事務A開始執行扣減庫存操作
UPDATE stock = stock - 1但會讀取到被修改后的最新數據修改。結果:事務AB庫存判斷成功雖然解決了不可重復讀問題但還是會導致超賣。
解決方法:樂觀鎖、悲觀鎖、分布式鎖、庫存判斷加
For UPDATE<select id="selectStockForUpdate" resultType="com.example.Goods">SELECT id, stock FROM goods WHERE id = #{id} FOR UPDATE ?<!-- 關鍵:對查詢到的行加排他鎖 --></select>
串行化(S):事務串行化執行,適用RR下會出現幻讀且業務不允許的場景及事務必須嚴格按照提交順序執行的場景。
1. 💰 金融核心系統 - 銀行轉賬
-- 事務A: 檢查A余額≥100 → A-100 → B+100-- 事務B: 檢查B余額≥50 → B-50 → C+50
風險:事務A先開啟但是在未提交的情況下,事務B開啟并檢測B的余額,業務邏輯上B用戶賬戶余額一定滿足>=50,但是在RC,RR情況下事務A未提交所以事務B可能產生誤判。
串行化解決方案:
嚴格順序執行:
事務A完全執行后,再執行事務B
或事務B完全執行后,再執行事務A