显式设置 set transaction isolation level read committed 且置于 begin transaction 之前,可有效阻止脏读;否则将继承调用方低级别导致脏读。

直接结论:在存储过程中显式设置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED 可有效阻止脏读,但必须放在 BEGIN TRANSACTION 之前,且不能依赖连接默认级别。
为什么存储过程里不设隔离级别就容易脏读
SQL Server 连接的默认隔离级别是 READ COMMITTED,听起来很安全——但这是“会话级默认”,不是“存储过程级保障”。一旦调用方(比如应用层或另一个存储过程)在外部显式改了隔离级别(例如设成 READ UNCOMMITTED),再进来的存储过程就会继承这个低级别,SELECT 就可能读到未提交的数据。
常见触发场景:
- 应用代码中执行了
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED后再调用你的存储过程 - 上游存储过程用
sp_executesql动态拼了低隔离查询,影响了后续语句 - 连接池复用时,前一个请求没重置隔离级别,残留设置污染当前执行
在存储过程中正确设置 READ COMMITTED 的写法
关键点:隔离级别必须在事务开始前、任何数据操作前生效;且建议用 SET 显式覆盖,不依赖隐式行为。
正确示例:
CREATE PROCEDURE GetCustomerBalance
@CustomerId INT
AS
BEGIN
-- ✅ 必须放在这里:BEGIN TRANSACTION 之前,且在任何 SELECT/UPDATE 前
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
<pre class="brush:php;toolbar:false;">BEGIN TRY
BEGIN TRANSACTION;
-- 这里的 SELECT 只能看到已提交数据
SELECT Balance FROM Customers WHERE ID = @CustomerId;
-- 其他业务逻辑...
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCHEND
错误写法(踩坑点):
-
SET放在BEGIN TRANSACTION之后 → 实际上已晚,部分语句可能已按旧级别执行 - 只在
IF @@TRANCOUNT = 0时才设 → 忽略嵌套调用场景,不可靠 - 用注释代替实际
SET,指望 DBA 统一配置 → 生产环境无法保证
READ COMMITTED 在存储过程中的实际影响
它不是“万能锁”,而是基于 SQL Server 默认的锁机制(共享锁 + 意向锁)实现的。这意味着:
- 每次
SELECT会加瞬时共享锁,读完即释放,不会阻塞其他事务太久 - 但它**不防止不可重复读**:同一存储过程内两次
SELECT同一行,中间被别人改并提交,第二次可能读到新值 - 如果存储过程里有
JOIN多张表,每张表都按此级别独立加锁,无跨表一致性保证 - 若涉及 Linked Server 跨库查询,
READ COMMITTED**不传递到远端数据库**,远端仍按自己默认级别跑,可能引入脏读
更稳妥的做法:用快照隔离替代 READ COMMITTED
如果存储过程读多写少,且你控制得了数据库配置,开启数据库级快照隔离(ALLOW_SNAPSHOT_ISOLATION ON)后,在存储过程中用:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
好处:
- 完全避免脏读 + 不可重复读(因为读的是事务启动时刻的快照)
- 不加锁,读操作几乎不阻塞写操作
- 对调用方隔离级别无依赖,更可靠
注意:需提前在数据库上启用,且会增加 tempdb 压力;不适合高频更新小表的场景。
真正容易被忽略的,是隔离级别设置的位置和作用域——它不随存储过程定义固化,而随执行时的上下文流动。哪怕写了 READ COMMITTED,漏掉那行 SET 或放错位置,整个防护就形同虚设。










