主题
事务与并发控制
最后一讲:多个用户同时读写同一张表,数据库怎么保证不出乱子。ACID、并发异常、隔离级别、锁与死锁——本课压轴考点,也是面试必问。
先记住一句话:事务(transaction)是数据库的最小工作单元,里面的操作要么全做、要么全不做——"全不做"和"没做"的区别,就是银行转钱时"扣款失败"和"扣了款没到账"的区别。
一、事务与 ACID
转账的标准模板(两条 UPDATE 必须同生共死):
sql
START TRANSACTION;
UPDATE account SET balance = balance - 500 WHERE id = 1; -- 张三减 500
UPDATE account SET balance = balance + 500 WHERE id = 2; -- 李四加 500
COMMIT; -- 两条都成功才提交
-- 任何一条失败则 ROLLBACK; 两条全部撤销| 特性 | 英文 | 靠什么实现 | 一句话 |
|---|---|---|---|
| 原子性 | Atomicity | undo log(回滚日志) | 要么全成,要么全不算 |
| 一致性 | Consistency | 其余三者共同保证 | 转账前后总额不变 |
| 隔离性 | Isolation | 锁 + MVCC | 并发事务互不干扰 |
| 持久性 | Durability | redo log(重做日志) | 提交了就丢不了,断电也不丢 |
🧠 记忆锚点:A 原子靠 undo,D 持久靠 redo,I 隔离靠锁和 MVCC,C 一致性是目的其余是手段。
二、并发带来的三类异常
两个事务同时跑,不加控制会出现三种经典异常。设张三账户有 1000 元:
| 异常 | 场景 | 通俗解释 |
|---|---|---|
| 脏读 | 张三改余额为 2000(未提交)→ 李四读到 2000 → 张三回滚 | 读到了别人还没提交、之后可能作废的数据 |
| 不可重复读 | 李四两次读同一行,中间张三把它改了并提交 | 同一事务内两次读,结果不一样(针对 UPDATE) |
| 幻读 | 李四按条件统计"余额>1000 的账户"两次,中间王五新开了一个大额账户 | 两次读,行数变了(针对 INSERT,像出现"幽灵行") |
区分口诀:脏读读的是未提交;不可重复读是"值变了";幻读是"行数变了"。
三、四种隔离级别
SQL 标准给出四个隔离级别,从松到严,隔离越严并发越差:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✗ 会发生 | ✗ | ✗ |
| READ COMMITTED(读已提交) | ✓ 防住 | ✗ | ✗ |
| REPEATABLE READ(可重复读) | ✓ | ✓ 防住 | InnoDB 基本防住* |
| SERIALIZABLE(串行化) | ✓ | ✓ | ✓ 完全防住 |
* 标准 SQL 里 RR 不防幻读,但 MySQL InnoDB 在 RR 级通过 MVCC + 间隙锁(next-key lock)基本防住了幻读——这是 MySQL 的著名加分点,也是"MySQL 默认级别"考点的完整答案:InnoDB 默认 REPEATABLE READ(Oracle/PostgreSQL 默认 READ COMMITTED)。
四、锁:共享锁与排他锁
InnoDB 两把基础锁:
- 共享锁 S:读锁。
SELECT ... LOCK IN SHARE MODE——大家都能同时读 - 排他锁 X:写锁。
UPDATE/DELETE/INSERT自动加——和任何锁都互斥
兼容矩阵一行记牢:只有 S 与 S 兼容,其余组合一律等待。锁的粒度:InnoDB 默认行锁(锁索引项;条件没走索引会退化成表锁——这就是"UPDATE 必须带索引条件"的又一理由);显式锁全表用 LOCK TABLES t WRITE。
MVCC(多版本并发控制)一句话:每行藏着多个历史版本,读操作按规则取"自己该看的那个版本",从而读不加锁、读写不冲突——这就是 InnoDB 在 RR 级既防不可重复读又不牺牲并发的原因。
五、死锁:两把锁交叉等
text
事务 A:锁住了行 1,想要行 2
事务 B:锁住了行 2,想要行 1
→ 互相等待,永远等不到 = 死锁InnoDB 有死锁检测,发现后回滚代价小的一方并报错 Deadlock found(另一事务正常继续)。死锁不是 bug,是并发固有风险,工程上的预防原则:
- 多个事务按相同顺序访问资源(比如都先锁小 id 再锁大 id)
- 事务尽量短,别在事务里 sleep、等用户输入
- 大事务拆小;给热点行预扣额度而非逐行改
六、动手:亲眼复现一次锁等待与死锁
💡 动手实验 1(锁等待):开两个 mysql 客户端窗口 A、B:
sql-- 窗口 A START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; -- 不提交!锁着行 1sql-- 窗口 B:查同一行会被卡住(锁等待),超时才报错 SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 转圈…… -- 回到窗口 A 执行 COMMIT; 窗口 B 立刻出结果💡 动手实验 2(死锁):A 锁行 1 想锁行 2,B 锁行 2 想锁行 1,其中一方会收到
ERROR 1213: Deadlock found。用SHOW ENGINE INNODB STATUS\G能看到死锁现场的两个事务和各自持有的锁。复现一次,比背十遍"交叉等待"记得牢。
七、安全收尾:SQL 注入一句话
本课的 SQL 都在数据库里执行,而真正的漏洞往往在应用层拼接 SQL 的地方——这就是姊妹站《数据库与 SQL》里讲的 SQL 注入。这里只给结论:用户输入永远用参数化查询(占位符)传入,永远不要字符串拼接 SQL。把这句话带回你的程序设计课,数据库和应用两边就都安全了。
小结与课程结语
- 事务四性:A 原子、C 一致、I 隔离、D 持久;undo 保原子、redo 保持久
- 三类异常:脏读(未提交)、不可重复读(值变)、幻读(行变)
- 隔离级别从松到严:RU → RC → RR → SERIALIZABLE;MySQL InnoDB 默认 RR
- 锁:只有 S-S 兼容;InnoDB 行锁走索引;MVCC 读写不冲突
- 死锁预防:固定顺序、事务短、大拆小
到这里,本课六讲完结:概念 → 关系模型 → SQL → 定义与索引 → 设计范式 → 事务并发。把课程总览的验收清单逐条打勾,这门课的期末和上机就都有了底气。