前言
本篇文章主要是分享,长事务对undo log 有什么影响。 当然了先看看,长事务(时间长)或者大事务(执行的SQL多)会有哪些影响。
MySQL执行大(长)事务会存在什么问题?
- 占用数据库连接:事务长时间占用数据库链接,但是因为数据库连接是有限的,被长事务占用后,就会导致其他事务可能无法获取连接,导致应用的吞吐量下降,影响系统可用性。
- 难以回滚:由于大事务涉及的数据量较大,执行回滚操作可能会变得非常耗时。如果事务需要回滚或失败,可能需要花费很长时间才能完全回滚所有修改,这会对数据库的可用性和性能造成负面影响。
- 导致主从延迟:大事务可能有很多数据的更新操作,在主库执行的时间很长的话,在同步过程中,就可能在备库上也要执行很长时间的数据同步,那么这段同步的时间就会出现主从延迟。
- 锁竞争:大事务的话,写操作多了就可能要锁定许多数据。这可能导致其他并发事务在访问相同资源时遇到锁竞争,从而导致性能下降和延迟增加。长时间的锁定还可能导致其他事务的等待和阻塞。还容易引发死锁。
- 日志空间占用:大事务会生成大量的日志,尤其是binlog,当单个事务最大允许使用的Binlog文件的大小超过了max_binlog_cache_size时,会导致报错。
- 对MVCC影响:大事务会导致MVCC中旧版本的数据持续存在,影响数据库的Purging,从而影响整体性能。
- 影响索引覆盖:当一个长事务修改一个表时,可能会导致其他事务对该表的查询不使用覆盖索引技术。
解决方案
- 事务拆分:把一个大事务,拆成多个事务(拆分的话,就可能存在中间的事务出现问题,之前提交的事务不能混滚的问题)。
- 减少事务方法处理逻辑:把不需要在事务中的操作,如读操作,内存计算操作、IO操作,远程调用等,放到事务外处理。尽可能的事务中只有更新的SQL操作
长事务导致Undo Log堆积
长事务导致Undo Log堆积的核心机制在于MVCC的版本可见性规则,具体过程可分为以下六个技术层面:
堆积原因
一、事务版本链依赖
-
快照读强依赖:
每个活跃事务在首次读操作时创建Read View,该视图需要访问所有在此事务启动前未提交的修改版本。若存在运行3小时的事务A(trx_id=100),所有trx_id≥100的事务产生的Undo Log都不能被清理。 -
版本链回溯规则:
def traverse_undo_chain(row, read_view): while row is not None: if row.trx_id < read_view.min_trx_id: return row # 找到可见版本 if not is_active(row.trx_id, read_view): row = row.prev else: break return None
该算法要求所有在min_trx_id之后的Undo版本必须保留
二、Purge协调机制
-
全局低水位线计算:
InnoDB维护purge_sys->purge_trx_no
表示可清理的最大事务ID,计算公式:purge_trx_no = MIN(oldest_read_view_trx, oldest_undo_trx)
当存在trx_id=50的长事务时,即使有trx_id=100的事务已提交,50-100之间的Undo Log也无法清除
-
Purge分段扫描:
Purge线程以128个page为单位批量处理,当存在长事务时会出现:// storage/innobase/trx/trx0purge.cc while (page_count < BATCH_SIZE) { if (undo->state != TRX_UNDO_CACHED) // 被长事务占用 break; page_count += undo->size; }
三、Undo Log文件扩展
-
回滚段结构限制:
每个回滚段支持1024个Undo Slot,当长事务持续占用slot时:Rollback Segment 0 ├─ Slot 0: trx100 (active 2h) ├─ Slot 1: trx200 (active 3h) └─ ...
新事务被迫创建新Undo页,导致文件膨胀
-
空间重用失效:
正常情况可复用的Undo页结构:struct trx_undo_t { ulint state; // TRX_UNDO_CACHED状态可复用 ulint size; };
被长事务持有的Undo页始终处于TRX_UNDO_ACTIVE状态,无法进入缓存队列
四、版本存活周期
通过具体实验观察版本保留情况:
-- 会话1(长事务)
START TRANSACTION;
SELECT * FROM users; -- 创建Read View
-- 会话2
UPDATE users SET score=90 WHERE id=1; -- 产生trx_id=150
-- 会话3
UPDATE users SET score=85 WHERE id=1; -- 产生trx_id=200
COMMIT; -- 会话3提交
此时Undo链结构:
当前版本(trx200) ← 版本trx150 ← 初始版本
由于会话1的Read View包含trx150和trx200,这两个Undo记录必须保留
解决方案
上面说了,主要就是从把非update操作移除事务,还是很多SQL就考虑拆分事务(拆分的话,就可能存在中间的事务出现问题,之前提交的事务不能混滚的问题)。
如何查询当前是否存在长时间事务:
活跃事务的查询,查询 事务 时间大于60s的事务
SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
-
内核级改进:
MySQL 8.0引入的Undo Log压缩:// storage/innobase/trx/trx0undo.cc if (undo->size > COMPRESS_THRESHOLD) { trx_undo_compress(undo); // 使用LZ4算法压缩 }
压缩率可达30%-50%
-
自适应Purge策略:
动态调整Purge线程数量:innodb_purge_threads = max(4, CPU_CORES * 0.25)
-
事务分片:
将大事务拆分为批次处理:batch_size = 1000 for i in range(0, total_rows, batch_size): with conn.transaction(): update_batch(i, i+batch_size) time.sleep(0.1) # 允许Purge介入
-
防御性编程:
在ORM框架层强制设置超时:// Hibernate配置 properties.put("javax.persistence.query.timeout", 30000); properties.put("hibernate.transaction.timeout", 60);
总结
通过上述多层次的机制分析可见,长事务导致的Undo堆积本质上是由于MVCC的版本可见性规则与Purge机制的协同问题。解决该问题需要从事务管理、系统监控、存储引擎优化等多个维度进行综合治理。