READ COMMITTED 是高并发存储过程的默认起点,但必须显式设置以避免继承异常隔离级别;它仅解决读-写阻塞,不防写-写冲突;UPDLOCK+ROWLOCK 须在事务内且走索引才生效;大数据量应选临时表并建索引;批量操作需避免循环,用集合操作分段提交。

为什么 READ COMMITTED 是高并发存储过程的默认起点,但不能不设
SQL Server 默认隔离级别确实是 READ COMMITTED,但这不等于“开了就行”。它决定的是读操作是否加共享锁、写操作锁的粒度与持续时间——直接影响死锁率和热点行排队长度。比如一个订单状态更新存储过程,若没显式声明 SET TRANSACTION ISOLATION LEVEL READ COMMITTED,而数据库被手动改过全局配置,或连接复用时继承了上一个会话的 SERIALIZABLE 级别,就会突然卡住。
关键点:
- 必须在存储过程开头显式设置,不依赖会话默认值
-
READ COMMITTED下仍会发生写-写阻塞(UPDATE 冲突同一行),它只解决读-写阻塞 - 若业务能容忍幻读(如报表统计),就别升到
REPEATABLE READ;升级后范围锁增多,死锁概率翻倍
UPDLOCK + ROWLOCK 不生效?先看事务和索引在不在
很多人写 SELECT ... WITH (UPDLOCK, ROWLOCK) 却发现锁不住、还是超卖,问题往往出在两个地方:没包在事务里,或者查询根本没走索引。
典型错误场景:
- 单独执行
SELECT id FROM orders WITH (UPDLOCK, ROWLOCK) WHERE order_no = @no—— 没有BEGIN TRANSACTION,语句一结束锁就释放 - WHERE 条件列
order_no没建索引,优化器直接升级为页锁甚至表锁,ROWLOCK提示被忽略 - 在事务开头就查一堆无关数据(如
SELECT * FROM users WITH (UPDLOCK)),锁提前占满,后续 UPDATE 反而等更久
正确姿势是:先 BEGIN TRAN,再用索引字段精准定位单行,紧接着做 UPDATE,最后 COMMIT。
临时表 vs 表变量:大数据量下选错直接拖慢10倍
在批量处理场景(如万级商品库存调整),用 @table 表变量替代 #temp 临时表,常导致执行计划崩坏——因为表变量没有统计信息,优化器永远按“1行”估算,强制走嵌套循环,实际数据上万时 CPU 火焰图直接拉满。
该用哪个,看三点:
- 数据量 @table,轻量、无日志开销
- 数据量 > 500 行,且要 JOIN 或 WHERE 过滤 → 必须用
#temp,并为连接/过滤字段建索引(如CREATE INDEX IX_ProductID ON #ProductUpdates(ProductID)) - 需要在多个存储过程中复用中间结果 → 只能用
#temp,表变量作用域仅限当前批
批量操作别手写循环,也别一股脑全塞进一个事务
用 WHILE 循环逐条 INSERT/UPDATE 是高并发存储过程最常见的性能杀手。每轮都触发日志写入、锁获取、执行计划重用判断,吞吐量卡死在几百 TPS。
真正有效的批量策略是:
- 用
INSERT INTO ... SELECT或UPDATE ... FROM替代循环,一次处理千行以上 - 单个事务控制在 500–2000 行以内,避免长事务持锁太久引发连锁等待
- 对超大批量(如 10 万+),在存储过程中分段提交:每次
TOP 1000+COMMIT,用@@ROWCOUNT判断是否继续
最易被忽略的一点:批量 UPDATE 前,确保目标表的关键 WHERE 字段有索引。否则即使批量,也会变成全表扫描+行锁升级,效果还不如单条。


















