幻读问题并非源于INSERT语句本身,而是“先查询后插入”的业务逻辑在并发场景下暴露了间隙锁漏洞。SERIALIZABLE隔离级别仅对单条SELECT语句有效,无法覆盖多语句操作,并且依赖索引和显式事务的支持。

直截了当,结论非常清晰:幻读问题并非INSERT语句本身的过错,而是“先查询后插入”这类业务逻辑在并发执行时暴露了间隙锁漏洞。许多开发者一遇到幻读就立刻想到SERIALIZABLE隔离级别,期望它能一劳永逸——然而,大多数场景下它只能防护部分情况,治标不治本,而且性能代价极高。
为什么 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 不一定有效
设了SERIALIZABLE就以为万事大吉?结果重复插入依然会出现。拆开来看,原因其实很实在:
- 它只对单条
SELECT语句生效——如果业务代码采用SELECT...; IF NOT EXISTS... INSERT...这种两步操作,中间那几十毫秒就是没有保护的窗口期。 - 没有索引时,SQL Server会将
RangeS-S锁升级为表锁,不仅无法防止幻读,还会导致整个表被锁定。 INSERT SELECT语句中的子查询如果没有显式加锁,隔离级别不会自动继承,同样会出现幻读。- 触发器中执行的隐式
SELECT完全不受外层事务隔离级别的约束。
换言之,SERIALIZABLE并非一把万能锁,它仅在自己管辖的单条语句上生效;一旦业务逻辑被拆分为两步操作,中间就会出现脆弱的窗口期。
真正有效的解决方案:用原子性操作替代隔离级别
别再把赌注压在“锁范围”上,直接让“判断 + 插入”变成一条不可拆分的操作——这才是正解。
- 使用
MERGE语句:MERGE INTO orders USING (VALUES (@id, @status)) AS src(id, status) ON src.id = orders.id WHEN NOT MATCHED THEN INSERT (id, status) VALUES (src.id, src.status);—— 内部自动加锁,不存在中间状态。 - 利用唯一索引与错误捕获:创建
UNIQUE (order_no)约束,然后直接执行INSERT,遇到错误码2627(违反唯一约束)时进行重试或忽略,这种方式比加锁更轻量。 - 通过
UPDLOCK+HOLDLOCK手动锁定范围:先执行SELECT 1 FROM orders WITH (UPDLOCK, HOLDLOCK) WHERE order_no = @no;,再执行INSERT——锁粒度可控,并明确告知SQL Server将要在此处写入。
这三种方法的核心思路都是将“读取-判断-写入”绑定为一个原子操作,从而彻底消除中间状态的漏洞。
不可忽视的底层依赖:索引与事务封装
上述所有方案都基于一个前提:查询条件能够使用索引。如果WHERE status = 'pending'所对应的status列没有建立索引,那么HOLDLOCK会退化为表锁,SERIALIZABLE隔离级别将失效,MERGE语句的性能也会急剧下降。
此外,事务必须显式开启:使用BEGIN TRANSACTION,不能依赖隐式事务或自动提交模式——否则锁会在提交后立即释放,防护措施形同虚设。
真正导致问题的,从来不是选择哪个隔离级别,而是“读取-判断-写入”这三个步骤是否真正被绑定为一个原子操作,以及支撑它的索引是否存在、是否被查询实际使用。这才是解决幻读问题的根本逻辑。
