Skip to content

事务

一、事务的基本概念

事务(Transaction)是一组原子性的SQL操作,要么全部成功(提交,Commit),要么全部失败(回滚,Rollback)。例如“转账”操作(A扣钱→B加钱)必须作为一个事务,不能只执行其中一步。

二、事务的核心特性:ACID

SQL标准定义了事务的四大特性,是事务的“基石”:

特性定义实现方式
原子性(Atomicity)事务中的操作“要么全做,要么全不做”,不会中间中断。undo log(回滚日志):记录操作的反向逻辑,用于回滚未提交的事务。
一致性(Consistency)事务执行前后,数据库状态必须保持业务逻辑一致(如转账总金额不变)。依赖原子性、隔离性、持久性的共同保证,以及业务逻辑的正确性(如约束检查)。
持久性(Durability)事务提交后,修改的数据永久保存,即使系统崩溃也不会丢失。redo log(重做日志):先写日志再写数据,崩溃后通过日志恢复未刷盘的修改。
隔离性(Isolation)多个事务并发执行时,彼此之间不会互相干扰,每个事务看到的数据一致。锁机制(行级锁、表级锁)+ MVCC(多版本并发控制)。

三、事务的生命周期

MySQL中事务的生命周期由以下命令控制:

  • 开始事务START TRANSACTIONBEGIN(显式开启)。
  • 提交事务COMMIT(将修改持久化到数据库)。
  • 回滚事务ROLLBACK(撤销未提交的修改,恢复到事务开始前的状态)。

默认行为:MySQL默认开启autocommit=1(自动提交),即每个SQL语句都是一个独立的事务(执行后自动提交)。可通过SET autocommit=0关闭自动提交,此时需要手动COMMITROLLBACK

四、事务的隔离级别(并发控制的核心)

隔离性解决的是并发事务之间的干扰问题。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会:

  1. 将旧数据复制到undo log(形成一个旧版本)。
  2. 更新该行的trx_id为当前事务ID。
  3. 更新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_idm_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采用两阶段提交:
    1. Prepare阶段:写redo log(标记为prepare状态)。
    2. 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 logredo log保证;一致性则依赖上述特性的共同作用。

理解事务的原理,能帮助我们在实践中选择合适的隔离级别优化锁冲突避免长事务,从而提升数据库的并发性能和数据一致性。