很多人排查存储过程里的脏读、阻塞或死锁时,第一反应是“隔离级别不是已经写了吗”。问题往往不在有没有写,而在写的位置对不对、事务是不是已经开始、当前数据库又按什么规则解释这条 SET。
这篇文章分 SQL Server 和 MySQL 两条线来讲,先说明隔离级别什么时候才会真正生效,再看 READ COMMITTED 和 SERIALIZABLE 在生产环境里最容易带来的误判。读完后,你可以据此检查自己的存储过程是顺序写错了,还是并发模型本身就不适合当前级别。
SQL Server:隔离级别必须先于事务和数据操作
在 SQL Server 存储过程中,SET TRANSACTION ISOLATION LEVEL 要想生效,前提是它必须出现在任何数据操作语句和 BEGIN TRANSACTION 之前。这里的数据操作不仅包括 UPDATE、INSERT、DELETE,也包括第一条 SELECT。
一旦第一条 SELECT 已经执行,当前执行路径上的隔离级别就按当时的生效值锁定,后面再写 SET 也不会回头修正前面的读取行为。
为什么明明写了 READ COMMITTED,还是会出现脏读
常见现象有两种:一是 SELECT 读到了未提交数据,但代码里明明写了 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;二是并发调用同一个存储过程时,一个请求出现脏读,另一个没有。这通常说明隔离级别没有在稳定、统一的时点生效。
正确顺序应该是:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- 业务逻辑有几个细节尤其容易被忽略:
- 不要把
SET TRANSACTION ISOLATION LEVEL ...放到第一条SELECT之后,再补救已经来不及。 - 嵌套调用时,不能假设外层事务已经替你设好级别。哪怕
@@TRANCOUNT > 0,内层存储过程也应显式设置,否则可能继承外层留下的READ UNCOMMITTED。 -- SET ...只是注释,不会执行。生产环境里隔离级别是否真的生效,不能靠“代码看起来像写了”来判断。
READ COMMITTED 不是无锁读取,照样可能卡住写入
不少团队把 READ COMMITTED 当成“既安全又轻量”的默认选项,但在 SQL Server 中,这个理解并不完整。
当 READ_COMMITTED_SNAPSHOT = OFF(SQL Server 默认)时,READ COMMITTED 下的每条 SELECT 仍会加共享锁(S 锁),直到语句执行完成才释放。所以长时间查询并不会悄无声息地读过去,它可能直接阻塞后续 UPDATE,反过来写操作也会拖住读取。
哪些场景最容易把自己拖进阻塞或 deadlock
典型场景是报表类存储过程对历史表做全表扫描,同时业务线程正在更新同一张表上的热点行。结果就是双方互相等待,最后表现为 deadlock、锁等待飙升,或者接口长时间超时。
这类问题可以优先从下面几个方向检查:
- 看执行计划里是否出现
Table Scan或Index Scan,如果能优化成Index Seek,通常就能明显缩短锁持有时间。 - 避免在事务开头直接
SELECT *读取一批无关字段,查询越重,共享锁持续时间越长。 - 如果业务属于读多写少,且可以接受快照一致性,可以考虑启用数据库级
ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON。
不过要注意,一旦启用了 READ_COMMITTED_SNAPSHOT ON,会话级 SET TRANSACTION ISOLATION LEVEL READ COMMITTED 的行为就会转向行版本快照,不再是传统的共享锁读取。这里的“READ COMMITTED”名称没变,底层机制已经变了。
SERIALIZABLE 为什么常常是死锁放大器
SERIALIZABLE 看起来像是“最安全”的选择,但在大多数业务系统里,它更像是一个代价极高的并发开关。

在 SQL Server 中,SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 不只是锁住已经读取到的行,还会锁住可能插入新行的索引间隙。也就是说,即便只是查询 WHERE id = 123,只要索引上存在 id BETWEEN 100 AND 150 的空隙,对应范围也可能被一起锁住。
它会怎么把普通业务放大成批量死锁
典型翻车方式是:订单状态更新存储过程默认用了 SERIALIZABLE,等到批量补单时,20 个线程同时跑起来,结果全卡在 INSERT 上,死锁图里到处都是等待键锁。
SERIALIZABLE不适合作为绝大多数业务场景的默认隔离级别。- 它对并发性能影响很大,读操作也会显著提高锁冲突概率。
- 如果目标是真正的强一致,通常更应该优先评估“应用层重试 + 乐观锁”,而不是直接把数据库锁范围放到最大。
MySQL:必须在 START TRANSACTION 之前设置会话级隔离级别
MySQL 和 SQL Server 最容易混淆的点,是它们对“事务何时开始、隔离级别何时固定”这件事的处理不同。
在 MySQL 存储过程中,事务隔离级别是会话级生效的。一旦执行了 START TRANSACTION 或 BEGIN,当前事务使用的隔离级别就已经固定下来。此时再执行 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,并不会回头影响这个已经开始的事务。
MySQL 里最常见的误写和隐蔽问题
最常见的误写就是先开事务,再设置隔离级别:
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@session.transaction_isolation;这样的顺序下,事务内第一条 SELECT 仍然按照旧级别执行,后面的 SET SESSION 只会影响未来的新事务。
更隐蔽的问题出现在存储过程嵌套调用时:如果当前连接上已经存在事务,那么再执行 SET SESSION 也不会中途改变这个事务的隔离级别。也就是说,被外层已开启事务调用的内层过程,不能指望自己在事务进行中“临时改级别”。
更稳妥的写法应当遵循这个顺序:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 业务逻辑同时建议一起做这几件事:
- 每次进入关键逻辑前显式覆盖隔离级别,不要依赖连接池复用后的会话残留状态。
- 用
SELECT @@session.transaction_isolation检查当前级别,不要只相信配置文件或部署文档里的“统一设置”。 - 如果过程可能被已有事务调用,就要把“当前级别无法中途变更”纳入设计,而不是假设过程内部一定能重新设定。
跨 SQL Server 和 MySQL 迁移时,最容易错的其实是思维模型
这类问题难排查,不是因为命令本身复杂,而是因为很多人默认认为“只要写了 SET TRANSACTION ISOLATION LEVEL,后面的事务就会自动按我想的方式运行”。实际情况并不是这样。

SQL Server 更强调:隔离级别必须在 BEGIN TRANSACTION 和任何数据操作之前就已生效;MySQL 更强调:必须在 START TRANSACTION 之前设置好会话级隔离级别,一旦事务启动就无法中途修改。
两者共同点只有一个:级别变更只对未来语句或未来事务生效,不能回溯修正已经发生的读取。跨库迁移时,如果还沿用同一套事务思维,脏读、阻塞和死锁几乎是迟早会出现的问题。







