引言
“不要使用长事务”是 mysql 开发与运维中的黄金准则。然而,许多开发者仅将其视为性能建议,却未意识到其背后隐藏着系统级崩溃风险。本文将从 innodb 的底层机制出发,结合具体事务 id(trx_id)、undo log 版本链、read view 快照等核心组件,彻底剖析:
- 为什么 repeatable read 隔离级别必须维护历史版本;
- 为什么一个只包含
select的事务也能导致磁盘写满; - 为什么“事务中调用支付接口”这类看似合理的代码会引发雪崩。
只有理解了 mvcc 的完整工作流,才能真正明白:长事务的本质,是让整个数据库为你的快照背负历史包袱。
一、可重复读(repeatable read)的实现原理
mysql innodb 引擎在 repeatable read 隔离级别下,通过 mvcc(多版本并发控制) 实现一致性非锁定读。其核心依赖三个要素:
每行记录的隐藏字段:
db_trx_id:最后一次修改该行的事务 id;db_roll_ptr:指向 undo log 中的历史版本指针。
undo log:存储数据的历史版本,形成版本链(version chain)。
read view:事务执行第一个
select时创建的一致性视图,用于判断哪些版本可见。
1.1 版本链示例
假设初始插入由事务 trx_id = 100 完成:
insert into accounts (id, balance) values (1, 100);
随后三次更新分别由 trx_id = 101, 102, 103 执行:
| 版本 | 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 为实现 mvcc 而在内存中动态构建的一个一致性快照结构。它的核心作用是:在不加锁的前提下,让事务看到一个“逻辑上一致”的数据库状态。
关键特性:
- ✅ 纯内存结构:read view 不写入磁盘,不持久化,仅存在于事务执行期间的内存中。
- ✅ 一次性创建:在
repeatable read隔离级别下,事务执行第一个select语句时创建,之后全程复用,不再更新。 - ✅ 事务私有:每个事务拥有自己的 read view,彼此隔离。
- ✅ 轻量但关键:虽然结构简单,但它决定了整个事务能看到哪些数据版本。
为什么需要 read view?
因为 innodb 的行记录只保存最新版本,历史版本在 undo log 中。当一个事务读取数据时,它不能简单地“看到最新值”——那样会破坏隔离性。
read view 提供了一套基于事务 id 的可见性规则,让事务能沿着 undo 链找到“它应该看到的那个版本”。
read view 的内部字段
| 字段 | 含义 |
|---|---|
m_ids | 创建 read view 时,所有活跃(未提交)事务的 id 列表。这些事务的修改对当前事务不可见。 |
m_up_limit_id | m_ids 中的最小值。即 最小活跃事务 id。小于该值的事务都已提交。 |
m_low_limit_id | max(m_ids) + 1。即 下一个将要分配的事务 id。大于等于该值的事务在 read view 创建时尚未开始,属于“未来事务”。 |
m_creator_trx_id | 当前事务自身的 trx_id。用于识别“自己修改的数据”,即使未提交也可见。 |
📌 举例说明:
假设事务 t(trx_id=150)创建 read view 时,系统中只有它自己活跃,则:
m_ids = [150]m_up_limit_id = min([150]) = 150m_low_limit_id = max([150]) + 1 = 151m_creator_trx_id = 150
1.3 可见性判断规则
基于上述字段,innodb 对某一行版本的 db_trx_id 进行如下判断:
如果是自己修改的:
db_trx_id == m_creator_trx_id→ ✅ 可见。如果是未来事务产生的:
db_trx_id >= m_low_limit_id→ ❌ 不可见。如果是过去已提交事务产生的:
db_trx_id < m_up_limit_id→ ✅ 可见。如果是当时活跃但非自己的事务产生的:
db_trx_id ∈ m_ids且≠ m_creator_trx_id→ ❌ 不可见。其他情况(如 db_trx_id 在 [m_up_limit_id, m_low_limit_id) 区间但不在 m_ids 中):
表示该事务在 read view 创建前已提交 → ✅ 可见。若当前版本不可见,则沿 undo 链向上查找,直到找到可见版本或链尾。
⚠️ 关键点:read view 一旦创建,在 repeatable read 下全程复用,直到事务结束。
二、案例一:只读事务导致 undo 日志无法清理
2.1 场景还原
事务 t1(trx_id = 150) 执行以下操作后忘记提交:
-- t=0 start transaction; select balance from accounts where id = 1; -- ← 创建 read view -- 事务挂起 6 小时
根据上述规则,其 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 并发更新与 undo 积压
与此同时,业务系统高频更新同一行(如用户积分),每秒一次,由连续递增的事务执行:
-- 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文件写满,数据库进入只读模式;- 监控指标:
history list length> 200,000;- 简单查询延迟从 0.3ms 升至 50ms;
- 磁盘 io util 达 98%。
💥 结论:即使没有 dml,一个未提交的
select也能拖垮整个数据库。
三、案例二:应用层“合理”长事务引发雪崩
3.1 典型下单流程代码
@transactional
public void placeorder(long userid, long productid) {
// 1. 查库存(select)
int stock = productmapper.selectstock(productid); // ← 创建 read view!
// 2. 调用第三方支付(网络 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 高频辅助更新放大危害
系统另有服务每秒更新商品浏览量 200 次,由 trx_id = 22001, 22002, … 执行:
update products set view_count = view_count + 1 where id = 123;
在 20 秒内:
- 产生 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 到当前的所有中间状态;
- undo 日志增长速度 = 热点行更新频率 × 长事务数量 × 持续时间。
四、长事务的四大系统级危害
| 危害类型 | 机制 | 后果 |
|---|---|---|
| 磁盘耗尽 | undo 表空间无法 purge | ibdata1 或 undo tablespace 写满,数据库只读/宕机 |
| 查询性能暴跌 | 版本链过长,mvcc 回溯成本高 | 简单 select 延迟从 ms 级升至百 ms 级 |
| 主从延迟 | binlog 积压 + slave 回放慢 | 从库数据严重滞后,读写分离失效 |
| 锁冲突加剧 | 行锁持有时间过长 | 其他会话阻塞,死锁概率上升 |
结语
长事务的危害,源于 repeatable read 隔离级别下 read view 与 undo log 的强耦合。
一个未提交的事务,就像一个“时间锚点”,将数据库的历史牢牢钉住,阻止系统轻装前行。
真正的稳定性,来自于对事务边界的敬畏:
让事务只做数据库该做的事,且越快越好。
唯有如此,undo 日志才能及时回收,版本链才不会无限延长,数据库才能在高并发下稳健运行。
到此这篇关于在mysql中不建议使用长事务根因的文章就介绍到这了,更多相关mysql不建议使用长事务内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论