在MySQL中不建議使用長事務(wù)的根因詳析
引言
“不要使用長事務(wù)”是 MySQL 開發(fā)與運(yùn)維中的黃金準(zhǔn)則。然而,許多開發(fā)者僅將其視為性能建議,卻未意識到其背后隱藏著系統(tǒng)級崩潰風(fēng)險(xiǎn)。本文將從 InnoDB 的底層機(jī)制出發(fā),結(jié)合具體事務(wù) ID(trx_id)、Undo Log 版本鏈、Read View 快照等核心組件,徹底剖析:
- 為什么 REPEATABLE READ 隔離級別必須維護(hù)歷史版本;
- 為什么一個(gè)只包含
SELECT的事務(wù)也能導(dǎo)致磁盤寫滿; - 為什么“事務(wù)中調(diào)用支付接口”這類看似合理的代碼會引發(fā)雪崩。
只有理解了 MVCC 的完整工作流,才能真正明白:長事務(wù)的本質(zhì),是讓整個(gè)數(shù)據(jù)庫為你的快照背負(fù)歷史包袱。
一、可重復(fù)讀(REPEATABLE READ)的實(shí)現(xiàn)原理
MySQL InnoDB 引擎在 REPEATABLE READ 隔離級別下,通過 MVCC(多版本并發(fā)控制) 實(shí)現(xiàn)一致性非鎖定讀。其核心依賴三個(gè)要素:
每行記錄的隱藏字段:
DB_TRX_ID:最后一次修改該行的事務(wù) ID;DB_ROLL_PTR:指向 Undo Log 中的歷史版本指針。
Undo Log:存儲數(shù)據(jù)的歷史版本,形成版本鏈(Version Chain)。
Read View:事務(wù)執(zhí)行第一個(gè)
SELECT時(shí)創(chuàng)建的一致性視圖,用于判斷哪些版本可見。
1.1 版本鏈?zhǔn)纠?/h3>
假設(shè)初始插入由事務(wù) trx_id = 100 完成:
INSERT INTO accounts (id, balance) VALUES (1, 100);
隨后三次更新分別由 trx_id = 101, 102, 103 執(zhí)行:
| 版本 | balance | DB_TRX_ID | Undo 指向 |
|---|---|---|---|
| V4 | 400 | 103 | → V3 |
| V3 | 300 | 102 | → V2 |
| V2 | 200 | 101 | → V1 |
| V1 | 100 | 100 | NULL |
物理上只保留最新版本 V4,其余通過 Undo Log 鏈?zhǔn)交厮荨?/p>
1.2 Read View 是什么?——原理與機(jī)制
Read View(讀視圖)是 InnoDB 為實(shí)現(xiàn) MVCC 而在內(nèi)存中動(dòng)態(tài)構(gòu)建的一個(gè)一致性快照結(jié)構(gòu)。它的核心作用是:在不加鎖的前提下,讓事務(wù)看到一個(gè)“邏輯上一致”的數(shù)據(jù)庫狀態(tài)。
關(guān)鍵特性:
- ? 純內(nèi)存結(jié)構(gòu):Read View 不寫入磁盤,不持久化,僅存在于事務(wù)執(zhí)行期間的內(nèi)存中。
- ? 一次性創(chuàng)建:在
REPEATABLE READ隔離級別下,事務(wù)執(zhí)行第一個(gè)SELECT語句時(shí)創(chuàng)建,之后全程復(fù)用,不再更新。 - ? 事務(wù)私有:每個(gè)事務(wù)擁有自己的 Read View,彼此隔離。
- ? 輕量但關(guān)鍵:雖然結(jié)構(gòu)簡單,但它決定了整個(gè)事務(wù)能看到哪些數(shù)據(jù)版本。
為什么需要 Read View?
因?yàn)?InnoDB 的行記錄只保存最新版本,歷史版本在 Undo Log 中。當(dāng)一個(gè)事務(wù)讀取數(shù)據(jù)時(shí),它不能簡單地“看到最新值”——那樣會破壞隔離性。
Read View 提供了一套基于事務(wù) ID 的可見性規(guī)則,讓事務(wù)能沿著 Undo 鏈找到“它應(yīng)該看到的那個(gè)版本”。
Read View 的內(nèi)部字段
| 字段 | 含義 |
|---|---|
m_ids | 創(chuàng)建 Read View 時(shí),所有活躍(未提交)事務(wù)的 ID 列表。這些事務(wù)的修改對當(dāng)前事務(wù)不可見。 |
m_up_limit_id | m_ids 中的最小值。即 最小活躍事務(wù) ID。小于該值的事務(wù)都已提交。 |
m_low_limit_id | max(m_ids) + 1。即 下一個(gè)將要分配的事務(wù) ID。大于等于該值的事務(wù)在 Read View 創(chuàng)建時(shí)尚未開始,屬于“未來事務(wù)”。 |
m_creator_trx_id | 當(dāng)前事務(wù)自身的 trx_id。用于識別“自己修改的數(shù)據(jù)”,即使未提交也可見。 |
?? 舉例說明:
假設(shè)事務(wù) T(trx_id=150)創(chuàng)建 Read View 時(shí),系統(tǒng)中只有它自己活躍,則:
m_ids = [150]m_up_limit_id = min([150]) = 150m_low_limit_id = max([150]) + 1 = 151m_creator_trx_id = 150
1.3 可見性判斷規(guī)則
基于上述字段,InnoDB 對某一行版本的 DB_TRX_ID 進(jìn)行如下判斷:
如果是自己修改的:
DB_TRX_ID == m_creator_trx_id→ ? 可見。如果是未來事務(wù)產(chǎn)生的:
DB_TRX_ID >= m_low_limit_id→ ? 不可見。如果是過去已提交事務(wù)產(chǎn)生的:
DB_TRX_ID < m_up_limit_id→ ? 可見。如果是當(dāng)時(shí)活躍但非自己的事務(wù)產(chǎn)生的:
DB_TRX_ID ∈ m_ids且≠ m_creator_trx_id→ ? 不可見。其他情況(如 DB_TRX_ID 在 [m_up_limit_id, m_low_limit_id) 區(qū)間但不在 m_ids 中):
表示該事務(wù)在 Read View 創(chuàng)建前已提交 → ? 可見。若當(dāng)前版本不可見,則沿 Undo 鏈向上查找,直到找到可見版本或鏈尾。
?? 關(guān)鍵點(diǎn):Read View 一旦創(chuàng)建,在 REPEATABLE READ 下全程復(fù)用,直到事務(wù)結(jié)束。
二、案例一:只讀事務(wù)導(dǎo)致 Undo 日志無法清理
2.1 場景還原
事務(wù) T1(trx_id = 150) 執(zhí)行以下操作后忘記提交:
-- T=0 START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- ← 創(chuàng)建 Read View -- 事務(wù)掛起 6 小時(shí)
根據(jù)上述規(guī)則,其 Read View 為:
m_ids = [150]m_up_limit_id = 150m_low_limit_id = 151m_creator_trx_id = 150
這意味著:所有 DB_TRX_ID < 151 的版本都必須保留,因?yàn)樗鼈兛赡鼙?T1 讀取。
2.2 并發(fā)更新與 Undo 積壓
與此同時(shí),業(yè)務(wù)系統(tǒng)高頻更新同一行(如用戶積分),每秒一次,由連續(xù)遞增的事務(wù)執(zhí)行:
-- trx_id = 151, 152, 153, ..., 21750(6小時(shí)共21600次) UPDATE accounts SET points = points + 1 WHERE id = 1;
每次 UPDATE 生成新版本和 Undo 記錄。
為什么不能清理?
- Purge 線程清理?xiàng)l件:所有活躍事務(wù)都不再需要該舊版本。
- 事務(wù) 150 的 Read View 要求:所有
DB_TRX_ID < 151的版本必須保留(包括最初的 trx_id=100)。 - Undo 是鏈?zhǔn)浇Y(jié)構(gòu),只要最老版本(V1)不能刪,整條鏈都必須保留。
- 因此,即使 trx_id=151~21750 的事務(wù)早已提交,它們的 Undo 仍因依賴 V1 而無法 purge。
2.3 故障后果
- Undo 表空間從 500MB 膨脹至 8GB+;
ibdata1文件寫滿,數(shù)據(jù)庫進(jìn)入只讀模式;- 監(jiān)控指標(biāo):
History list length> 200,000;- 簡單查詢延遲從 0.3ms 升至 50ms;
- 磁盤 IO util 達(dá) 98%。
?? 結(jié)論:即使沒有 DML,一個(gè)未提交的
SELECT也能拖垮整個(gè)數(shù)據(jù)庫。
三、案例二:應(yīng)用層“合理”長事務(wù)引發(fā)雪崩
3.1 典型下單流程代碼
@Transactional
public void placeOrder(Long userId, Long productId) {
// 1. 查庫存(SELECT)
int stock = productMapper.selectStock(productId); // ← 創(chuàng)建 Read View!
// 2. 調(diào)用第三方支付(網(wǎng)絡(luò) I/O,耗時(shí) 10~30 秒)
paymentService.callRemoteAPI(...); // ?? 事務(wù)掛起!
// 3. 扣庫存 + 保存訂單
productMapper.decreaseStock(productId);
orderMapper.insert(new Order(...));
}
假設(shè)該事務(wù)分配到 trx_id = 22000。
- Read View:
m_ids = [22000]m_up_limit_id = 22000m_low_limit_id = 22001m_creator_trx_id = 22000
這意味著:所有 DB_TRX_ID < 22001 的版本都必須保留。
3.2 高頻輔助更新放大危害
系統(tǒng)另有服務(wù)每秒更新商品瀏覽量 200 次,由 trx_id = 22001, 22002, … 執(zhí)行:
UPDATE products SET view_count = view_count + 1 WHERE id = 123;
在 20 秒內(nèi):
- 產(chǎn)生 4,000 條 Undo 記錄(trx_id 22001 ~ 26000);
- 所有記錄因事務(wù) 22000 的 Read View 而無法 purge(因?yàn)樗鼈円蕾嚫绨姹荆?/li>
若同時(shí)有 50 個(gè)用戶下單:
- Undo 增長速率 = 200 × 50 × 20 = 200,000 條/分鐘;
- Purge backlog 暴漲;
- 主從復(fù)制延遲從 1 秒升至 15 分鐘;
- 應(yīng)用超時(shí)率飆升。
3.3 根本原因
- 問題不在新事務(wù) ID 大,而在舊版本無法釋放;
- Read View 凍結(jié)了歷史視角,迫使 InnoDB 保留從 trx_id=100 到當(dāng)前的所有中間狀態(tài);
- Undo 日志增長速度 = 熱點(diǎn)行更新頻率 × 長事務(wù)數(shù)量 × 持續(xù)時(shí)間。
四、長事務(wù)的四大系統(tǒng)級危害
| 危害類型 | 機(jī)制 | 后果 |
|---|---|---|
| 磁盤耗盡 | Undo 表空間無法 purge | ibdata1 或 undo tablespace 寫滿,數(shù)據(jù)庫只讀/宕機(jī) |
| 查詢性能暴跌 | 版本鏈過長,MVCC 回溯成本高 | 簡單 SELECT 延遲從 ms 級升至百 ms 級 |
| 主從延遲 | Binlog 積壓 + Slave 回放慢 | 從庫數(shù)據(jù)嚴(yán)重滯后,讀寫分離失效 |
| 鎖沖突加劇 | 行鎖持有時(shí)間過長 | 其他會話阻塞,死鎖概率上升 |
結(jié)語
長事務(wù)的危害,源于 REPEATABLE READ 隔離級別下 Read View 與 Undo Log 的強(qiáng)耦合。
一個(gè)未提交的事務(wù),就像一個(gè)“時(shí)間錨點(diǎn)”,將數(shù)據(jù)庫的歷史牢牢釘住,阻止系統(tǒng)輕裝前行。
真正的穩(wěn)定性,來自于對事務(wù)邊界的敬畏:
讓事務(wù)只做數(shù)據(jù)庫該做的事,且越快越好。
唯有如此,Undo 日志才能及時(shí)回收,版本鏈才不會無限延長,數(shù)據(jù)庫才能在高并發(fā)下穩(wěn)健運(yùn)行。
到此這篇關(guān)于在MySQL中不建議使用長事務(wù)根因的文章就介紹到這了,更多相關(guān)MySQL不建議使用長事務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis統(tǒng)計(jì)每條SQL的執(zhí)行時(shí)間的方法示例
這篇文章主要介紹了mybatis統(tǒng)計(jì)每條SQL的執(zhí)行時(shí)間的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
Navicat連接虛擬機(jī)mysql常見錯(cuò)誤問題及解決方法
這篇文章主要介紹了Navicat連接虛擬機(jī)mysql常見錯(cuò)誤問題及解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
MySQL聯(lián)合查詢之輕松實(shí)現(xiàn)數(shù)據(jù)關(guān)聯(lián)詳解
MySQL中當(dāng)查詢數(shù)據(jù)來自多張表時(shí)需要用到關(guān)聯(lián)查詢,下面這篇文章主要給大家介紹了關(guān)于MySQL聯(lián)合查詢之輕松實(shí)現(xiàn)數(shù)據(jù)關(guān)聯(lián)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06

