MySQL 事务
一、 什么是事务?
- 核心目的:保证一组数据库操作要么 全部成功,要么 全部失败。
- 经典场景:银行转账(查询余额、扣款、加款必须是原子操作)。
- MySQL 实现:事务支持在 存储引擎层 实现。
InnoDB
:支持 事务(主流选择)。MyISAM
:不支持 事务。
二、 事务特性 (ACID) - 重点:隔离性 (Isolation)
-
ACID:
- Atomicity (原子性):要么全做,要么全不做。
- Consistency (一致性):事务执行前后,数据库状态从一个一致性状态转移到另一个一致性状态。
- Isolation (隔离性):并发事务之间互不干扰。
- Durability (持久性):事务一旦提交,其结果就是永久性的。
-
并发问题:多个事务同时执行可能导致:
- 脏读 (Dirty Read):读到其他事务未提交的数据。
- 不可重复读 (Non-repeatable Read):同一事务内,多次读取同一数据,结果不同(因为其他事务修改并提交了)。
- 幻读 (Phantom Read):同一事务内,多次查询符合某个范围的记录,结果集数量不同(因为其他事务插入或删除了符合条件的记录)。
三、 SQL 标准隔离级别
-
目标:解决并发问题,但隔离级别越高,并发性能越低。需要在效率和安全性之间找到平衡。
-
读未提交 (Read Uncommitted):
- 现象:能读到其他事务 未提交 的变更。V1=2, V2=2, V3=2。
- 问题:脏读、不可重复读、幻读都可能发生。
- 实现:直接返回记录最新值,无视图概念。
-
读提交 (Read Committed):
- 现象:只能读到其他事务 已提交 的变更。V1=1, V2=2, V3=2。
- 解决:避免脏读。
- 实现:每个 SQL 语句开始时 创建一个视图 (Read View)。
- 注意:Oracle 默认 隔离级别。从 Oracle 迁移到 MySQL 时,可能需要将 MySQL 设为此级别 (
set transaction_isolation = 'READ-COMMITTED';
)。
-
可重复读 (Repeatable Read):
- 现象:事务执行期间看到的数据,始终与 事务启动时 看到的数据一致。V1=1, V2=1, V3=2 (假设事务 A 在 B 提交后才执行 V3 查询)。
- 解决:避免脏读、不可重复读。
- 实现:事务启动时 创建一个视图 (Read View),整个事务期间使用此视图。
- 注意:MySQL InnoDB 默认 隔离级别。可能出现幻读(InnoDB 通过 MVCC 和 Next-Key Lock 解决了部分幻读问题)。
- 适用场景:数据校对等需要事务期间数据快照一致的场景。
-
串行化 (Serializable):
- 现象:强制事务串行执行。读加读锁,写加写锁。V1=1, V2=1, V3=2 (事务 B 在修改时会被阻塞,直到 A 提交)。
- 解决:避免所有并发问题(脏读、不可重复读、幻读)。
- 实现:通过加锁实现,效率最低。
四、 事务隔离的实现 (以可重复读为例 - MVCC)
- 核心机制:多版本并发控制 (MVCC, Multi-Version Concurrency Control)。
- 关键组件:
- 回滚日志 (Undo Log):每次记录更新时,同时记录一条回滚操作,用于将数据恢复到前一个版本。
- Read View (读视图):事务进行快照读时,会基于当前活跃事务列表等信息生成一个 Read View,决定能看到哪个版本的数据。
- 工作原理:
- 查询时,根据事务的 Read View 和记录上的版本信息(隐藏字段,如事务 ID),通过回滚日志链找到该事务可见的版本。
- 不同事务(不同 Read View)访问同一条记录,可能看到不同的版本。
- 回滚日志的删除:当系统确认 没有 比该回滚日志更早的 Read View (即没有活跃事务需要访问该旧版本) 时,回滚日志才会被删除。
五、 长事务的风险与避免
- 什么是长事务:执行时间过长的事务。
- 风险:
- 存储空间:导致系统需要保留大量的、非常老旧的回滚日志,占用巨大存储空间 (尤其在 MySQL 5.5 及之前,ibdata 文件不会自动收缩)。
- 锁资源:可能长时间持有锁,阻塞其他事务,影响数据库性能,甚至拖垮整个库。
- 事务启动方式:
- 显式启动 (推荐):
BEGIN
或START TRANSACTION
。提交用COMMIT
,回滚用ROLLBACK
。- 需要
set autocommit=1
(默认值)。
- 需要
- 隐式启动 (不推荐):
set autocommit=0
。执行任何 SQL 都会启动事务,需手动COMMIT
或ROLLBACK
或断开连接才结束。- 风险:容易在长连接或连接池场景下,因忘记提交而产生意外的长事务。
- 显式启动 (推荐):
- 优化频繁事务:使用
COMMIT WORK AND CHAIN
。- 作用:提交当前事务,并 自动开启 下一个事务。
- 优点:省去了再次执行
BEGIN
的交互开销,同时保持了事务边界的明确性。
- 监控长事务:
- 查询
information_schema.innodb_trx
表。 - 示例:查找持续超过 60 秒的事务:
sql SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
- 查询
六、 如何避免或处理长事务
-
代码层面:
- 拆分大事务:将复杂的业务逻辑分解成更小的事务单元。
- 剥离非必要操作:将查询、耗时计算、外部系统调用(RPC)等操作移出事务边界。先准备好数据,再开启事务快速执行 DML。
- 优化 SQL:确保事务内的 SQL 语句高效执行,避免慢查询。
- 避免交互:绝不在事务中等待用户输入或外部系统的同步响应。
-
配置与架构层面:
- 坚持
autocommit=1
:使用显式事务管理。 - 合理设置超时:配置
innodb_lock_wait_timeout
,避免事务长时间等待锁。 - 异步处理:对于非核心、可最终一致的逻辑,考虑使用消息队列等异步方式处理。
- 坚持
-
监控与运维:
- 建立监控告警:定期检查
information_schema.innodb_trx
,对超过阈值的长事务进行告警。 - 分析与干预:对告警的长事务进行分析,确认其状态和影响,必要时手动
KILL
掉(需谨慎评估影响)。 - 自动化工具:考虑使用如
pt-kill
等工具,根据预设规则自动查杀符合条件的长事务或空闲事务。 - 定期审查:定期 review 代码中的事务使用方式,确保符合规范。
- 建立监控告警:定期检查