Skip to content

事务锁与隔离级别

1. 这是什么

事务用于保证一组操作的整体一致性,锁用于控制并发访问。
隔离级别则描述了事务之间相互可见的程度。

一句话理解:

  • 事务解决的是“这一组操作要不要算一个整体”
  • 锁和隔离级别解决的是“多个事务同时来时怎么不乱”

2. 为什么重要

很多并发写入问题、脏读、不可重复读、幻读,最终都和这组概念有关。
后端系统的数据一致性,离不开这些基础能力。

如果不理解事务和锁,你就很难解释这些真实问题:

  • 为什么同一条数据被并发修改后结果不对
  • 为什么读到的数据前后不一致
  • 为什么系统偶尔会死锁
  • 为什么事务开大了系统反而更慢

3. 先建立直觉:事务不是“开了就自动安全”

很多人会把事务理解成:

  • 加了 @Transactional 就安全了

这不够。
更准确的理解是:

  • 事务给你一致性边界
  • 锁和隔离级别决定并发时的可见性和冲突行为
  • 一致性和性能之间永远有权衡

4. 核心内容

4.1 ACID 怎么理解

事务最常见的四个特性是:

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

学习阶段更重要的是抓住业务直觉:

  • 原子性:要么都成功,要么都回滚
  • 一致性:前后都不能破坏业务约束
  • 隔离性:并发事务之间不要互相把数据看乱
  • 持久性:提交后结果应能保存下来

4.2 三类经典并发现象

脏读

一个事务读到了另一个事务尚未提交的数据。

不可重复读

同一事务里,两次读取同一条记录,结果不一致。

幻读

同一事务里,两次按条件查询,结果集条数变了,好像“凭空多了或少了记录”。

这三者看起来很像,但重点不同:

  • 不可重复读更偏“同一行数据内容变了”
  • 幻读更偏“符合条件的记录集合变了”

4.3 四种隔离级别

最常见的四种隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

可以先用这张表快速建立印象:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED避免可能可能
REPEATABLE READ避免避免依数据库实现和锁机制而定
SERIALIZABLE避免避免避免

注意:

  • 不同数据库实现细节会有差异
  • MySQL InnoDB 的实际行为,要结合 MVCC 和锁机制一起理解

4.4 行锁、表锁、间隙锁

行锁

  • 锁住某些具体记录
  • 并发度相对更高

表锁

  • 锁住整张表
  • 简单粗暴,但并发能力更弱

间隙锁

  • 锁住记录之间的区间
  • 主要用于防止某些并发插入带来的幻读问题

学习阶段最关键的是建立这种直觉:

  • 锁不只是“卡住别人”
  • 它是在用更少的并发自由度换更强的一致性保证

4.5 死锁为什么会发生

死锁通常发生在:

  • 两个或多个事务互相持有对方需要的锁

典型特征是:

  • 你等我
  • 我等你
  • 谁也过不去

数据库通常会选择:

  • 检测死锁
  • 回滚其中一个事务

所以线上看到死锁,并不意味着数据库坏了,而是:

  • 并发访问路径和锁顺序出了问题

5. 学习重点

这一章最重要的是掌握这些判断:

  • 一致性和并发性能之间存在权衡
  • 事务和锁总是要一起讨论
  • 隔离级别不是越高越一定更适合
  • 幻读比不可重复读更偏“结果集变化”
  • 死锁问题不能只靠重试,还要看访问顺序和索引设计

6. 常见问题

6.1 把事务范围开得过大

事务越大:

  • 锁持有时间越长
  • 并发冲突越容易放大

6.2 不理解隔离级别对性能的影响

隔离越强,通常限制越多,代价也越高。

6.3 出现死锁时只会重试,不会分析原因

重试是缓解手段,不是根治手段。
真正要看的是:

  • SQL 顺序
  • 访问路径
  • 索引命中
  • 事务范围

7. 动手验证

这一节适合在 MySQL 中开两个会话窗口操作。
当前环境没有 mysql 客户端,所以我没有在本机直接执行,但步骤已经按“双会话实验”整理好了。

7.1 准备测试表

sql
DROP TABLE IF EXISTS account_demo;

CREATE TABLE account_demo (
    id BIGINT PRIMARY KEY,
    name VARCHAR(32) NOT NULL,
    balance INT NOT NULL
);

INSERT INTO account_demo (id, name, balance) VALUES
(1, 'A', 100),
(2, 'B', 100);

7.2 先看当前隔离级别

sql
SELECT @@transaction_isolation;

7.3 模拟不可重复读

会话 A

sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM account_demo WHERE id = 1;

会话 B

sql
START TRANSACTION;
UPDATE account_demo SET balance = 200 WHERE id = 1;
COMMIT;

回到会话 A

sql
SELECT balance FROM account_demo WHERE id = 1;
COMMIT;

观察点:

  • 两次查询结果是否变化

7.4 模拟锁等待

会话 A

sql
START TRANSACTION;
UPDATE account_demo SET balance = balance - 10 WHERE id = 1;

不要提交,先停住。

会话 B

sql
START TRANSACTION;
UPDATE account_demo SET balance = balance + 10 WHERE id = 1;

观察点:

  • 会话 B 会等待,因为会话 A 已持有该行锁

7.5 模拟死锁

会话 A

sql
START TRANSACTION;
UPDATE account_demo SET balance = balance - 10 WHERE id = 1;

会话 B

sql
START TRANSACTION;
UPDATE account_demo SET balance = balance - 10 WHERE id = 2;

再回会话 A

sql
UPDATE account_demo SET balance = balance + 10 WHERE id = 2;

再回会话 B

sql
UPDATE account_demo SET balance = balance + 10 WHERE id = 1;

观察点:

  • 一方会报死锁错误
  • 数据库会自动回滚其中一个事务

7.6 你应该怎么验证结果

重点观察这些现象:

  • 同一事务中两次读取结果是否一致
  • 会话是否进入锁等待
  • 是否出现死锁回滚
  • 不同隔离级别下行为是否变化

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 模拟脏读、不可重复读、幻读场景
  • 观察不同隔离级别的行为差异
  • 复盘一次典型死锁产生过程
  • 总结“事务范围、索引命中、锁等待时间”之间的关系

9. 自测问题

  • 四种隔离级别分别解决了什么问题?
  • 幻读为什么比不可重复读更难处理?
  • 为什么事务和锁总是要一起讨论?
  • 为什么事务范围过大会放大并发问题?
  • 死锁出现后为什么不能只满足于“加重试”?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • ACID 是事务的基本语义框架
  • 脏读、不可重复读、幻读对应的是不同并发现象
  • 隔离级别越强,通常并发自由度越低
  • 行锁、表锁、间隙锁是在一致性和并发之间做权衡
  • 死锁排查要回到事务顺序、访问路径和锁粒度