事务与锁是最容易因为“背结论”而答错的部分。回答时要先区分快照读和当前读,再说明隔离级别、索引条件和锁范围。
ACID 分别由什么保证
- 原子性(Atomicity):undo log 等机制支持回滚;
- 一致性(Consistency):由事务机制、约束和业务规则共同保证;
- 隔离性(Isolation):MVCC 与锁共同实现;
- 持久性(Durability):redo log、刷盘策略和存储系统共同保证。
“一致性完全由数据库自动保证”是不准确的。余额不能为负、订单状态不可逆等业务约束仍需要应用和数据库共同表达。
四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(RU) | 可能 | 可能 | 可能 |
| 读已提交(RC) | 避免 | 可能 | 可能 |
| 可重复读(RR) | 避免 | 避免 | 快照读通常避免;当前读依赖锁 |
| 串行化(Serializable) | 避免 | 避免 | 避免 |
InnoDB 默认是可重复读(RR)。不能简单说“RR 彻底解决所有幻读”,因为普通 SELECT 的快照读与 SELECT ... FOR UPDATE 的当前读机制不同。
什么是 MVCC
多版本并发控制(MVCC)让读操作在很多情况下不用阻塞写操作。InnoDB 通过隐藏事务信息、undo log 版本链和 Read View 判断某个版本是否可见。
RC 和 RR 的 Read View 区别
- RC:通常每条一致性读语句创建新的 Read View;
- RR:通常事务第一次一致性读时创建 Read View,并在事务内复用。
因此 RC 下两次查询可能看到其他事务已经提交的新值,而 RR 下通常保持一致。
快照读和当前读
普通查询通常是快照读:
SELECT * FROM orders WHERE id = 1;
需要读取最新版本并加锁的是当前读:
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
SELECT * FROM orders WHERE id = 1 FOR SHARE;
UPDATE orders SET status = 'paid' WHERE id = 1;
DELETE FROM orders WHERE id = 1;
旧语法 LOCK IN SHARE MODE 仍可能见到,但 MySQL 8 更推荐 FOR SHARE。
行锁真的只锁一行吗
InnoDB 的行锁本质上锁索引记录。锁范围取决于:
- 使用了哪个索引;
- 条件是唯一等值、普通等值还是范围;
- 隔离级别;
- 记录是否存在;
- 优化器的执行计划。
如果更新条件没有可用索引,可能扫描并锁住大量记录,表现得像“锁表”。
记录锁、间隙锁和临键锁
- 记录锁(Record Lock):锁住索引记录;
- 间隙锁(Gap Lock):锁住索引记录之间的间隙,主要阻止插入;
- 临键锁(Next-Key Lock):记录锁与其前方间隙的组合。
在 RR 下,范围当前读可能使用临键锁避免其他事务插入范围内的新记录。
假设索引值为 10, 20, 30:
SELECT *
FROM orders
WHERE amount >= 20 AND amount < 30
FOR UPDATE;
具体锁范围不能只靠 SQL 文本猜测,应结合索引、执行计划和 performance_schema.data_locks 查看。
乐观锁和悲观锁
悲观锁直接依赖数据库锁:
START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
乐观锁通常使用版本号:
UPDATE products
SET stock = stock - 1,
version = version + 1
WHERE id = ?
AND stock > 0
AND version = ?;
应用必须检查受影响行数;更新 0 行意味着冲突或库存不足,不能当成成功。
死锁是怎么发生的
事务 A:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
事务 B 以相反顺序访问:
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
两者可能形成循环等待。InnoDB 会检测死锁并回滚代价较小的事务之一。
如何减少死锁
- 事务按固定顺序访问资源;
- 缩短事务,不在持锁期间调用远程接口;
- 为过滤条件建立合适索引;
- 一次锁定需要的记录;
- 对死锁错误做有限次数、带退避的重试;
- 避免把锁等待超时当作死锁处理。
如何查看锁和死锁
MySQL 8 推荐:
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
SHOW ENGINE INNODB STATUS\G
INFORMATION_SCHEMA.INNODB_LOCKS 和 INNODB_LOCK_WAITS 在较新的 MySQL 版本中已经被 Performance Schema 对应表替代。
redo log、undo log 和 binlog
- redo log:InnoDB 物理日志,主要用于崩溃恢复;
- undo log:保存旧版本,支持事务回滚和 MVCC;
- binlog:Server 层逻辑日志,用于复制和时间点恢复。
事务提交时需要协调 redo log 与 binlog,避免出现“数据库已提交但复制日志缺失”或相反状态。
面试速答
SELECT 会加锁吗
普通一致性读通常不加行锁;FOR UPDATE、FOR SHARE、串行化隔离级别以及部分特殊场景会加锁。
锁等待超时和死锁一样吗
不一样。死锁是循环等待,InnoDB 可以检测并主动回滚事务;锁等待超时是等待超过 innodb_lock_wait_timeout。
长事务有什么危害
会长期持锁、阻碍 undo 清理、扩大版本链、增加复制延迟与故障恢复成本。
下一篇将把这些机制用于慢 SQL、线上抖动和大表变更排查。