电商交易系统重要概念:什么是脏读、不可重复读、幻读?

ModelEngine·创作计划征文活动 10w+人浏览 1.4k人参与

在数据库事务并发执行时,如果没有适当的隔离控制,会出现三类经典的数据一致性问题:脏读(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当前读时,能通过间隙锁防止幻读。


六、如何避免?—— 开发建议

  1. 避免脏读:生产环境不要用 READ UNCOMMITTED
  2. 避免不可重复读:核心业务(如库存、支付)使用 REPEATABLE READ 或显式加锁
  3. 避免幻读
    • 范围操作(如查最近订单)必须用 SELECT ... FOR UPDATE
    • 确保查询字段有合适索引(否则锁全表!)
    • 考虑用唯一约束替代应用层检查(如 (user_id, sku_id, 5min_window) 唯一键)

🌰 一句话记住:
“脏读看未提交,不可重复读看已提交的修改,幻读看新出现的行。”

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值