事务

本文所说的 MySQL 事务都是指在 InnoDB 引擎下,MyISAM 引擎是不支持事务的。

事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID

原子性

事务内操作要么全部成功,要么全部回滚

核心靠的是 Undo Log(回滚日志)

  1. 事务修改数据前,先记录旧版本到 undo log

  2. 事务执行过程中如果报错,或你手动 ROLLBACK,就按 undo 把已做的修改全部撤销。

  3. 崩溃恢复时,未提交事务会被回滚,已提交事务不会被回滚。

隔离性

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED)

  2. 读提交 (READ COMMITTED)

  3. 可重复读 (REPEATABLE READ)

  4. 串行化 (SERIALIZABLE)

默认是可重复读 (REPEATABLE READ),很多时候会修改为读提交 (READ COMMITTED)

事务隔离级别要实际解决的问题

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读

可重复读

同一事务里,每次读同一行,结果不变,通常针对数据更新(UPDATE)操作

不可重复读

同一事务里,两次读同一行,结果变了,通常针对数据更新(UPDATE)操作

幻读

幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。

隔离级别

脏读

不可重复读

幻读

读未提交

可能

可能

可能

不可重复读

不可能

可能

可能

可重复读

不可能

不可能

可能

串行化

不可能

不可能

不可能

一般没人使用串行化

实现原理

先说读未提交可以理解为没有隔离

串行化就是读的时候加共享锁,写得时候加排他锁,也就是排队修改

可重复读&不可重复读原理

MVCC

MySQL 采用了 MVCC (多版本并发控制) 的方式,数据库不只保存“当前这一版数据”,还会保留“历史版本”,读请求根据自己的事务视角去读合适的版本,同一行数据有“时间线”,每个事务看到的是这条时间线中的某个快照

MVCC的优势是读不加锁也不会互相阻塞对方。

详解

每行数据都有隐藏列:

  • ROW_TRX_ID:最后一次修改这行的事务 ID

  • ROW_ROLL_PTR:指向 undo log 里的旧版本

  • ROW_ROW_ID:无主键时才会用到

每次更新都会把旧值写在undo log。一行数据会形成新版本->上一个版本->上一个版本。事务在读的时候会形成read view,read view决定哪个事务能看到哪个版本。

read view是一张可见性名单,是mysql内存里的一个,主要记录了:

  1. creator_trx_id:当前读事务自己的事务 ID

  2. m_ids:创建 Read View 时,还活跃(未提交)的事务 ID 列表

  3. m_up_limit_idm_ids 里最小的事务 ID(活跃事务最小值)

  4. m_low_limit_id:创建 Read View 时“下一个将分配的事务 ID”(可看作上界)

判断某条记录版本(row_trx_id)是否可见,规则是:

  1. row_trx_id == creator_trx_id
    当前事务自己改的,可见

  2. row_trx_id < m_up_limit_id
    说明这个版本在快照创建前就已提交,可见

  3. row_trx_id >= m_low_limit_id
    说明这是快照之后才开始的“未来事务”,不可见

  4. m_up_limit_id <= row_trx_id < m_low_limit_id
    这时再看它是否在 m_ids 里:

    • 在:创建快照时它还没提交,不可见

    • 不在:创建快照时已提交,可见

如果当前版本不可见,InnoDB 就沿着 undo log 的版本链找更老版本,直到找到可见版本。

RC和RR的核心区别

READ COMMITTED:每条 SELECT 都新建 Read View。,所以同一个事务中多次查询可能会出现不一样的快照读

REPEATABLE READ:事务里第一次快照读创建 Read View,后续复用同一个。同一个事务每次读的数据都是一样的

为什么RC多次创建还更快呢?

  1. 创建read view开销很小

  2. RC更容易读到较新版本,undo回溯链更小

  3. RC的锁冲突更少(写并发时还是会加锁的)

当前读VS快照读

  1. 快照读(Snapshot Read) 是什么

    • 读的是“历史一致性版本”(MVCC 版本),不一定是最新已提交值。

    • 典型语句:普通 SELECT(不带锁)。

    • 特点:一般不加行锁,读写冲突少,并发高。

  2. 当前读(Current Read) 是什么

    • 读的是“当前最新版本”,并且会加锁,防止并发修改。

    • 典型语句:SELECT ... FOR UPDATESELECT ... FOR SHAREUPDATEDELETE

    • 特点:会有锁等待/阻塞,但能保证后续修改的正确性。

  3. 核心区别

    • 读的数据版本:快照读读“可见旧版本”;当前读读“最新版本”。

    • 是否加锁:快照读通常不加锁;当前读会加锁。

    • 使用目的:快照读偏查询性能;当前读偏并发一致性控制。

持久性

事务一旦提交,那么就是永久性的,不会因为宕机等故障导致数据丢失

InnoDB 先改内存页(Buffer Pool),不会每次都立刻写数据文件,同时将对应的重做信息写到redo log,这就是所谓的:WAL(Write-Ahead Logging)。数据页慢慢刷盘没关系,只要日志是可靠的,崩溃后就能补回去

写日志文件是追加写非常快,而数据页shuapan随机写+16kb页,一次小更新也可能触发非常重的IO。

和binlog的关系

  • Redo Log 保证存储引擎层持久性(本机崩溃恢复)。

  • Binlog 保证复制/主从和点时间恢复(PITR)能力。

  • 生产上通常要求两者都可靠,避免“本地恢复了但复制不一致”。

两者相配合保证了持久性。

最常见的持久性参数

  • innodb_flush_log_at_trx_commit=1:每次提交都把 redo 刷到磁盘(最安全)。

  • sync_binlog=1:每次提交都把 binlog fsync(复制一致性更强)。

  • 想要“强持久 + 复制一致”,通常这两个都设为 1

一致性

以上三个特性合起来保证一致性