在数据库并发事务处理场景中,若缺乏有效的隔离机制,会引发脏读 、不可重复读 、幻读 三类核心数据一致性问题,部分数据库还会伴随丢失更新的衍生问题。这些问题的根源是多个事务对同一批数据的交叉操作,且操作时序未被合理管控。
下面对每类问题进行超详细拆解,包括定义、核心成因、典型场景、危害及解决思路:
一、 脏读(Dirty Read)
1. 核心定义
一个事务读取到了另一个事务 尚未提交 的数据修改,这部分未提交的、可能被回滚的数据被称为 脏数据。
2. 核心成因
低隔离级别下,数据库允许事务读取其他事务未持久化的内存修改数据,且不做任何锁或版本控制限制。
3. 典型场景(银行转账)
| 时间顺序 | 事务 T1(转账扣钱) | 事务 T2(查询余额) |
|---|---|---|
| T1 | 读取账户 A 余额 = 1000 元 | - |
| T2 | - | 开始执行 |
| T3 | 执行 UPDATE A SET balance = 500 WHERE id=1(未提交) |
- |
| T4 | - | 读取账户 A 余额 = 500 元 |
| T5 | 发现转账对象错误,执行 ROLLBACK(余额回滚为 1000 元) | - |
| T6 | - | 基于 500 元的余额,执行了消费 300 元的操作 |
4. 危害
事务 T2 基于无效的脏数据做业务决策,会导致业务逻辑错乱,比如上述场景中,账户 A 实际余额 1000 元,但 T2 误以为只有 500 元,后续操作会出现数据偏差。
5. 解决思路
提升事务隔离级别至 读已提交(Read Committed) 及以上,数据库会保证事务只能读取其他事务已提交的数据。
二、 不可重复读(Non-repeatable Read)
1. 核心定义
同一个事务内,多次读取同一行数据,得到的结果不一致。因为在两次读取之间,其他事务提交了对该数据的更新操作。
2. 核心成因
隔离级别为读已提交时,虽然解决了脏读,但允许同一事务内的多次读取,感知到其他事务提交的更新,破坏了事务内数据的一致性。
3. 关键区别(与脏读对比)
- 脏读读取的是 未提交 的数据;
- 不可重复读读取的是 已提交 的数据。不可重复读的问题不在于数据 "脏",而在于同一事务内数据读取结果不稳定。
4. 典型场景(财务对账)
| 时间顺序 | 事务 T1(对账,多次读同一数据) | 事务 T2(修改余额) |
|---|---|---|
| T1 | 开始执行,第一次读取账户 A 余额 = 1000 元 | - |
| T2 | - | 开始执行 |
| T3 | - | 执行 UPDATE A SET balance = 1500 WHERE id=1 |
| T4 | - | 提交事务 |
| T5 | 第二次读取账户 A 余额 = 1500 元 | - |
| T6 | T1 内两次读取结果不一致,对账逻辑混乱 | - |
5. 危害
依赖同一事务内多次读取数据一致性的业务(如对账、统计、计算)会出现结果错误,无法保证业务逻辑的正确性。
6. 解决思路
- 提升隔离级别至 可重复读(Repeatable Read) :数据库通过 MVCC(多版本并发控制) 或 行锁,保证同一事务内多次读取同一数据时,只能看到事务启动时的版本。
- 手动加锁:对读取的数据加 共享锁,阻止其他事务修改,直到当前事务结束。
三、 幻读(Phantom Read)
1. 核心定义
同一个事务内 ,多次执行 相同的范围查询 ,得到的结果集条数不一致。因为在两次查询之间,其他事务提交了对该范围数据的 插入或删除 操作,就像出现了 "幻觉"。
2. 核心成因
可重复读隔离级别能解决单行数据的不可重复读,但无法管控范围查询下的新增 / 删除操作,因为新增数据的行锁在事务启动时不存在,无法被拦截。
3. 关键区别(与不可重复读对比)
| 对比维度 | 不可重复读 | 幻读 |
|---|---|---|
| 操作类型 | 针对 单行数据的 UPDATE | 针对 范围数据的 INSERT/DELETE |
| 结果变化 | 数据内容改变 | 结果集数量改变 |
| 解决级别 | 可重复读即可解决 | 需串行化级别才能彻底解决 |
4. 典型场景(库存盘点)
| 时间顺序 | 事务 T1(盘点库存 > 0 的商品) | 事务 T2(新增商品) |
|---|---|---|
| T1 | 开始执行,查询 SELECT * FROM goods WHERE stock > 0,得到 10 条数据 |
- |
| T2 | - | 开始执行 |
| T3 | - | 插入一条 stock = 50 的新商品,提交事务 |
| T4 | T1 再次执行相同查询,得到 11 条数据 | - |
| T5 | T1 基于第一次的 10 条数据生成盘点报告,与实际 11 条不符 | - |
5. 特殊情况:"写幻读"
更危险的幻读场景是 写操作的幻觉:事务 T1 先查询范围数据,再基于查询结果执行更新,但其他事务插入了新数据,导致 T1 的更新操作意外修改了新增的数据。
sql
-- 事务 T1
BEGIN;
-- 步骤1:查询所有库存>0的商品,此时有10条
SELECT * FROM goods WHERE stock > 0;
-- 步骤2:更新这些商品的价格(此时T2插入了1条新商品)
UPDATE goods SET price = price * 1.1 WHERE stock > 0;
COMMIT;
-- 结果:T1 本意是更新10条商品,实际更新了11条,包含了T2新增的商品
6. 解决思路
- 串行化(Serializable)隔离级别 :数据库对范围查询加 表锁 或 间隙锁,阻止其他事务在该范围插入 / 删除数据,事务串行执行,彻底解决幻读,但性能损耗极大。
- 间隙锁 + 行锁(Next-Key Lock):MySQL InnoDB 存储引擎在可重复读级别下,会自动为范围查询加间隙锁,阻止其他事务在间隙中插入数据,可缓解大部分幻读场景,但无法完全避免。
四、 衍生问题:丢失更新(Lost Update)
1. 核心定义
两个事务同时修改同一行数据,后提交的事务会覆盖先提交事务的修改,导致先提交的修改丢失。
2. 分类
- 第一类丢失更新:事务 T1 修改数据后未提交,事务 T2 也修改同一数据并提交,随后 T1 回滚,导致 T2 的修改被回滚丢失。
- 第二类丢失更新:事务 T1 和 T2 都读取同一数据,T1 先修改提交,T2 后修改提交,T2 的修改覆盖了 T1 的修改。
3. 解决思路
- 加 排他锁:修改数据时,阻止其他事务读取和修改该数据;
- 使用 乐观锁:基于版本号或时间戳,在提交时校验数据是否被修改过,若被修改则放弃更新。
五、 各类并发问题与隔离级别的对应关系
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 丢失更新 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | ✅ 会出现 | ✅ 会出现 | ✅ 会出现 | ✅ 会出现 |
| 读已提交(Read Committed) | ❌ 不会出现 | ✅ 会出现 | ✅ 会出现 | ✅ 会出现 |
| 可重复读(Repeatable Read) | ❌ 不会出现 | ❌ 不会出现 | ⚠️ 部分出现(InnoDB 缓解) | ❌ 不会出现 |
| 串行化(Serializable) | ❌ 不会出现 | ❌ 不会出现 | ❌ 不会出现 | ❌ 不会出现 |
注:✅ 代表会出现,❌ 代表不会出现,⚠️ 代表部分场景会出现