Appearance
事务
一、事务的基本概念
事务(Transaction)是一组原子性的SQL操作,要么全部成功(提交,Commit),要么全部失败(回滚,Rollback)。例如“转账”操作(A扣钱→B加钱)必须作为一个事务,不能只执行其中一步。
二、事务的核心特性:ACID
SQL标准定义了事务的四大特性,是事务的“基石”:
| 特性 | 定义 | 实现方式 |
|---|---|---|
| 原子性(Atomicity) | 事务中的操作“要么全做,要么全不做”,不会中间中断。 | undo log(回滚日志):记录操作的反向逻辑,用于回滚未提交的事务。 |
| 一致性(Consistency) | 事务执行前后,数据库状态必须保持业务逻辑一致(如转账总金额不变)。 | 依赖原子性、隔离性、持久性的共同保证,以及业务逻辑的正确性(如约束检查)。 |
| 持久性(Durability) | 事务提交后,修改的数据永久保存,即使系统崩溃也不会丢失。 | redo log(重做日志):先写日志再写数据,崩溃后通过日志恢复未刷盘的修改。 |
| 隔离性(Isolation) | 多个事务并发执行时,彼此之间不会互相干扰,每个事务看到的数据一致。 | 锁机制(行级锁、表级锁)+ MVCC(多版本并发控制)。 |
三、事务的生命周期
MySQL中事务的生命周期由以下命令控制:
- 开始事务:
START TRANSACTION或BEGIN(显式开启)。 - 提交事务:
COMMIT(将修改持久化到数据库)。 - 回滚事务:
ROLLBACK(撤销未提交的修改,恢复到事务开始前的状态)。
默认行为:MySQL默认开启autocommit=1(自动提交),即每个SQL语句都是一个独立的事务(执行后自动提交)。可通过SET autocommit=0关闭自动提交,此时需要手动COMMIT或ROLLBACK。
四、事务的隔离级别(并发控制的核心)
隔离性解决的是并发事务之间的干扰问题。SQL标准定义了四个隔离级别,从低到高依次为:
| 隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) | 实现方式 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | ✅允许 | ✅允许 | ✅允许 | 无锁(直接读最新版本) |
| 读已提交(Read Committed) | ❌禁止 | ✅允许 | ✅允许 | MVCC(快照读)+ 行级锁 |
| 可重复读(Repeatable Read) | ❌禁止 | ❌禁止 | ❌(InnoDB解决) | MVCC(快照读)+ 行级锁 |
| 串行化(Serializable) | ❌禁止 | ❌禁止 | ❌禁止 | 表级锁(串行执行) |
1. 关键并发问题解释
- 脏读:读取未提交的事务数据(如事务A修改了数据但未提交,事务B读取后,A回滚,B读到“脏数据”)。
- 不可重复读:同一事务中多次读取同一数据,结果不一致(如事务B第一次读是100,事务A修改并提交为200,B再次读变成200)。
- 幻读:同一事务中多次执行同一查询,结果集行数不一致(如事务B读
age=20有10条,事务A插入1条并提交,B再次读变成11条)。
2. MySQL默认隔离级别:可重复读(Repeatable Read)
InnoDB引擎在可重复读级别下,通过MVCC(多版本并发控制)解决了幻读(注意:SQL标准中可重复读允许幻读,但InnoDB优化了这一点)。
五、事务的实现原理
InnoDB引擎通过锁机制、MVCC和事务日志共同实现事务的ACID特性。
1. 锁机制(解决并发修改冲突)
InnoDB支持行级锁(粒度细,并发高)和表级锁(粒度粗,并发低),核心锁类型:
- 共享锁(S锁):允许事务读数据,不允许写(多个事务可同时持有S锁)。
示例:SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE;(加S锁)。 - 排他锁(X锁):允许事务修改数据,不允许其他事务读或写(仅一个事务可持有X锁)。
示例:SELECT * FROM user WHERE id=1 FOR UPDATE;(加X锁)、INSERT/UPDATE/DELETE(隐式加X锁)。
意向锁(Intention Lock):表级锁,用于快速判断表中是否有行级锁(如意向共享锁IS、意向排他锁IX),避免全表扫描检查锁。
2. MVCC(多版本并发控制:解决并发读冲突)
MVCC是InnoDB的核心并发控制机制,通过保存数据的多个版本,让事务读取快照数据(而非最新数据),从而实现“读不加锁,写加锁”的高并发。
(1)MVCC的实现依赖
- 事务ID(trx_id):每个事务启动时分配的唯一ID(递增)。
- 回滚段(Undo Log):记录数据的旧版本,用于回滚或快照读。
- 隐藏列:InnoDB的每一行数据都有两个隐藏列:
trx_id:最后修改该行的事务ID。roll_ptr:指向回滚段中undo log的指针(用于构建版本链)。
(2)版本链的构建
当事务修改数据时,InnoDB会:
- 将旧数据复制到undo log(形成一个旧版本)。
- 更新该行的
trx_id为当前事务ID。 - 更新
roll_ptr指向undo log中的旧版本。
这样,多个版本的数据通过roll_ptr连成一条版本链。
(3)快照读的规则(Read View)
事务读取数据时,InnoDB会生成一个Read View(快照),包含以下信息:
m_low_limit_id:当前事务启动时,最大的已提交事务ID(即“快照的上限”)。m_up_limit_id:当前事务启动时,最小的未提交事务ID(即“快照的下限”)。m_creator_trx_id:当前事务的ID。
读取规则(以可重复读为例):
- 若行的
trx_id≤m_low_limit_id(已提交的事务):可见。 - 若行的
trx_id=m_creator_trx_id(当前事务修改的):可见。 - 否则:通过
roll_ptr回溯版本链,找到符合条件的旧版本。
示例(可重复读级别):
- 事务A(trx_id=100)启动,读取
user表中age=20的记录(快照读,看到版本链中trx_id≤100的版本)。 - 事务B(trx_id=200)插入一条
age=20的记录并提交(trx_id=200)。 - 事务A再次读取
age=20的记录:仍看到事务启动时的快照(trx_id≤100),不会看到事务B插入的记录(解决幻读)。
3. 事务日志(保证ACID的关键)
InnoDB通过redo log(重做日志)和undo log(回滚日志)保证事务的原子性、持久性和一致性。
(1)redo log(重做日志:保证持久性)
- 作用:记录数据修改的物理变化(如“将id=1的行的name从‘张三’改为‘李四’”),用于崩溃恢复(即使数据未刷盘,也能通过redo log恢复)。
- 特点:
- 循环写(固定大小,如4个文件,每个1G)。
- 先写日志(WAL,Write-Ahead Logging):修改数据时,先写redo log buffer,再定期刷新到磁盘(
innodb_flush_log_at_trx_commit控制刷新策略)。
- 崩溃恢复流程:重启MySQL时,InnoDB会扫描redo log,将未刷盘的修改重新应用到数据文件。
(2)undo log(回滚日志:保证原子性)
- 作用:记录数据修改的逻辑反向操作(如插入→删除,修改→恢复旧值),用于回滚未提交的事务(
ROLLBACK)或MVCC中的快照读。 - 特点:
- 逻辑日志(不记录物理地址,只记录操作的反向)。
- 存放在回滚段(Rollback Segment)中,事务提交后,undo log会被标记为可删除(但不会立即删除,因为MVCC可能需要旧版本)。
(3)binlog(二进制日志:辅助复制和恢复)
- 作用:记录所有修改数据的逻辑操作(如
INSERT INTO user VALUES(...)),用于主从复制(从库同步主库的binlog)和全量恢复(结合备份文件)。 - 与redo log的区别:
- redo log是InnoDB引擎特有的,binlog是MySQL服务器层的(所有引擎都支持)。
- redo log是循环写,binlog是追加写(不会覆盖)。
- 两阶段提交(2PC):为了保证redo log和binlog的一致性(避免“redo log提交但binlog未写”的情况),InnoDB采用两阶段提交:
- Prepare阶段:写redo log(标记为prepare状态)。
- Commit阶段:写binlog→写redo log(标记为commit状态)。
若在Prepare阶段崩溃,重启后会检查binlog:若binlog存在,则提交事务;否则回滚。
六、实践中的关键问题
1. 长事务的危害与解决
- 危害:
- 持有锁很久,阻塞其他事务(如
SELECT ... FOR UPDATE的长事务会占用排他锁)。 - 导致undo log膨胀(需要保存旧版本,回滚段满后会触发清理,影响性能)。
- 持有锁很久,阻塞其他事务(如
- 解决方法:
- 避免在事务中做长时间操作(如查询大量数据、等待用户输入)。
- 拆分大事务为小事务(如批量插入时,每1000条提交一次)。
- 设置事务超时时间(
innodb_lock_wait_timeout,默认50秒)。
2. 隔离级别的选择
- 读未提交:几乎不用(脏读问题严重)。
- 读已提交:适用于需要“实时数据”的场景(如电商订单查询),但会有不可重复读问题。
- 可重复读:MySQL默认级别,适用于大多数场景(如金融系统,需要数据一致性)。
- 串行化:适用于并发量极低、需要绝对一致的场景(如统计报表),但性能极差。
3. 锁冲突的优化
- 优化查询语句:用索引避免全表扫描(全表扫描会加表级锁),如
SELECT * FROM user WHERE id=1(用id索引,加行级锁)。 - 减少事务大小:缩短事务执行时间(如先查询再修改,避免在事务中查询大量数据)。
- 合理使用锁:避免不必要的
FOR UPDATE(如只读操作不需要加排他锁)。
七、总结
MySQL的事务核心是ACID,其中隔离性是并发控制的关键,InnoDB通过锁机制(解决修改冲突)和MVCC(解决读冲突)实现隔离级别;原子性和持久性通过undo log和redo log保证;一致性则依赖上述特性的共同作用。
理解事务的原理,能帮助我们在实践中选择合适的隔离级别、优化锁冲突、避免长事务,从而提升数据库的并发性能和数据一致性。
