事务隔离级别决定了事务之间的可见性,是并发控制的核心机制
事务的 ACID 特性
| 特性 | 说明 | 实现机制 |
|---|---|---|
| 原子性(Atomicity) | 事务要么全部成功,要么全部失败 | undo log |
| 一致性(Consistency) | 事务前后数据保持一致状态 | 由 A、I、D 共同保证 |
| 隔离性(Isolation) | 并发事务之间互不干扰 | 锁 + MVCC |
| 持久性(Durability) | 事务提交后永久保存 | redo log |
并发事务问题
1. 脏读
定义:读到了其他事务未提交的数据。
sql
-- 时间线演示
时间 事务A 事务B
---- ---------------- ----------------
T1 BEGIN;
T2 BEGIN;
T3 UPDATE user SET age=20
WHERE id=1; -- age 从 18 改为 20
T4 SELECT age FROM user
WHERE id=1; -- 读到 20(脏读!)
T5 ROLLBACK; -- 回滚,age 恢复为 18
T6 -- 事务B 拿到的 20 是无效数据
危害:业务逻辑基于错误数据执行,导致数据不一致。
2. 不可重复读
定义 :同一事务内,两次读取同一数据结果不同(针对修改/删除)。
sql
时间 事务A 事务B
---- ---------------- ----------------
T1 BEGIN;
T2 SELECT age FROM user
WHERE id=1; -- 读到 age=18
T3 BEGIN;
T4 UPDATE user SET age=20
WHERE id=1;
T5 COMMIT;
T6 SELECT age FROM user
WHERE id=1; -- 读到 age=20(不可重复读!)
T7 -- 同一事务两次查询结果不一致
3. 幻读
定义 :同一事务内,两次读取的记录数不同(针对插入)。
sql
时间 事务A 事务B
---- ---------------- ----------------
T1 BEGIN;
T2 SELECT * FROM user
WHERE age > 18; -- 查到 2 条记录
T3 BEGIN;
T4 INSERT INTO user(age)
VALUES(25); -- 插入新记录
T5 COMMIT;
T6 SELECT * FROM user
WHERE age > 18; -- 查到 3 条记录(幻读!)
T7 -- 多出来一条"幻影"记录
不可重复读 vs 幻读:
- 不可重复读:侧重于数据内容变化(修改、删除)
- 幻读:侧重于数据条数变化(插入)
四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| 读未提交(RU) | ✗ | ✗ | ✗ | 最高 |
| 读已提交(RC) | ✓ | ✗ | ✗ | 高 |
| 可重复读(RR) | ✓ | ✓ | ✗* | 中 |
| 串行化 | ✓ | ✓ | ✓ | 最低 |
✗ 表示可能发生,✓ 表示已解决
MySQL InnoDB 默认使用 RR 级别,并通过 MVCC + 间隙锁解决了幻读问题
各级别详解
1. 读未提交(Read Uncommitted)
事务A 事务B
│ │
│ 修改 id=1 的值 │
│ (未提交) │
│ │
│ 读取 id=1 ✓ 能读到未提交的数据!
│ │
│ 回滚 │
▼ ▼
问题:事务B 读到了"脏数据"
特点:
- 几乎不加锁,性能最高
- 数据一致性最差
- 生产环境几乎不用
2. 读已提交(Read Committed)
事务A 事务B
│ │
│ 修改 id=1 的值 │
│ (未提交) │
│ │
│ 读取 id=1 → 读到旧值 ✓
│ │
│ 提交 │
│ │
│ 读取 id=1 → 读到新值 ✗ 两次读取不一致!
▼ ▼
问题:解决了脏读,但存在不可重复读
特点:
- Oracle、PostgreSQL 默认级别
- 每次查询生成新的 Read View
- 适合需要看到最新数据的场景
3. 可重复读(Repeatable Read)
事务A 事务B
│ │
│ 读取 id=1 → 值=100
│ │
│ 修改 id=1 为 200 │
│ 提交 │
│ │
│ 读取 id=1 → 值=100 ✓ 仍然读到旧值
▼ ▼
原理:MVCC 读取快照版本
特点:
- MySQL InnoDB 默认级别
- 事务开始时生成 Read View,整个事务期间复用
- 通过间隙锁解决幻读问题
4. 串行化(Serializable)
事务A 事务B
│ │
│ 查询 id=1 (加共享锁) │
│ │
│ 修改 id=1 → 阻塞等待
│ │
│ 提交 │
│ │
│ 获得锁,执行修改
▼ ▼
原理:读写都加锁,完全串行执行
特点:
- 最高隔离级别,完全解决并发问题
- 性能最差,并发度最低
- 适用于对一致性要求极高的场景
MVCC 实现原理
核心概念
MVCC(Multi-Version Concurrency Control,多版本并发控制)通过保存数据的历史版本,实现非阻塞读。
隐藏字段
每行数据自动添加三个隐藏字段:
| 字段 | 说明 |
|---|---|
DB_TRX_ID |
最后修改该行的事务ID(6字节) |
DB_ROLL_PTR |
指向 undo log 的指针(7字节) |
DB_ROW_ID |
行ID,用于索引(6字节,可选) |
版本链
┌─────────────────────────────────────────────────────┐
│ 当前数据 (trx_id=101) │
│ id=1, name="李四" │
│ roll_pointer ──────────────────────────┐ │
└──────────────────────────────────────────│───────────┘
▼
┌─────────────────────────────────┐
│ undo log 版本1 (trx_id=100) │
│ name="张三" │
│ roll_pointer ─────┐ │
└────────────────────│────────────┘
▼
┌─────────────────────────────────┐
│ undo log 版本2 (trx_id=50) │
│ name="王五" │
│ roll_pointer = null │
└─────────────────────────────────┘
Read View 结构
java
class ReadView {
// 当前活跃事务ID列表
List<Long> m_ids; // [100, 101, 102]
// 最小活跃事务ID
long min_trx_id; // 100
// 下一个要分配的事务ID
long max_trx_id; // 103
// 当前事务ID
long creator_trx_id; // 99
}
可见性判断
遍历版本链,判断每个版本是否可见:
1. trx_id == creator_trx_id
→ 自己修改的,可见 ✓
2. trx_id < min_trx_id
→ 版本在当前事务之前已提交,可见 ✓
3. trx_id >= max_trx_id
→ 版本是将来事务产生的,不可见 ✗
4. trx_id 在 m_ids 中
→ 版本是未提交事务产生的,不可见 ✗
5. trx_id 不在 m_ids 中
→ 版本已提交,可见 ✓
RC 和 RR 的区别
| 级别 | Read View 生成时机 | 效果 |
|---|---|---|
| RC | 每次查询都生成新的 | 能看到最新提交的数据 |
| RR | 事务开始时生成一次 | 整个事务看到相同的数据快照 |
RC 级别:
查询1 → 生成 ReadView1 → 看到版本A
其他事务提交新版本
查询2 → 生成 ReadView2 → 看到版本B(新版本)
RR 级别:
查询1 → 生成 ReadView → 看到版本A
其他事务提交新版本
查询2 → 复用 ReadView → 仍然看到版本A
快照读与当前读
快照读
读取数据的历史版本,不加锁。
sql
-- 普通 SELECT 就是快照读
SELECT * FROM user WHERE id = 1;
当前读
读取数据的最新版本,加锁。
sql
-- 共享锁(S锁)
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM user WHERE id = 1 FOR SHARE; -- MySQL 8.0+
-- 排他锁(X锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 以下操作也是当前读
UPDATE user SET name = 'test' WHERE id = 1;
DELETE FROM user WHERE id = 1;
INSERT INTO user VALUES (1, 'test');
对比
| 类型 | 读取版本 | 是否加锁 | 适用场景 |
|---|---|---|---|
| 快照读 | 历史版本 | 否 | 普通查询 |
| 当前读 | 最新版本 | 是 | 更新、删除、加锁查询 |
间隙锁详解
什么是间隙锁
间隙锁(Gap Lock)锁定一个范围,防止其他事务在这个范围内插入数据。
假设表中有数据:id = 1, 5, 10
间隙锁锁定的范围:
(-∞, 1] (1, 5] (5, 10] (10, +∞)
↑ ↑ ↑ ↑
间隙1 间隙2 间隙3 间隙4
解决幻读
sql
-- 事务A
BEGIN;
SELECT * FROM user WHERE id > 5 FOR UPDATE;
-- 锁住 (5, +∞) 范围
-- 事务B
INSERT INTO user VALUES (8, 'test'); -- 阻塞!无法插入
INSERT INTO user VALUES (3, 'test'); -- 成功,不在锁定范围
间隙锁类型
| 类型 | 说明 |
|---|---|
| Gap Lock | 只锁间隙,不锁记录 |
| Record Lock | 只锁记录,不锁间隙 |
| Next-Key Lock | Record Lock + Gap Lock,锁记录+前间隙 |
Next-Key Lock 示例:
数据:id = 1, 5, 10
SELECT * FROM user WHERE id = 5 FOR UPDATE;
锁定的范围:(1, 5] -- Next-Key Lock
↑ ↑
间隙 记录
设置隔离级别
查看隔离级别
sql
-- MySQL 5.7+
SELECT @@transaction_isolation;
-- MySQL 5.6 及之前
SELECT @@tx_isolation;
-- 查看全局和会话级别
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
设置隔离级别
sql
-- 设置会话级别(仅当前连接有效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局级别(新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置下一个事务的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
配置文件设置
ini
[mysqld]
transaction-isolation = REPEATABLE-READ
隔离级别选择
各级别适用场景
| 隔离级别 | 适用场景 | 典型应用 |
|---|---|---|
| RU | 几乎不用 | 日志分析等非关键业务 |
| RC | 需要实时数据、高并发 | 电商商品展示、社交媒体 |
| RR | 默认选择,通用场景 | 大多数业务系统 |
| Serializable | 强一致性要求 | 金融交易、库存扣减 |
性能对比
并发性能:RU > RC > RR > Serializable
原因:
- RU:几乎不加锁
- RC:MVCC,每次查询生成 Read View
- RR:MVCC + 间隙锁
- Serializable:完全加锁串行
常见问题
Q1: 为什么 MySQL 默认用 RR 而不是 RC?
- 数据一致性更好:RR 解决了不可重复读问题
- 幻读问题已解决:InnoDB 通过 MVCC + 间隙锁解决了幻读
- 主从复制友好:基于语句的复制在 RR 下更安全
- 历史原因:MySQL 早期版本就默认 RR
Q2: RR 如何解决幻读?
sql
-- 快照读:通过 MVCC 解决
SELECT * FROM user WHERE id > 5;
-- 读取事务开始时的快照,看不到新插入的数据
-- 当前读:通过间隙锁解决
SELECT * FROM user WHERE id > 5 FOR UPDATE;
-- 锁住 (5, +∞) 范围,其他事务无法插入
Q3: 什么时候用 RC?
- 需要看到最新数据:如商品库存实时展示
- 高并发场景:减少间隙锁带来的锁等待
- 业务容忍不可重复读:两次查询结果不一致不影响业务
Q4: RC 和 RR 的锁区别?
| 方面 | RC | RR |
|---|---|---|
| 间隙锁 | 无 | 有 |
| Next-Key Lock | 无 | 有 |
| 锁范围 | 只锁匹配行 | 锁行 + 间隙 |
| 并发度 | 更高 | 较低 |
总结
隔离级别选择原则
在满足业务需求的前提下,选择并发性能最高的隔离级别
快速选择指南
| 场景 | 推荐级别 | 原因 |
|---|---|---|
| 默认选择 | RR | 平衡一致性和性能 |
| 高并发读、低一致性要求 | RC | 减少锁竞争 |
| 金融交易、强一致性 | Serializable | 最高隔离级别 |
| 几乎不用 | RU | 数据一致性太差 |
核心要点
- 理解三种问题:脏读、不可重复读、幻读
- 掌握 MVCC:版本链、Read View、可见性判断
- 区分两种读:快照读(MVCC)、当前读(加锁)
- 合理选择级别:根据业务需求权衡一致性和性能