4. 事务与锁机制:ACID特性、事务隔离级别、行锁与间隙锁实战
事务,说白了就是一组操作,要么全做,要么全不做。我刚开始接触数据库时,觉得这东西挺玄乎的。后来线上出了几次数据不一致的问题,才真正体会到——没有事务,数据库就是个玩具。
4.1 ACID特性:数据库的四大底线
ACID 是事务的四个基本特性。我习惯把它们理解为数据库的「底线」。任何一个靠谱的数据库,都得守住这四条。
| 特性 | 含义 | 我的一点理解 |
|---|---|---|
| 原子性(Atomicity) | 事务不可分割,要么全成功,要么全回滚 | 就像转账,扣钱和加钱必须一起发生 |
| 一致性(Consistency) | 事务前后,数据必须符合所有约束 | 账户余额不能为负,这个规则不能破 |
| 隔离性(Isolation) | 并发事务之间互不干扰 | 你改你的,我改我的,别互相影响 |
| 持久性(Durability) | 事务一旦提交,数据永久保存 | 哪怕数据库宕机,数据也不能丢 |
核心观点:ACID 里,一致性是最终目标,原子性、隔离性、持久性都是手段。我在项目中见过不少人只关注隔离性,却忽略了原子性——结果事务执行一半,系统崩了,数据就乱了。
4.2 事务隔离级别:从「读未提交」到「可串行化」
隔离级别解决的是并发事务带来的问题。你想想看,如果两个事务同时读写同一行数据,会发生什么?
常见的并发问题有三个:
- 脏读:读到别人还没提交的数据。万一人家回滚了,你就读了个寂寞。
- 不可重复读:同一个事务里,两次读同一行,结果不一样。因为中间被别的事务改了。
- 幻读:同一个事务里,两次查询同一个条件,结果集不一样。因为中间被别的事务插入了新行。
SQL 标准定义了四个隔离级别,一个比一个严格:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 | 最高 |
| 读已提交(READ COMMITTED) | 不会 | 可能 | 可能 | 高 |
| 可重复读(REPEATABLE READ) | 不会 | 不会 | 可能(InnoDB 解决了) | 中 |
| 可串行化(SERIALIZABLE) | 不会 | 不会 | 不会 | 最低 |
我的建议:大多数业务场景,用「读已提交」就够了。MySQL 默认是「可重复读」,这是历史原因。如果你用 Oracle 或 PostgreSQL,默认就是「读已提交」。我个人习惯,除非有特殊需求,否则不轻易上「可串行化」——性能代价太大。
4.3 行锁与间隙锁:InnoDB 的锁机制实战
嗯,这里要注意。MySQL 的 InnoDB 引擎,锁的机制其实挺巧妙的。它不光锁行,还会锁「间隙」。
4.3.1 行锁(Record Lock)
行锁就是锁住索引上的某一条记录。比如:
-- 事务A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 此时 id=1 这一行被锁住了
-- 事务B(会被阻塞)
UPDATE users SET name = '张三' WHERE id = 1;
行锁只锁一行,并发度最高。但有个前提——查询必须走索引。如果不走索引,InnoDB 会升级成表锁。我曾经在项目里踩过这个坑:一个 UPDATE 没走索引,结果锁了整张表,业务直接卡死。
4.3.2 间隙锁(Gap Lock)
间隙锁锁的是「两个索引值之间的空隙」。它存在的意义,就是为了解决幻读。
举个例子:
-- 假设 users 表里有 id: 1, 3, 5, 7
-- 事务A
BEGIN;
SELECT * FROM users WHERE id BETWEEN 3 AND 5 FOR UPDATE;
-- 此时锁住了 (3,5) 这个间隙,以及 id=3 和 id=5 这两行
-- 事务B(会被阻塞)
INSERT INTO users (id, name) VALUES (4, '新用户');
-- 因为 id=4 落在了 (3,5) 这个间隙里
避坑指南:我曾经在线上遇到过一个问题:一个批量插入的脚本,因为间隙锁导致大量死锁。原因是两个事务同时往同一个间隙里插数据,互相等待对方释放锁。解决方案很简单——把隔离级别降到「读已提交」,间隙锁就失效了。但前提是你能接受幻读。
4.3.3 临键锁(Next-Key Lock)
临键锁 = 行锁 + 间隙锁。它是 InnoDB 在「可重复读」级别下的默认锁机制。
说白了,就是既锁住记录本身,也锁住记录前面的间隙。这样既能防止别的事务修改这行,也能防止别的事务在它前面插入新行。
-- 假设表中有 id: 1, 3, 5
-- 临键锁会锁住这些区间:
-- (-∞, 1], (1, 3], (3, 5], (5, +∞)
-- 每个区间都是一个临键锁
4.4 实战经验总结
说了这么多,我总结几条实战经验:
- 隔离级别别乱调。「读未提交」基本没人用,「可串行化」基本没人敢用。大多数场景,「读已提交」是最优解。
- 索引是锁的基础。没有索引,行锁变表锁。我见过太多人因为忘了建索引,导致锁冲突爆炸。
- 间隙锁是好东西,但别滥用。它解决了幻读,但也带来了死锁风险。如果你的业务能接受幻读,降级隔离级别是个好选择。
- 短事务是王道。锁持有时间越短,并发越高。我习惯把大事务拆成多个小事务,每个事务只做一件事。
最后说一句:事务和锁机制,是数据库并发控制的基石。你不需要记住所有细节,但一定要理解它们的设计思想。遇到问题的时候,知道往哪个方向排查,就够了。