事务是一组要么全部成功、要么全部撤销的数据库操作。假设小林正在把订单金额从 100 改成 120,但还没提交;小周此时只想查询订单。最简单的方案是让小周一直等锁,结果读多写少的系统也会产生大量排队。
只有一份数据
写入过程中要么让读者看见未完成结果,要么让读者等待。前者可能读到脏数据,后者牺牲并发。
保留历史版本
写者产生新版本,读者按自己的观察时点选择旧版本。读写能同时推进,但系统要维护历史记录并判断可见性。
MVCC(Multi-Version Concurrency Control,多版本并发控制)是让一行数据保留可追溯的历史版本,使普通读取通常不用阻塞写入的机制;事务隔离则规定并发事务彼此能看见哪些修改。InnoDB 是 MySQL 默认的事务存储引擎,本文围绕它解释两个事务为何可能看见不同结果。
事务是一组要么全部成功、要么全部撤销的数据库操作。假设小林正在把订单金额从 100 改成 120,但还没提交;小周此时只想查询订单。最简单的方案是让小周一直等锁,结果读多写少的系统也会产生大量排队。
写入过程中要么让读者看见未完成结果,要么让读者等待。前者可能读到脏数据,后者牺牲并发。
写者产生新版本,读者按自己的观察时点选择旧版本。读写能同时推进,但系统要维护历史记录并判断可见性。
隔离级别是数据库允许并发事务互相看见到什么程度的设置。它在一致性与并发能力之间做取舍。
| 现象 | 只改变的条件 | 事务 A 的观察 |
|---|---|---|
| 脏读 | B 修改 100→120,但未提交 | A 已读到 120;若 B 回滚,A 读过从未成立的数据 |
| 不可重复读 | A 两次按主键读;中间 B 更新并提交 | 第一次 100,第二次 120,同一行不同 |
| 幻读 | A 两次按范围统计;中间 B 插入并提交 | 第一次 3 行,第二次 4 行,像多出“幻影” |
InnoDB 是 MySQL 常用的事务存储引擎。它会为聚簇索引记录维护事务相关信息:DB_TRX_ID 记录最近修改该版本的事务编号,DB_ROLL_PTR 指向 undo log(撤销日志)中的上一版本信息。undo log 本来负责回滚,也能重建一致性读取需要的旧版本。
Read View(读视图)是一次一致性读取用来判断版本能否看见的快照规则。先把它理解成一张名单:创建视图时,哪些事务还活跃、下一个尚未分配的事务编号是多少、当前事务是谁。
下面使用简化但可推演的例子:事务 A 编号 25;创建视图时,其他活跃事务为 [28, 30],最小活跃编号 28,下一个尚未分配的编号是 31。A 查询版本链 trx 30 → 20 → 10。
| 隔离级别 | 普通读取 | 主要结果 | 代价 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能看见未提交修改 | 允许脏读 | 一致性最弱 |
| READ COMMITTED(读已提交) | 每次一致性读创建新快照 | 不脏读,但同一事务两次结果可变 | 可重复性降低 |
| REPEATABLE READ(可重复读) | 同一事务的普通一致性读复用首个快照 | 普通读取通常保持一致;InnoDB 默认 | 旧版本可能保留更久 |
| SERIALIZABLE(串行化) | 把并发行为约束得更像顺序执行 | 隔离最强 | 更多等待、更低并发 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT amount FROM orders WHERE id = 1; -- 第一次一致性读建立快照 SELECT amount FROM orders WHERE id = 1; -- 继续使用该快照 COMMIT;
SELECT ... FOR UPDATE、UPDATE 等当前读读取最新状态,并依靠索引范围锁处理插入冲突。快照读是普通 SELECT 按 Read View 选择历史版本;当前读是读取最新可用版本并对目标记录或范围加锁,例如 SELECT ... FOR UPDATE、UPDATE、DELETE。
| 语句 | 读哪个状态 | 读取是否加记录/范围锁 | 常见目的 |
|---|---|---|---|
SELECT ... | 快照允许的版本 | 通常不加 | 报表、展示 |
SELECT ... FOR SHARE | 当前状态 | 共享锁 | 确认数据后保持可用 |
SELECT ... FOR UPDATE | 当前状态 | 排他锁 | 读后准备修改 |
UPDATE / DELETE | 当前状态 | 排他锁 | 修改或删除 |
清理线程只有在旧版本不再可能被任何活跃视图读取时,才能回收相关 undo 信息。长事务持续持有旧快照,会拉长版本链,使读取为了找到可见版本多做追链,也可能增加 undo 表空间压力。
事务持续时间、undo 历史长度、长时间未提交连接、慢查询中版本重建和回表成本。
缩短事务;不要在事务中等待用户或远程调用;分批处理大任务;设置合理超时;应用能重试被回滚的事务。
-- 会话 A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT amount FROM orders WHERE id=1; -- 100 -- 会话 B 更新为 120 并 COMMIT 后,回到 A SELECT amount FROM orders WHERE id=1; -- 普通读仍可能为 100 SELECT amount FROM orders WHERE id=1 FOR UPDATE; -- 当前读看到 120 COMMIT;
不是。普通一致性读通常通过版本选择避免加记录锁;写操作和加锁读仍使用锁。MVCC 与锁共同构成 InnoDB 的并发控制。
REPEATABLE READ 下,同一事务的普通一致性读复用第一次一致性读建立的快照;READ COMMITTED 下,每次一致性读建立新快照。事务开始不等于一定立刻创建普通读快照。