在数据库事务并发执行时,如果没有适当的隔离控制,会出现三类经典的数据一致性问题:脏读(Dirty Read)、不可重复读(Non-Repeatable Read)、幻读(Phantom Read)。它们的严重程度依次递增,也是 SQL 标准定义四种隔离级别的依据。
下面通过具体业务场景 + 示例逐一说明:
一、脏读(Dirty Read)
读到了另一个事务尚未提交(可能回滚)的数据
📌 场景:用户查看账户余额
- 事务 A:正在转账(从账户 X 扣 100 元)
- 事务 B:同时查询账户 X 的余额
⏱️ 执行过程:
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 余额从 500 → 400
-- 事务 B(此时事务 A 未提交!)
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1; -- 读到 400 元!
-- 事务 A(突然出错,回滚)
ROLLBACK; -- 余额恢复为 500
❌ 问题:
事务 B 看到了 400 元,但这个数据从未真正存在过(因为 A 回滚了)。
这就是 脏读——读到了“脏数据”。
✅ 解决方案:隔离级别 ≥
READ COMMITTED(RC)即可禁止脏读。
二、不可重复读(Non-Repeatable Read)
同一个事务内,两次读同一行数据,结果不同(因为其他事务已提交修改)
📌 场景:电商下单前检查库存
- 事务 A:准备下单,先查库存
- 事务 B:同时卖出同一商品,扣减库存并提交
⏱️ 执行过程:
-- 事务 A
START TRANSACTION;
SELECT stock FROM products WHERE sku_id = 1001; -- 第一次读:stock = 10
-- 事务 B
START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE sku_id = 1001; -- stock = 9
COMMIT; -- 提交!
-- 事务 A(继续)
SELECT stock FROM products WHERE sku_id = 1001; -- 第二次读:stock = 9 ❗
❌ 问题:
事务 A 在同一个事务中,对同一行(sku_id=1001)读了两次,结果不一致。
如果 A 第一次读到 10 就认为“有货”,但第二次发现只剩 9,可能导致逻辑错误(如超卖判断失效)。
✅ 解决方案:隔离级别 ≥
REPEATABLE READ(RR)可禁止不可重复读。
三、幻读(Phantom Read)
同一个事务内,两次执行相同的范围查询,返回的记录数不同(因为其他事务插入/删除了符合范围的新行)
📌 场景:管理员禁止用户在 5 分钟内重复下单同一商品
- 事务 A:检查用户最近是否有下单
- 事务 B:同时插入一条新的订单
⏱️ 执行过程:
-- 事务 A
START TRANSACTION;
SELECT * FROM orders
WHERE user_id = 100 AND sku_id = 200
AND create_time > NOW() - INTERVAL 5 MINUTE; -- 查到 0 条,认为可下单
-- 事务 B
START TRANSACTION;
INSERT INTO orders (user_id, sku_id, ...)
VALUES (100, 200, ...); -- 插入一条新订单
COMMIT;
-- 事务 A(继续)
SELECT * FROM orders
WHERE user_id = 100 AND sku_id = 200
AND create_time > NOW() - INTERVAL 5 MINUTE; -- 现在查到 1 条!
❌ 问题:
事务 A 两次执行完全相同的范围查询,结果集行数从 0 变成 1,仿佛出现了“幻影”(Phantom)。
如果 A 在第一次查询后直接插入订单,就会违反“5分钟内不能重复下单”的业务规则。
✅ 标准 SQL 解决方案:隔离级别 =
SERIALIZABLE
✅ MySQL InnoDB 特殊优化:在REPEATABLE READ下通过 Next-Key Lock(间隙锁) 实际避免了幻读(需使用SELECT ... FOR UPDATE等当前读)。
四、三者对比总结
| 问题类型 | 操作对象 | 原因 | 是否修改了已存在行 | 典型场景 |
|---|---|---|---|---|
| 脏读 | 单行 | 读到未提交的数据 | 是(但会回滚) | 查看中间状态余额 |
| 不可重复读 | 单行 | 其他事务已提交修改 | 是 | 库存、价格变动 |
| 幻读 | 多行(范围) | 其他事务插入/删除了新行 | 否(是新增/消失行) | 订单去重、分页防重复 |
🔑 关键区别:
- 脏读 / 不可重复读:针对已存在的某一行
- 幻读:针对满足条件的行集合(范围变化)
五、MySQL 中的实际表现(InnoDB 引擎)
| 隔离级别 | 脏读 | 不可重复读 | 幻读(标准) | 幻读(MySQL RR) |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | ✅ |
| READ COMMITTED | ❌ | ✅ | ✅ | ✅(普通 SELECT 无锁) |
| REPEATABLE READ | ❌ | ❌ | ✅ | ❌(通过间隙锁避免) |
| SERIALIZABLE | ❌ | ❌ | ❌ | ❌ |
💡 注意:MySQL 的
REPEATABLE READ强于 SQL 标准,在使用SELECT ... FOR UPDATE/UPDATE等当前读时,能通过间隙锁防止幻读。
六、如何避免?—— 开发建议
- 避免脏读:生产环境不要用
READ UNCOMMITTED - 避免不可重复读:核心业务(如库存、支付)使用
REPEATABLE READ或显式加锁 - 避免幻读:
- 范围操作(如查最近订单)必须用
SELECT ... FOR UPDATE - 确保查询字段有合适索引(否则锁全表!)
- 考虑用唯一约束替代应用层检查(如
(user_id, sku_id, 5min_window)唯一键)
- 范围操作(如查最近订单)必须用
🌰 一句话记住:
“脏读看未提交,不可重复读看已提交的修改,幻读看新出现的行。”


被折叠的 条评论
为什么被折叠?



