事务和锁是关系型数据库最核心的概念之一。理解它们不仅是面试的必修课, 更是写出正确、高效的数据操作代码的基础。本文将深入讲解 ACID、隔离级别、MVCC 和各类锁机制。
ACID 四大特性
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部回滚。通过 Undo Log 实现。
- 一致性(Consistency):事务前后数据满足所有约束条件。由其他三个特性共同保证。
- 隔离性(Isolation):并发事务之间相互隔离。通过锁和 MVCC 实现。
- 持久性(Durability):已提交事务的数据永久保存。通过 Redo Log 实现。
事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ (InnoDB 默认) | 不可能 | 不可能 | 部分解决 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 显式事务
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 或 ROLLBACK
MVCC:多版本并发控制
InnoDB 通过 MVCC 实现非锁定读(快照读),在 REPEATABLE READ 级别下避免不可重复读:
- 每行记录有
DB_TRX_ID(最近修改的事务 ID)和DB_ROLL_PTR(回滚指针) - 读取时通过 Read View 判断哪些版本可见
- READ COMMITTED 级别每次查询创建新的 Read View
- REPEATABLE READ 级别在事务开始时创建 Read View,整个事务期间不变
InnoDB 锁类型
行锁(Record Lock)
-- 对某一行加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 排他锁(X 锁)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁(S 锁)
间隙锁(Gap Lock)
锁定索引记录之间的间隙,防止幻读。只在 REPEATABLE READ 级别生效:
-- 表中 id 有 10, 20, 30 三条记录
-- 间隙锁锁定区间:(-∞, 10), (10, 20), (20, 30), (30, +∞)
SELECT * FROM users WHERE id BETWEEN 15 AND 25 FOR UPDATE;
-- 锁定 id=20 记录 + (15,20) 和 (20,25) 间隙
临键锁(Next-Key Lock)
行锁 + 间隙锁的组合,InnoDB 默认的行级锁算法:
-- 锁住记录及其前面的间隙
-- 对于 id=20,临键锁锁住 (10, 20] 区间
SELECT * FROM users WHERE id = 20 FOR UPDATE;
死锁
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁住 id=1
-- 此时事务 B 执行:
-- UPDATE accounts SET balance = balance - 100 WHERE id = 2; -- 锁住 id=2
-- 事务 A 继续
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待 B 释放锁
-- 事务 B 继续
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 等待 A 释放锁
-- 死锁!InnoDB 检测到后回滚其中一个事务
避免死锁的实践:按照相同的顺序访问资源。
如转账场景,始终先操作 id 较小的账户,再操作 id 较大的账户。
实战:处理高并发扣减库存
-- ❌ 错误的做法
-- 先查询再更新,存在竞态条件
-- SELECT stock FROM products WHERE id = 1; -- 读到 stock=10
-- UPDATE products SET stock = 9 WHERE id = 1;
-- ✅ 正确的做法:利用行锁原子更新
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock >= 1;
-- 检查 affected_rows,为 0 则库存不足
-- ✅ 或使用悲观锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- stock >= 1 则更新
UPDATE products SET stock = stock - 1 WHERE id = 1;
总结
理解事务和锁是后端开发的必备技能。核心要点:知道每种隔离级别能解决什么问题、 理解 MVCC 的 Read View 机制、区分各种锁类型和应用场景、 以及在实际业务中正确处理并发更新的问题。MVCC + Next-Key Lock 的组合 使 InnoDB 在性能和一致性之间取得了很好的平衡。
← 返回首页