并发异常是什么
隔离级别存在的意义是抑制并发异常,主要有四类:脏读(读到别人未提交的数据)、不可重复读(同一事务里两次读同一行结果不同)、幻读(同一事务里同一条件两次查询行数不同)、丢失更新(两个事务基于同一旧值更新,后提交的覆盖前一个)。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 下语句会因序列化冲突而报错,应用必须具备重试逻辑(捕获冲突错误后重跑整个事务),否则冲突会直接变成用户可见的失败。