事务锁与隔离级别
1. 这是什么
事务用于保证一组操作的整体一致性,锁用于控制并发访问。
隔离级别则描述了事务之间相互可见的程度。
一句话理解:
- 事务解决的是“这一组操作要不要算一个整体”
- 锁和隔离级别解决的是“多个事务同时来时怎么不乱”
2. 为什么重要
很多并发写入问题、脏读、不可重复读、幻读,最终都和这组概念有关。
后端系统的数据一致性,离不开这些基础能力。
如果不理解事务和锁,你就很难解释这些真实问题:
- 为什么同一条数据被并发修改后结果不对
- 为什么读到的数据前后不一致
- 为什么系统偶尔会死锁
- 为什么事务开大了系统反而更慢
3. 先建立直觉:事务不是“开了就自动安全”
很多人会把事务理解成:
- 加了
@Transactional就安全了
这不够。
更准确的理解是:
- 事务给你一致性边界
- 锁和隔离级别决定并发时的可见性和冲突行为
- 一致性和性能之间永远有权衡
4. 核心内容
4.1 ACID 怎么理解
事务最常见的四个特性是:
- 原子性
- 一致性
- 隔离性
- 持久性
学习阶段更重要的是抓住业务直觉:
- 原子性:要么都成功,要么都回滚
- 一致性:前后都不能破坏业务约束
- 隔离性:并发事务之间不要互相把数据看乱
- 持久性:提交后结果应能保存下来
4.2 三类经典并发现象
脏读
一个事务读到了另一个事务尚未提交的数据。
不可重复读
同一事务里,两次读取同一条记录,结果不一致。
幻读
同一事务里,两次按条件查询,结果集条数变了,好像“凭空多了或少了记录”。
这三者看起来很像,但重点不同:
- 不可重复读更偏“同一行数据内容变了”
- 幻读更偏“符合条件的记录集合变了”
4.3 四种隔离级别
最常见的四种隔离级别:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
可以先用这张表快速建立印象:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
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 是事务的基本语义框架
- 脏读、不可重复读、幻读对应的是不同并发现象
- 隔离级别越强,通常并发自由度越低
- 行锁、表锁、间隙锁是在一致性和并发之间做权衡
- 死锁排查要回到事务顺序、访问路径和锁粒度