SQL 隔离级别详解与应用案例

2次阅读

read committed 查不到刚插入记录是因为事务未提交,其他事务默认不可见;repeatable read 下仍可能幻读,因间隙锁无法覆盖所有场景;高并发扣库存应选 repeatable read 配合原子 update 或 select for update。

SQL 隔离级别详解与应用案例

READ COMMITTED 为什么查不到刚插入的记录?

因为事务还没提交,其他事务默认看不到未提交的变更。这是隔离级别的基本约束,不是 bug

  • postgresql 默认是 READ COMMITTEDmysql InnoDB 在 RR 模式下也常被误以为“实时可见”,其实不然
  • 执行 INSERT 后立刻在另一个会话查不到,大概率是没提交,而不是隔离级别设错了
  • 如果真需要“刚写就可见”,得用 SELECT ... FOR UPDATE 或显式加锁,但会阻塞并发,别盲目上

REPEATABLE READ 下为什么还会出现幻读?

MySQL InnoDB 的 REPEATABLE READ 通过间隙锁(gap lock)抑制大部分幻读,但不等于完全消除——尤其涉及 INSERT ... SELECT 或无索引字段查询时。

  • 幻读典型场景:SELECT count(*) WHERE status = 'pending' 返回 5,接着另一事务插入一条 status = 'pending' 并提交,当前事务再查还是 5;但如果执行 UPDATE ... WHERE status = 'pending',InnoDB 可能只锁已有行,新插入的行不受影响
  • 真正防幻读得靠 SERIALIZABLE,但代价是自动将所有 SELECT 转成 SELECT ... LOCK IN SHARE MODE,并发性能断崖下跌
  • 多数业务用 REPEATABLE READ + 应用层重试或唯一约束更实际,比如插入前先 SELECT FOR UPDATE 检查是否存在

如何临时切换会话隔离级别?

SET TRANSACTION ISOLATION LEVEL,但要注意作用域和生效时机。

  • 必须在事务开始前设置,否则报错:Error 1568 (25001): Transaction characteristics can't be changed while a transaction is in progress
  • MySQL 支持:READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE;PostgreSQL 不支持 READ UNCOMMITTED(等价于 READ COMMITTED
  • 示例:SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SELECT * FROM orders; —— 这个 SELECT 就运行在 RC 级别下
  • 注意:某些 ORM(如 django)会在连接池初始化时固定隔离级别,手动 SET 可能被覆盖,得看框架文档是否允许 per-transaction override

高并发下单扣库存,该选哪个隔离级别?

别迷信“越高级越安全”,SERIALIZABLE 在订单场景往往引发大量锁等待甚至死锁,REPEATABLE READ 配合行锁+唯一约束才是更稳的选择。

  • 典型错误:用 SELECT ... WHERE stock > 0 判断后 UPDATE SET stock = stock - 1,中间可能被其他事务插队扣成负数 —— 这不是隔离级别问题,是逻辑漏洞
  • 正确做法:一条 UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1,检查 ROW_COUNT() 是否为 1
  • 如果业务要求强一致性(比如秒杀),加 SELECT ... FOR UPDATE 是明确意图,但要确保 WHERE 条件走索引,否则升级为表锁
  • PostgreSQL 的 SELECT ... FOR UPDATE SKIP LOCKED 能跳过已被锁的行,适合分页派单类场景,MySQL 8.0+ 才支持类似语法

实际应用中,隔离级别不是配置开关,而是和 SQL 写法、索引设计、事务粒度捆在一起的。改了级别却没调查询逻辑,反而更容易出隐蔽问题。

text=ZqhQzanResources