Theory · MySQL · InnoDB
一组 SQL 的原子契约 —— 每个字母背后都是一套机制:undo、redo、MVCC 与锁
原子性靠 undo log 回滚、持久性靠 redo log + 双"1"配置、隔离性靠 MVCC + 锁——一致性是前三者的结果
脏读 / 不可重复读 / 幻读:SQL 标准按"是否禁止异常"划隔离级别,与 MVCC 的机制口径互补
autocommit、START TRANSACTION、SAVEPOINT、隐式提交清单、长事务识别——面试与生产都在这里翻车
ACID · §17.2
事务内操作全做或全不做:失败即回滚到事务前状态。保证者:undo log——回滚时按反向逻辑逆操作(insert→delete、update→反向 update)。提交后想"撤销历史"也是它(闪回思想的根基)。
并发事务互不观察对方的中间状态。保证者:MVCC(快照读)+ 锁(当前读/写写)组合拳,强度由隔离级别选择。机制细节见 mvcc 与 lock-internals。
一旦 commit,断电也不丢。保证者:redo log(WAL)——先顺序写日志再落数据页;配合 innodb_flush_log_at_trx_commit 与 sync_binlog 双 1 配置、doublewrite buffer、binlog 两阶段提交。细节见 log-redo-undo-binlog。
事务把数据库从一个合法状态带到另一个合法状态:约束(主键/唯一/外键)、应用层业务规则不被破坏。它是 A、I、D 三者协同的结果而非独立机制——手册把 doublewrite/崩溃恢复也归入此处防护"内部处理损坏"。
Mapping Table
| 性质 | 机制 | 做什么 | 深讲在哪 |
|---|---|---|---|
| 原子性 | undo log(回滚段) | 记录逻辑逆操作;ROLLBACK 时逆放;insert undo 提交即弃、update undo 服务版本链 | mvcc · undo 版本链 / log-redo-undo-binlog |
| 持久性 | redo log(WAL)+ binlog 2PC | commit 先落 redo(prepare)→ 写 binlog → redo 提交;崩溃恢复按 redo 重放、按 undo 回滚未提交 | log-redo-undo-binlog |
| 隔离性(读) | MVCC:隐藏列 + undo 版本链 + ReadView | 快照读不加锁,沿版本链挑可见版本;RC/RR 只差视图建立时机 | mvcc 全 deck |
| 隔离性(写) | 行锁:Record / Gap / Next-Key + 意向锁 | 写写串行化;当前读防幻读靠 next-key;表级先走意向锁与 MDL | lock-internals 全 deck |
| 一致性 | 以上全部 + 约束/恢复 | 主键唯一外键约束、崩溃恢复(redo 前滚 + undo 回滚)、doublewrite 防页撕裂 | 本 deck 第 2 页 · log-redo-undo-binlog |
Concurrency Anomalies
| 异常 | 定义(标准口径) | 对比维度 | 一句话记法 |
|---|---|---|---|
| 脏读 dirty read | 读到其他事务未提交的数据;若对方回滚,你基于"从未存在"的数据做了决策 | 数据是否提交 | 读到了"不存在过"的值 |
| 不可重复读 non-repeatable read | 同一事务内,同一行两次读取值不同(他人 UPDATE/DELETE 已提交) | 同一行的值 | 一行读两次,值变了 |
| 幻读 phantom | 同一事务内,同一条件两次读取返回的行集合不同(他人 INSERT/DELETE 已提交) | 符合条件 的行集合 | 多出 / 少了一"批"行 |
mvcc 从实现角度回答"RR 下幻读解决了吗"(快照读复用 ReadView / 当前读靠 next-key lock)。本页是标准定义口径:异常是"现象分类",机制是"如何消除"——先现象后机制,答题层次立刻清晰。
SQL 标准里幻读的成因包含 INSERT 和 DELETE/UPDATE 改变行集合归属——"同一行两次读值不同"归不可重复读,"符合条件的行集合变化"归幻读,两者可能由同一 UPDATE 触发,按现象归类而不是按语句归类。
Walkthrough
Isolation Matrix
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | InnoDB 实现要点(机制深讲见 mvcc) |
|---|---|---|---|---|
| READ UNCOMMITTED | 允许 | 允许 | 允许 | 无视图保证,行为像 RC 但不一致——生产禁用 |
| READ COMMITTED | 禁止 | 允许 | 允许 | 每条快照读新建 ReadView;gap 锁禁用(外键/重复键检查除外);UPDATE 半一致读;只支持 row binlog |
| REPEATABLE READ(InnoDB 默认) | 禁止 | 禁止 | 标准允许 InnoDB 基本消除 | 首次快照读建 ReadView 全程复用;当前读用 next-key lock 挡插入——比标准 RR 更强 |
| SERIALIZABLE | 禁止 | 禁止 | 禁止 | autocommit=0 时普通 SELECT 隐式转 FOR SHARE,读也加锁 |
① 标准只要求 RR 禁止脏读+不可重复读,允许幻读;InnoDB 的 RR 通过 MVCC + next-key lock 把幻读"基本消除"(快照读不会、当前读被阻断、混用有一条缝)。② 标准没规定默认级别,各库自选:MySQL 选 RR,PostgreSQL 选 RC。
RC 下 gap 锁禁用、半一致读提前放行,语句的加锁范围与执行顺序不受控,statement 格式重放到从库可能得到不同结果——手册直接规定 READ COMMITTED "only row-based binary logging is supported"(MIXED 自动转 row)。
Why REPEATABLE READ
早年 MySQL 默认 binlog 是 statement 格式:从库重放 SQL 语句本身。这要求语句在主库执行时"锁住它读到的范围"直到提交,重放结果才确定——RR + next-key lock 恰好提供这个保证;RC 禁 gap 锁、加半一致读,语句结果不可复现(MySQL Bug #23051 即此问题的官方记录)。
8.0 默认 binlog_format=ROW,语句重放安全的约束已松绑——但默认级别保持 RR(兼容与语义惯性)。若切 RC,必须确认 binlog 是 row/MIXED;不少互联网公司仍选 RC 换更低锁冲突(见 QA 15)。
| 对比项 | SQL 标准 RR | InnoDB RR |
|---|---|---|
| 脏读 / 不可重复读 | 禁止 / 禁止 | 禁止 / 禁止(ReadView 复用) |
| 幻读 | 允许(标准只管"读已提交"语义的行级一致) | 快照读不会(视图复用);当前读被 next-key lock 阻断;混用两类读留一条缝(mvcc · 幻读页) |
| 实现自由度 | 标准只定义现象边界,不规定实现 | MVCC(快照读无锁)+ 2PL(当前读/写锁)组合,强于标准 |
Two-Phase Locking
| 概念 | 内容 |
|---|---|
| 2PL 协议 | 事务分两阶段:增长段只加锁不放锁,收缩段只放锁不加锁。满足 2PL 的调度可串行化——隔离性的经典理论基础 |
| S2PL(Strict 2PL) | 更严格:写锁(实践中读锁也)一直持有到事务结束才释放。现代数据库普遍采用——避免级联回滚(读不到别人未提交的写) |
| InnoDB 的事实行为 | DML 与锁定读的行锁在 commit / rollback 时才释放。手册 §17.7.2.4 原话:"All locks set by FOR SHARE and FOR UPDATE queries are released when the transaction is committed or rolled back."——正是 S2PL |
| 与 MVCC 的分工 | 快照读不取锁(走 ReadView,见 mvcc);当前读与写写冲突走 2PL(见 lock-internals)。"隔离性 = MVCC + 2PL" 是完整表述 |
锁窗口 = 事务时长。两阶段特性意味着事务中途无法提前放锁来"绕开"死锁——只能靠缩小事务、统一加锁顺序、合理索引来降低概率;真发生则靠 InnoDB 死锁检测回滚受害者(lock-internals · 死锁页)。
热点行的持锁时间直接决定并发度:事务里做 RPC、慢查询、等待人工输入,等于把行锁窗口拉长几十倍——这就是"事务要小"的协议层根据。隔离级别选 RC 也是在放宽锁窗口(gap 锁禁用、半一致读)。
Syntax 1/3 · §15.3.1
-- autocommit=1(默认):每条语句原子 -- 手册:as if it were surrounded by -- START TRANSACTION and COMMIT SET autocommit = 0; -- 之后须显式 COMMIT/ROLLBACK -- 推荐的显式事务(可带修饰符) START TRANSACTION [READ WRITE | READ ONLY] [WITH CONSISTENT SNAPSHOT]; -- 结束方式 COMMIT [AND [NO] CHAIN] [[NO] RELEASE]; ROLLBACK; -- 同样支持 CHAIN/RELEASE
| 要点 | 说明 |
|---|---|
| autocommit=1 | 每语句即事务;出错时该语句自动回滚,但已提交语句不回滚 |
| BEGIN / BEGIN WORK | START TRANSACTION 别名;存储程序内 BEGIN 被解析为 BEGIN…END 块,必须用 START TRANSACTION |
| READ ONLY | 手册:"MySQL enables extra optimizations for queries on InnoDB tables when the transaction is known to be read-only"——只读事务跳过 trx id 分配等开销(§10.5.3) |
| AND CHAIN / RELEASE | CHAIN:提交后立刻以相同特性开新事务;RELEASE:提交后断开会话 |
Syntax 2/3 · §15.3.4 / §15.3.3
SAVEPOINT sp1; -- ... ROLLBACK TO sp1; -- 回到 sp1,事务未结束 -- 外层 ROLLBACK 仍可整体回滚 RELEASE SAVEPOINT sp1;
语义:ROLLBACK TO 撤销 sp1 之后的行级变更、保留更早的;sp1 仍存在可再次 ROLLBACK TO;同名 SAVEPOINT 新建会覆盖旧的;COMMIT / 整体 ROLLBACK 清除全部 savepoint。典型用途:批量导入分批设点、失败从最近 checkpoint 续跑。
| 类别 | 代表语句 |
|---|---|
| DDL(执行前后各提交一次) | CREATE/ALTER/DROP TABLE、CREATE INDEX、RENAME/TRUNCATE TABLE、CREATE/ALTER/DROP VIEW、存储过程/触发器 |
| 账号与权限 | CREATE/DROP/ALTER USER、GRANT、REVOKE、SET PASSWORD |
| 事务控制(仅执行前提交) | BEGIN、START TRANSACTION、SET autocommit=1、LOCK TABLES |
| 管理与维护 | ANALYZE/CHECK/OPTIMIZE/REPAIR TABLE、FLUSH、LOAD INDEX… |
| 复制控制 | START/STOP REPLICA、CHANGE REPLICATION SOURCE TO |
例外:CREATE/DROP TEMPORARY TABLE 不触发隐式提交,但也不可回滚——手册明说"causes transactional atomicity to be violated"。
Syntax 3/3 · SET TRANSACTION
-- 语法:SET [GLOBAL | SESSION] TRANSACTION -- ISOLATION LEVEL {RU | RC | RR | SERIALIZABLE} -- [, READ ONLY | READ WRITE] SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 当前会话后续事务 SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 之后新建的会话 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 不带作用域 = 只影响"下一个事务"(one-shot) -- 8.0 亦可直接设系统变量(注意是连字符) SET GLOBAL transaction_isolation = 'read-committed';
| 作用域 | 影响范围 |
|---|---|
| GLOBAL | 之后新建的会话;已有会话不变 |
| SESSION | 当前会话后续所有事务 |
| 无关键字 | 仅下一个事务——连接池里要小心"一次性设置"漂移 |
SET TRANSACTION 用空格(READ COMMITTED);系统变量用连字符('read-committed')。变量 transaction_isolation 自 5.7.20 起取代 tx_isolation。改 GLOBAL 不影响已有连接——线上误配"以为生效"的经典来源。修改前提:若已设 read_only,切换隔离级别需要相应权限(§15.3.6)
Long Transactions
① 连接与锁窗口被拉长:行锁 / MDL 长时间持有 → 阻塞与死锁概率上升(lock-internals);② MVCC 侧钉住 update undo → History list length 上涨 → undo 膨胀、版本链变长(机制详见 mvcc · purge 与长事务);③ 复制延迟放大:大事务在从库单线程重放。
information_schema.INNODB_TRX:trx_started(起始时间)、trx_state、trx_rows_modified——查 trx_started < NOW() - INTERVAL 60 SECOND;SHOW ENGINE INNODB STATUS 的 History list length 看 purge 积压;performance_schema.events_transactions_current 补位。
| 业务侧治理 | 具体做法 |
|---|---|
| 事务里不做"慢东西" | 禁止 RPC/HTTP 调用、大计算、等人工确认;把事务收敛为"取数→内存决策→短事务落库" |
| 大批量写分批提交 | 10 万行 DELETE 拆成小批循环,避免超长 undo 与主从延迟;每批独立事务 + 间隔 |
| 应用层兜底 | 统一事务模板:begin 后 defer rollback,成功才 commit(防 panic 挂连接);DB 侧配合 innodb_lock_wait_timeout 与 kill 长事务脚本 |
| 监控告警 | 对 INNODB_TRX 时长、History list length、trx_rows_modified 设阈值告警——事后复盘靠 binlog,事前预警靠这三件 |
Interview QA · 1/2
A→undo log 逆操作回滚;D→redo log(WAL)+ redo/binlog 两阶段提交;I→MVCC(快照读)+ 锁(当前读);C→AID 协同 + 约束 + 业务语义。补一句"只有 C 是目的,AID 是手段"。
数据库侧的一致性 = 原子性 + 隔离性 + 持久性共同保证执行正确,再加主键/唯一/外键等约束与崩溃恢复;业务侧一致性(如转账两边同变)由应用逻辑配合事务完成,不是存储引擎单方面给的。
脏读:读到未提交且可能回滚的数据;不可重复读:同一行两次读值不同(UPDATE/DELETE);幻读:同条件两次读行集合不同(INSERT/DELETE 进入或退出条件)。对比维度:值 vs 集合基数。
快照读:解决——全程复用 ReadView,新插入行不可见;当前读:靠 next-key lock 阻断插入;缝在混用:看不见的行 UPDATE 改得到。完整推演见 mvcc · 幻读页,本 deck 提供标准定义口径。
历史:statement binlog 需要语句级可复现,RR+next-key lock 保证,RC 禁 gap 锁不可保证(Bug #23051)。现状:8.0 默认 row binlog,约束解除但默认保留;RR 实现强于标准 RR。PostgreSQL 默认 RC 可作横向对比。
2PL:事务分加锁增长段与放锁收缩段,可串行化的理论基础;S2PL 要求写锁持有到事务结束。InnoDB:FOR SHARE/FOR UPDATE 与 DML 的行锁都在 commit/rollback 才释放(§17.7.2.4 原话)。后果:锁窗口=事务时长,死锁无法靠中途放锁规避。
能跑但代价大:DDL 执行前后各做一次隐式提交(§15.3.3),之前的事务被静默提交;同时 DDL 要 MDL 排他锁,被长事务阻塞会引发整表排队。临时表 DDL 是例外:不隐式提交但也不可回滚。
默认 RR 的视图在第一次快照读才建立;该修饰符把建立时点提前到事务开始(手册:等价于 START TRANSACTION 后立刻 SELECT 一次)。其他隔离级别下被忽略并给 warning。
Interview QA · 2/2
部分回滚:撤销 savepoint 之后的行变更,事务仍活跃、可继续提交或整体回滚;该 savepoint 仍存在可重复使用,更早的 savepoint 不受影响。注意行锁不因 ROLLBACK TO 提前释放——仍在 2PL 框架内到 commit 才放。
DDL(CREATE/ALTER/DROP/RENAME/TRUNCATE TABLE、CREATE INDEX…)前后各一次;CREATE/ALTER/DROP USER、GRANT/REVOKE;BEGIN/START TRANSACTION/SET autocommit=1/LOCK TABLES 仅执行前提交;ANALYZE/OPTIMIZE/FLUSH 等管理语句。临时表 DDL 例外。
发现:information_schema.INNODB_TRX 的 trx_started / trx_rows_modified;SHOW ENGINE INNODB STATUS 的 History list length。危害:锁与 MDL 窗口拉长、update undo 无法 purge 致 undo 膨胀、从库重放大事务延迟。治理:慢操作移出事务、分批提交。
两层:语义上写操作被拒绝(TEMPORARY 表 DML 除外);性能上手册明说 InnoDB 对已知只读事务启用额外优化——只读事务可不分配 trx id、不计入 purge 判定(§10.5.3)。报表/对账场景值得显式声明。
GLOBAL 只影响之后新建会话(已有连接不变);SESSION 影响当前会话后续事务;不带作用域只影响下一个事务。坑:语句值用空格、系统变量 transaction_isolation 用连字符;改 GLOBAL 后长连接不生效。
统一封装:Begin 后 defer Rollback(提交后置标志防误回滚),成功路径才 Commit;事务内禁止 RPC/慢调用;用 context 超时联动避免连接被挂死;批量写分批提交。本质是把 2PL 的锁窗口压到最小。
RC:gap 锁禁用 + 半一致读 → 锁冲突与死锁更少,互联网公司常用,但必须 row binlog 且应用要接受"读最新已提交";RR:跨语句一致读(报表/对账)与可重复语义。前提检查:binlog_format 与团队对隔离语义的依赖。
写路径:改 buffer pool 页 + 记 undo(可回滚)+ 记 redo(prepare)→ 写 binlog → redo commit;回滚:按 undo 逆操作恢复行与二级索引(delete-mark 记录等 purge 清理)。追问出口:undo/redo 细节见 log-redo-undo-binlog,可见性与 purge 见 mvcc。
Related Decks
mvcc.html · 隔离性的读侧机制:ReadView / 版本链 / 幻读 / purge
lock-internals.html · 隔离性的写侧机制:锁层级 / MDL / 死锁
log-redo-undo-binlog.html · 原子性与持久性的载体:undo / redo / binlog 2PC
replication.html · RR 默认与 binlog 格式的历史因果在复制的落地
index-btree.html · 加锁与回表都发生在索引结构上
ACID → undo / redo+2PC / MVCC+锁 / 约束
三异常 → 脏读(未提交) · 值(一行) · 集合(一批行)
级别 → RU·RC·RR·SERA 矩阵 + InnoDB 超纲点
2PL → 锁窗口 = 事务时长 → 小事务的一切理由
语法 → autocommit · CONSISTENT SNAPSHOT · 隐式提交清单
References
本 deck 全部结论可溯源至下列一手材料
| dev.mysql.com/doc/refman/8.0/en/mysql-acid.html | §17.2 InnoDB and the ACID Model:各字母的 related features 清单(autocommit/COMMIT/ROLLBACK、isolation level、doublewrite、innodb_flush_log_at_trx_commit) |
| dev.mysql.com/doc/refman/8.0/en/commit.html | §15.3.1:autocommit 语义、START TRANSACTION 修饰符、WITH CONSISTENT SNAPSHOT 等价语义与非 RR 忽略、READ ONLY 优化、CHAIN/RELEASE |
| dev.mysql.com/doc/refman/8.0/en/savepoint.html | §15.3.4:SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT 语义 |
| dev.mysql.com/doc/refman/8.0/en/implicit-commit.html | §15.3.3:DDL 前后隐式提交、账号权限语句、事务控制语句仅前提交、临时表例外(atomicity violated) |
| dev.mysql.com/doc/refman/8.0/en/set-transaction.html | §15.3.6:GLOBAL/SESSION/one-shot 作用域与 transaction_characteristics 语法 |
| dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html | §17.7.2.1:四级别行为、InnoDB 默认 RR、RC 半一致读与仅 row binlog、SERIALIZABLE 隐式 FOR SHARE |
| dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html | §17.7.2.4:"All locks set by FOR SHARE and FOR UPDATE queries are released when the transaction is committed or rolled back"(S2PL 行为依据)· NOWAIT/SKIP LOCKED |
| dev.mysql.com/doc/refman/8.0/en/information-schema-innodb-trx-table.html | INNODB_TRX 列(trx_started / trx_state / trx_rows_modified)——长事务识别 |
| percona.com/blog/mysql-performance-implications-of-innodb-isolation-modes/ · bugs.mysql.com/bug.php?id=23051 | 默认 RR 与 statement 复制安全的历史因果;RC 破坏 SBR 的官方记录 |