首页/数据/SQL 速查/并发异常:脏读、不可重复读、幻读
变更与事务52 条

并发异常:脏读、不可重复读、幻读

三类典型并发异常与隔离级别的对应关系:级别越高异常越少,但冲突与重试成本上升。

SQL 速查数据更新于 2026-10-13

并发异常是什么

隔离级别存在的意义是抑制并发异常,主要有四类:脏读(读到别人未提交的数据)、不可重复读(同一事务里两次读同一行结果不同)、幻读(同一事务里同一条件两次查询行数不同)、丢失更新(两个事务基于同一旧值更新,后提交的覆盖前一个)。SQL 标准用隔离级别与这些异常对应,但丢失更新不在标准定义范围内,它需要乐观锁、当前读或串行化才能挡住。MySQL InnoDB 默认 REPEATABLE READ,PostgreSQL 默认 READ COMMITTED,跨库迁移时并发行为会跟着变。

基本信息

READ UNCOMMITTED允许脏读,生产基本不用
READ COMMITTED防脏读;仍可能出现不可重复读与幻读
REPEATABLE READ防脏读与不可重复读;MySQL 用 next-key 锁基本抑制幻读
SERIALIZABLE全部异常都能防止,代价是并发度最低
丢失更新不属于隔离级别定义范围,需乐观锁或当前读处理
默认值差异MySQL InnoDB 默认 REPEATABLE READ;PostgreSQL 默认 READ COMMITTED

典型使用场景

  • 设计库存扣减、账务记账等并发敏感逻辑,先明确要防哪类异常
  • 排查「同一事务两次读结果不同」,判断隔离级别与快照时机
  • 为高冲突业务选择隔离级别与重试策略
  • 定位死锁与更新丢失,判断是锁范围问题还是应用逻辑问题
  • 跨库迁移时对比默认隔离级别,避免并发行为悄悄改变

不可重复读与幻读演示

不可重复读与幻读演示
-- 不可重复读(READ COMMITTED 下)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;   -- 300
# 另一会话:UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1;   -- 500:两次结果不同
COMMIT;

-- 幻读:同一条件两次查询行数不同
SELECT COUNT(*) FROM orders WHERE user_id = 42 AND amount > 100;   -- 3
# 另一会话插入一条满足条件的行并提交
SELECT COUNT(*) FROM orders WHERE user_id = 42 AND amount > 100;   -- 4

丢失更新与对策

丢失更新与对策
# 危险写法:先读后写,中间没有并发保护
SELECT stock FROM products WHERE id = 1;      # 读到 10
# 应用算出 9,再写回
UPDATE products SET stock = 9 WHERE id = 1;   # 覆盖了别人刚提交的改动

# 对策一:条件更新(乐观锁),受影响行数为 0 说明被别人改过
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 7 AND stock >= 1;

# 对策二:当前读,先加锁再计算
START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;   # 加行锁,读最新已提交值
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

隔离级别与异常对照

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会MySQL 用间隙锁基本抑制;PG 为快照隔离,仍可能
SERIALIZABLE不会不会不会
PostgreSQL 的 REPEATABLE READ 属于快照隔离,可能出现「写偏斜」:两个事务分别修改同一条件的相关数据,各自都合法但合起来违反约束;要彻底避免需使用 SERIALIZABLE(PG 用 SSI 实现)。
隔离级别越高并发越低:SERIALIZABLE 下语句会因序列化冲突而报错,应用必须具备重试逻辑(捕获冲突错误后重跑整个事务),否则冲突会直接变成用户可见的失败。

易错点

  • 在 READ COMMITTED 下写「读—计算—写」,并发时出现丢失更新
  • 以为改成 REPEATABLE READ 就万事大吉:仍会遇到锁等待、死锁,PG 的快照隔离还可能写偏斜
  • 把 SERIALIZABLE 当万能开关,缺少重试逻辑导致序列化冲突直接报给用户
  • 跨库迁移不核对默认隔离级别(MySQL RR、PG RC),并发行为悄悄改变

数据来源与参考

  • MySQL 8.0 Reference Manual · Transaction Isolation Levels
  • PostgreSQL Documentation · Transaction Isolation
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌