唯一可靠防幻读的方式是将整个事务设为SERIALIZABLE或启用SNAPSHOT(SQL Server),且必须在调用存储过程前由客户端或会话设置;SET TRANSACTION ISOLATION LEVEL在存储过程或触发器内无法改变已开启事务的隔离级别,仅对后续新事务生效。

直接说结论:在存储过程中设置隔离级别防幻读,唯一可靠的方式是把整个事务设为 SERIALIZABLE 或启用 SNAPSHOT(SQL Server),但必须在调用存储过程前、由客户端或上层会话完成设置——SET TRANSACTION ISOLATION LEVEL 不能在存储过程内部“生效”到外层事务,更不能在触发器里用。
为什么存储过程里 SET TRANSACTION ISOLATION LEVEL 常被忽略
很多人在存储过程开头写 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE,以为能“局部生效”,实际它只影响该语句之后、当前会话中**尚未启动的事务**。如果调用方已经 BEGIN TRANSACTION,这个 SET 就完全无效——事务隔离级别在 BEGIN TRANSACTION 时就已确定,后续无法更改。
- SQL Server 中,执行
SET TRANSACTION ISOLATION LEVEL后再BEGIN TRANSACTION,新事务才按新级别运行 - MySQL 中,
SET SESSION TRANSACTION ISOLATION LEVEL只对后续事务起作用,不影响当前已开启的事务 - PostgreSQL 不允许在事务块内执行
SET TRANSACTION ISOLATION LEVEL,直接报错
真正可控的设置点只有两个位置
防幻读不是“写进存储过程里就完事”,而是要控制事务的起点和上下文:
-
应用层显式开启事务时指定:比如 JDBC 调用
connection.beginTransaction(IsolationLevel.SERIALIZABLE),再执行存储过程 -
客户端会话提前设置:在调用存储过程前,先执行
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE,再BEGIN TRANSACTION+EXEC your_proc
注意:SQL Server 的 SNAPSHOT 需提前在数据库级启用:ALTER DATABASE YourDB SET ALLOW_SNAPSHOT_ISOLATION ON,否则 SET TRANSACTION ISOLATION LEVEL SNAPSHOT 会失败。
存储过程里“先查后改”逻辑必须重构
哪怕隔离级别设对了,存储过程里写 SELECT COUNT(*) FROM audit_log WHERE user_id = @uid 再决定是否 INSERT,依然可能因并发插入导致幻读——因为 SELECT 和 INSERT 是两个语句,在 REPEATABLE READ 下不锁插入间隙。
- 改用原子操作:例如
INSERT INTO audit_log (...) SELECT ... WHERE NOT EXISTS (SELECT 1 FROM audit_log WHERE ...) - SQL Server 可加提示:在关键
SELECT后面追加WITH (UPDLOCK, HOLDLOCK),等效于范围锁 - 避免在存储过程中依赖未索引字段做条件查询,否则
HOLDLOCK可能升级为表锁
测试幻读不能只跑单线程
所有“没问题”的测试都是假象。必须开两个独立会话,同时执行同一存储过程,且都满足触发幻读的条件(比如插入同一组时间范围的新记录),观察是否出现预期外的重复计数或漏拦截。
最容易被忽略的一点:SERIALIZABLE 在高并发下可能引发死锁或长时间阻塞,而 SNAPSHOT 虽然不锁但依赖 tempdb 版本存储——没监控 tempdb 空间和版本清理延迟的话,线上突然慢得像卡住,根本想不到是隔离级别惹的祸。

















