MySQL 长事务的危害
死锁(Deadlocks)
长事务增加了死锁的可能性。死锁发生在两个或多个事务互相等待对方释放锁的情况下,导致事务无法继续执行。例如,事务 A 持有表 T1 的行锁并试图获取表 T2 的行锁,而事务 B 持有表 T2 的行锁并试图获取表 T1 的行锁,这样就会形成死锁。
锁竞争(Lock Contention)
长事务会占用更多的锁资源,导致其他事务在等待锁释放时阻塞。这会增加锁的竞争,降低整体的并发性能。例如,一个长事务可能会锁定大量数据行,从而阻止其他事务对这些数据进行读写操作。
行版本数量增加(Row Versioning)
在多版本并发控制(MVCC)机制下,每个事务读取数据时都会看到一个特定时间点的数据版本。长事务会导致更多的行版本被创建,因为事务需要保留数据的多个版本以便回滚。这会增加存储开销和内存使用。
事务回滚成本增加
如果长事务最终需要回滚,那么回滚的成本也会更高。这是因为需要根据 undo log 恢复大量的数据操作,这不仅消耗更多的 CPU 时间,还会占用更多的磁盘 I/O 资源。
数据库膨胀(Database Bloat)
长事务可能会导致数据库文件膨胀。这是因为 InnoDB 存储引擎在删除行时并不会立即回收空间,而是标记为逻辑删除。如果长事务频繁执行删除操作而不提交,那么数据库文件可能会变得非常大。
一致性读取问题
长事务可能会影响其他事务的一致性读取。如果一个事务执行了很长时间,导致 undo log 不能被回收,导致回滚段空间膨胀。
空间回收延迟
InnoDB 存储引擎在事务提交后才会回收空间。如果事务长时间未提交,那么已删除的数据行所占用的空间无法被立即回收,这会导致空间浪费。
性能瓶颈
长事务可能会成为数据库性能的瓶颈。由于事务长时间占用资源,可能会导致数据库的整体吞吐量下降,响应时间变长。
日志文件增长
长事务会生成大量的 Redo 日志(redo log),这会导致日志文件的增长。如果日志文件过大,可能会导致日志切换等问题,进而影响数据库的性能。
同样,大事务还会影响到 Binlog。每个线程有一个 binlog cache。如果 binlog 要记录的内容过多,会导致超过 cache 大小,从而需要交换到磁盘中。
故障恢复难度
如果数据库发生故障需要恢复,那么长事务的恢复过程会更加复杂。因为需要从 undo log 中恢复大量的操作,这会增加恢复的时间和复杂度。
主备延迟
如何避免长事务对业务的影响
首先,从应用开发端来看:
- 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。
- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。
- 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)
其次,从数据库端来看:
- 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警/或者 kill;
- Percona 的 pt-kill 这个工具不错,推荐使用;
- 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
- 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
为了避免长事务带来的问题,可以采取以下措施:
- 优化事务设计:尽量减少事务的持续时间,将大事务拆分成多个小事务。
- 定期提交:在事务中定期执行
COMMIT操作,以减少事务的锁持有时间和资源占用。 - 使用乐观锁:对于读多写少的应用场景,可以考虑使用乐观锁来减少锁的竞争。
- 调整隔离级别:根据应用需求合理调整事务的隔离级别,减少锁的使用。
- 监控和报警:设置监控和报警机制,及时发现并处理长事务。
- 优化索引:合理设计索引,减少不必要的锁竞争。
- 使用分区表:对于大数据量的表,可以考虑使用分区表来分散锁的范围。