长事务导致undo log堆积

前言

本篇文章主要是分享,长事务对undo log 有什么影响。 当然了先看看,长事务(时间长)或者大事务(执行的SQL多)会有哪些影响。

MySQL执行大(长)事务会存在什么问题?

  1. 占用数据库连接:事务长时间占用数据库链接,但是因为数据库连接是有限的,被长事务占用后,就会导致其他事务可能无法获取连接,导致应用的吞吐量下降,影响系统可用性。
  2. 难以回滚:由于大事务涉及的数据量较大,执行回滚操作可能会变得非常耗时。如果事务需要回滚或失败,可能需要花费很长时间才能完全回滚所有修改,这会对数据库的可用性和性能造成负面影响。
  3. 导致主从延迟:大事务可能有很多数据的更新操作,在主库执行的时间很长的话,在同步过程中,就可能在备库上也要执行很长时间的数据同步,那么这段同步的时间就会出现主从延迟。
  4. 锁竞争:大事务的话,写操作多了就可能要锁定许多数据。这可能导致其他并发事务在访问相同资源时遇到锁竞争,从而导致性能下降和延迟增加。长时间的锁定还可能导致其他事务的等待和阻塞。还容易引发死锁。
  5. 日志空间占用:大事务会生成大量的日志,尤其是binlog,当单个事务最大允许使用的Binlog文件的大小超过了max_binlog_cache_size时,会导致报错。
  6. 对MVCC影响:大事务会导致MVCC中旧版本的数据持续存在,影响数据库的Purging,从而影响整体性能
  7. 影响索引覆盖:当一个长事务修改一个表时,可能会导致其他事务对该表的查询不使用覆盖索引技术。

解决方案

  1. 事务拆分:把一个大事务,拆成多个事务(拆分的话,就可能存在中间的事务出现问题,之前提交的事务不能混滚的问题)。
  2. 减少事务方法处理逻辑:把不需要在事务中的操作,如读操作,内存计算操作、IO操作,远程调用等,放到事务外处理。尽可能的事务中只有更新的SQL操作

长事务导致Undo Log堆积

长事务导致Undo Log堆积的核心机制在于MVCC的版本可见性规则,具体过程可分为以下六个技术层面:

堆积原因

一、事务版本链依赖

  1. 快照读强依赖
    每个活跃事务在首次读操作时创建Read View,该视图需要访问所有在此事务启动前未提交的修改版本。若存在运行3小时的事务A(trx_id=100),所有trx_id≥100的事务产生的Undo Log都不能被清理。

  2. 版本链回溯规则

    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协调机制

  1. 全局低水位线计算
    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也无法清除

  2. 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文件扩展

  1. 回滚段结构限制
    每个回滚段支持1024个Undo Slot,当长事务持续占用slot时:

    Rollback Segment 0
    ├─ Slot 0: trx100 (active 2h)
    ├─ Slot 1: trx200 (active 3h)
    └─ ...
    

    新事务被迫创建新Undo页,导致文件膨胀

  2. 空间重用失效
    正常情况可复用的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;

  1. 内核级改进
    MySQL 8.0引入的Undo Log压缩:

    // storage/innobase/trx/trx0undo.cc
    if (undo->size > COMPRESS_THRESHOLD) {
        trx_undo_compress(undo);  // 使用LZ4算法压缩
    }
    

    压缩率可达30%-50%

  2. 自适应Purge策略
    动态调整Purge线程数量:

    innodb_purge_threads = max(4, CPU_CORES * 0.25)
    
  3. 事务分片
    将大事务拆分为批次处理:

    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介入
    
  4. 防御性编程
    在ORM框架层强制设置超时:

    // Hibernate配置
    properties.put("javax.persistence.query.timeout", 30000); 
    properties.put("hibernate.transaction.timeout", 60);
    

总结

通过上述多层次的机制分析可见,长事务导致的Undo堆积本质上是由于MVCC的版本可见性规则与Purge机制的协同问题。解决该问题需要从事务管理、系统监控、存储引擎优化等多个维度进行综合治理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

提前退休了-程序员阿飞

兄弟们能否给口饭吃

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值