在MySQL中不建議使用長事務的根因詳析
引言
“不要使用長事務”是 MySQL 開發(fā)與運維中的黃金準則。然而,許多開發(fā)者僅將其視為性能建議,卻未意識到其背后隱藏著系統(tǒng)級崩潰風險。本文將從 InnoDB 的底層機制出發(fā),結合具體事務 ID(trx_id)、Undo Log 版本鏈、Read View 快照等核心組件,徹底剖析:
- 為什么 REPEATABLE READ 隔離級別必須維護歷史版本;
- 為什么一個只包含
SELECT的事務也能導致磁盤寫滿; - 為什么“事務中調(diào)用支付接口”這類看似合理的代碼會引發(fā)雪崩。
只有理解了 MVCC 的完整工作流,才能真正明白:長事務的本質(zhì),是讓整個數(shù)據(jù)庫為你的快照背負歷史包袱。
一、可重復讀(REPEATABLE READ)的實現(xiàn)原理
MySQL InnoDB 引擎在 REPEATABLE READ 隔離級別下,通過 MVCC(多版本并發(fā)控制) 實現(xiàn)一致性非鎖定讀。其核心依賴三個要素:
每行記錄的隱藏字段:
DB_TRX_ID:最后一次修改該行的事務 ID;DB_ROLL_PTR:指向 Undo Log 中的歷史版本指針。
Undo Log:存儲數(shù)據(jù)的歷史版本,形成版本鏈(Version Chain)。
Read View:事務執(zhí)行第一個
SELECT時創(chuàng)建的一致性視圖,用于判斷哪些版本可見。
1.1 版本鏈示例
假設初始插入由事務 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 鏈式回溯。
1.2 Read View 是什么?——原理與機制
Read View(讀視圖)是 InnoDB 為實現(xiàn) MVCC 而在內(nèi)存中動態(tài)構建的一個一致性快照結構。它的核心作用是:在不加鎖的前提下,讓事務看到一個“邏輯上一致”的數(shù)據(jù)庫狀態(tài)。
關鍵特性:
- ? 純內(nèi)存結構:Read View 不寫入磁盤,不持久化,僅存在于事務執(zhí)行期間的內(nèi)存中。
- ? 一次性創(chuàng)建:在
REPEATABLE READ隔離級別下,事務執(zhí)行第一個SELECT語句時創(chuàng)建,之后全程復用,不再更新。 - ? 事務私有:每個事務擁有自己的 Read View,彼此隔離。
- ? 輕量但關鍵:雖然結構簡單,但它決定了整個事務能看到哪些數(shù)據(jù)版本。
為什么需要 Read View?
因為 InnoDB 的行記錄只保存最新版本,歷史版本在 Undo Log 中。當一個事務讀取數(shù)據(jù)時,它不能簡單地“看到最新值”——那樣會破壞隔離性。
Read View 提供了一套基于事務 ID 的可見性規(guī)則,讓事務能沿著 Undo 鏈找到“它應該看到的那個版本”。
Read View 的內(nèi)部字段
| 字段 | 含義 |
|---|---|
m_ids | 創(chuàng)建 Read View 時,所有活躍(未提交)事務的 ID 列表。這些事務的修改對當前事務不可見。 |
m_up_limit_id | m_ids 中的最小值。即 最小活躍事務 ID。小于該值的事務都已提交。 |
m_low_limit_id | max(m_ids) + 1。即 下一個將要分配的事務 ID。大于等于該值的事務在 Read View 創(chuàng)建時尚未開始,屬于“未來事務”。 |
m_creator_trx_id | 當前事務自身的 trx_id。用于識別“自己修改的數(shù)據(jù)”,即使未提交也可見。 |
?? 舉例說明:
假設事務 T(trx_id=150)創(chuàng)建 Read View 時,系統(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 進行如下判斷:
如果是自己修改的:
DB_TRX_ID == m_creator_trx_id→ ? 可見。如果是未來事務產(chǎn)生的:
DB_TRX_ID >= m_low_limit_id→ ? 不可見。如果是過去已提交事務產(chǎn)生的:
DB_TRX_ID < m_up_limit_id→ ? 可見。如果是當時活躍但非自己的事務產(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 中):
表示該事務在 Read View 創(chuàng)建前已提交 → ? 可見。若當前版本不可見,則沿 Undo 鏈向上查找,直到找到可見版本或鏈尾。
?? 關鍵點:Read View 一旦創(chuàng)建,在 REPEATABLE READ 下全程復用,直到事務結束。
二、案例一:只讀事務導致 Undo 日志無法清理
2.1 場景還原
事務 T1(trx_id = 150) 執(zhí)行以下操作后忘記提交:
-- T=0 START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- ← 創(chuàng)建 Read View -- 事務掛起 6 小時
根據(jù)上述規(guī)則,其 Read View 為:
m_ids = [150]m_up_limit_id = 150m_low_limit_id = 151m_creator_trx_id = 150
這意味著:所有 DB_TRX_ID < 151 的版本都必須保留,因為它們可能被 T1 讀取。
2.2 并發(fā)更新與 Undo 積壓
與此同時,業(yè)務系統(tǒng)高頻更新同一行(如用戶積分),每秒一次,由連續(xù)遞增的事務執(zhí)行:
-- trx_id = 151, 152, 153, ..., 21750(6小時共21600次) UPDATE accounts SET points = points + 1 WHERE id = 1;
每次 UPDATE 生成新版本和 Undo 記錄。
為什么不能清理?
- Purge 線程清理條件:所有活躍事務都不再需要該舊版本。
- 事務 150 的 Read View 要求:所有
DB_TRX_ID < 151的版本必須保留(包括最初的 trx_id=100)。 - Undo 是鏈式結構,只要最老版本(V1)不能刪,整條鏈都必須保留。
- 因此,即使 trx_id=151~21750 的事務早已提交,它們的 Undo 仍因依賴 V1 而無法 purge。
2.3 故障后果
- Undo 表空間從 500MB 膨脹至 8GB+;
ibdata1文件寫滿,數(shù)據(jù)庫進入只讀模式;- 監(jiān)控指標:
History list length> 200,000;- 簡單查詢延遲從 0.3ms 升至 50ms;
- 磁盤 IO util 達 98%。
?? 結論:即使沒有 DML,一個未提交的
SELECT也能拖垮整個數(shù)據(jù)庫。
三、案例二:應用層“合理”長事務引發(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)絡 I/O,耗時 10~30 秒)
paymentService.callRemoteAPI(...); // ?? 事務掛起!
// 3. 扣庫存 + 保存訂單
productMapper.decreaseStock(productId);
orderMapper.insert(new Order(...));
}
假設該事務分配到 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)另有服務每秒更新商品瀏覽量 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);
- 所有記錄因事務 22000 的 Read View 而無法 purge(因為它們依賴更早版本)。
若同時有 50 個用戶下單:
- Undo 增長速率 = 200 × 50 × 20 = 200,000 條/分鐘;
- Purge backlog 暴漲;
- 主從復制延遲從 1 秒升至 15 分鐘;
- 應用超時率飆升。
3.3 根本原因
- 問題不在新事務 ID 大,而在舊版本無法釋放;
- Read View 凍結了歷史視角,迫使 InnoDB 保留從 trx_id=100 到當前的所有中間狀態(tài);
- Undo 日志增長速度 = 熱點行更新頻率 × 長事務數(shù)量 × 持續(xù)時間。
四、長事務的四大系統(tǒng)級危害
| 危害類型 | 機制 | 后果 |
|---|---|---|
| 磁盤耗盡 | Undo 表空間無法 purge | ibdata1 或 undo tablespace 寫滿,數(shù)據(jù)庫只讀/宕機 |
| 查詢性能暴跌 | 版本鏈過長,MVCC 回溯成本高 | 簡單 SELECT 延遲從 ms 級升至百 ms 級 |
| 主從延遲 | Binlog 積壓 + Slave 回放慢 | 從庫數(shù)據(jù)嚴重滯后,讀寫分離失效 |
| 鎖沖突加劇 | 行鎖持有時間過長 | 其他會話阻塞,死鎖概率上升 |
結語
長事務的危害,源于 REPEATABLE READ 隔離級別下 Read View 與 Undo Log 的強耦合。
一個未提交的事務,就像一個“時間錨點”,將數(shù)據(jù)庫的歷史牢牢釘住,阻止系統(tǒng)輕裝前行。
真正的穩(wěn)定性,來自于對事務邊界的敬畏:
讓事務只做數(shù)據(jù)庫該做的事,且越快越好。
唯有如此,Undo 日志才能及時回收,版本鏈才不會無限延長,數(shù)據(jù)庫才能在高并發(fā)下穩(wěn)健運行。
到此這篇關于在MySQL中不建議使用長事務根因的文章就介紹到這了,更多相關MySQL不建議使用長事務內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Ubuntu上檢查MySQL是否啟動并放開3306端口的常見方法
在使用Ubuntu系統(tǒng)時,MySQL數(shù)據(jù)庫是許多開發(fā)人員和系統(tǒng)管理員的常用工具,本文將詳細介紹如何在Ubuntu上檢查MySQL是否啟動,以及如何放開MySQL默認的3306端口,以便允許外部訪問,需要的朋友可以參考下2025-07-07
分享MYSQL插入數(shù)據(jù)時忽略重復數(shù)據(jù)的方法
當程序中insert時,已存在的數(shù)據(jù)不插入,不存在的數(shù)據(jù)insert。在網(wǎng)上搜了下,可以使用存儲過程或者是用NOT EXISTS 來判斷是否存在2013-09-09
MySQL學習筆記2:數(shù)據(jù)庫的基本操作(創(chuàng)建刪除查看)
我們所安裝的MySQL說白了是一個數(shù)據(jù)庫的管理工具,真正有價值的東西在于數(shù)據(jù)關系型數(shù)據(jù)庫的數(shù)據(jù)是以表的形式存在的,N個表匯總在一起就成了一個數(shù)據(jù)庫現(xiàn)在來看看數(shù)據(jù)庫的基本操作2013-01-01
詳解用SELECT命令在MySQL執(zhí)行查詢操作的教程
這篇文章主要介紹了詳解用SELECT命令在MySQL執(zhí)行查詢操作的教程,本文中還給出了基于PHP腳本的操作演示,需要的朋友可以參考下2015-05-05
mysql:Can''t start server: can''t create PID file: No space
這篇文章主要介紹了mysql啟動失敗不能正常啟動并報錯Can't start server: can't create PID file: No space left on device問題解決方法,需要的朋友可以參考下2015-05-05

