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 实战经验总结

说了这么多,我总结几条实战经验:

  1. 隔离级别别乱调。「读未提交」基本没人用,「可串行化」基本没人敢用。大多数场景,「读已提交」是最优解。
  2. 索引是锁的基础。没有索引,行锁变表锁。我见过太多人因为忘了建索引,导致锁冲突爆炸。
  3. 间隙锁是好东西,但别滥用。它解决了幻读,但也带来了死锁风险。如果你的业务能接受幻读,降级隔离级别是个好选择。
  4. 短事务是王道。锁持有时间越短,并发越高。我习惯把大事务拆成多个小事务,每个事务只做一件事。

最后说一句:事务和锁机制,是数据库并发控制的基石。你不需要记住所有细节,但一定要理解它们的设计思想。遇到问题的时候,知道往哪个方向排查,就够了。