SERIALIZABLE 能防止 INSERT 场景幻读,因其对查询范围加 RangeS-S 锁阻塞并发插入,但无法解决“检查后插入”逻辑漏洞,且需配合索引、原子操作和正确事务封装才有效。

直接结论:把事务隔离级别设为 SERIALIZABLE 能防止 INSERT 场景下的幻读,但代价是严重阻塞并发写入,且不解决“检查后插入”类逻辑漏洞。
为什么 SERIALIZABLE 能拦住幻读
SQL Server 在 SERIALIZABLE 级别下,会对 SELECT 查询涉及的索引范围加 RangeS-S(共享范围锁),不只是锁已有行,还会锁住“可能插入新行”的间隙。这意味着:另一个事务执行 INSERT 时,如果该插入位置落在你前一个 SELECT 的扫描范围内,就会被阻塞,直到你事务结束。
常见错误现象:没设 SERIALIZABLE,只用 READ COMMITTED,执行类似“查有没有 pending 订单 → 没有就插入一条”,结果两个并发事务都查到空,都插入,违反业务唯一性约束。
- 必须显式开启事务并设置级别:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;,否则默认仍是READ COMMITTED - 仅靠
SELECT不够,得配合后续写操作(如INSERT)才构成完整防护闭环 - 若查询条件没走索引(例如
WHERE status = 'pending'但status列无索引),SQL Server 会升级为表锁,性能骤降
SERIALIZABLE 下 INSERT 仍可能失败的典型场景
即使用了 SERIALIZABLE,下面这些情况依然会报错或绕过防护:
- 应用层先
SELECT判断存在性,再单独发INSERT—— 这两步之间存在时间窗口,SERIALIZABLE只保证单条语句的范围锁,不保护跨语句逻辑 - 使用
INSERT ... SELECT但子查询未显式加锁,例如:INSERT INTO orders SELECT ... FROM other_table WHERE ...,此时子查询默认仍按当前隔离级别走,未必受SERIALIZABLE影响 - 目标表上有触发器,触发器内隐式执行的
SELECT不继承外层事务的隔离级别
比 SERIALIZABLE 更轻量的替代方案
多数真实业务不需要全局串行化,可考虑以下更精准的控制方式:
- 用
UPDLOCK+HOLDLOCK提前锁定范围:SELECT * FROM orders WITH (UPDLOCK, HOLDLOCK) WHERE status = 'pending';,这等效于手动加SERIALIZABLE的范围锁,但粒度可控 - 改用
MERGE语句,配合WHEN NOT MATCHED THEN INSERT,它在内部自动处理存在性判断与插入的原子性 - 依赖唯一索引 +
IGNORE_DUP_KEY或捕获2627错误码重试,适合“尽力插入、不强求一次成功”的场景
真正容易被忽略的点是:幻读防护从来不是单靠隔离级别就能一劳永逸的事。它必须和你的具体 SQL 写法、索引设计、以及是否把“读-判-写”封装成原子操作绑定在一起。脱离这些谈 SERIALIZABLE,等于只装了门锁却忘了关窗。

















