SQL Server不支持真正嵌套事务,SAVE TRANSACTION仅为回滚锚点而非事务边界;嵌套调用会延长锁持有时间、放大死锁风险,应拆分为原子存储过程并由应用层统一控制事务与锁序。
调用 SAVE TRANSACTION 或在已存在事务内再执行 BEGIN TRANSACTION,并不会创建独立子事务;它只增加 @@TRANCOUNT 计数,所有操作仍归属最外层事务。一旦最终 ROLLBACK,整个事务链全部回滚——这和“嵌套”字面意思完全相反。
常见错误现象是:开发者以为 SAVE TRANSACTION savepoint_a 后的失败可以局部回滚,结果 ROLLBACK TRANSACTION savepoint_a 成功,但后续又执行了 ROLLBACK(比如异常未捕获),导致之前所有改动全丢。
SAVE TRANSACTION 来“隔离”逻辑块;它只是回滚锚点,不是事务边界@@TRANCOUNT,尤其不要在 TRY/CATCH 外裸写 BEGIN TRANSACTION
表面看是“一个事务里做了多件事”,实际是锁持有时间被不可控拉长:事务 A 在 UPDATE orders 后没提交,紧接着调用另一个含 UPDATE customers 的存储过程(复用同一事务上下文),而事务 B 正好按相反顺序访问——死锁闭环就形成了。
更隐蔽的是,嵌套调用常伴随隐式锁升级。比如内层过程执行 SELECT * FROM order_items WHERE order_id = @id,但 order_id 没索引,触发全表扫描+页锁,瞬间锁住上千行,远超业务真正需要的几行。
Clustered Index Scan 或 Index Scan,这是锁范围失控的信号WHERE 条件字段都加上索引,且确保类型严格匹配(如 INT 字段别传字符串)OPENQUERY、EXEC xp_cmdshell),它们不释放锁但会卡住几秒与其在存储过程里模拟嵌套,不如把业务逻辑拆成原子操作,由调用方统一控制事务边界和表访问顺序。例如转账场景,不要写一个“扣款+入账+记日志”的大过程,而是拆为三个小过程,在应用层用单个事务包裹,并强制按 accounts → transactions → audit_log 顺序执行。
-- LOCK ORDER: accounts → transactions
TOP (500) + 循环,每批后 COMMIT,避免单次锁住上万行orders → users 更新,而风控服务反着来,死锁必然重现你可以在测试环境把锁序、索引、事务粒度都调到最优,但只要有一个开发在新写的存储过程中动态拼接表名、或在事务里加了个 HTTP 调用、或忘了给新字段建索引,高并发下死锁就会在凌晨三点准时出现。
最有效的防线其实是代码审查 checklist:每次 PR 必须确认是否含 OPENQUERY、是否所有 WHERE 字段都有索引、是否声明了 LOCK ORDER 注释、@@TRANCOUNT 是否被意外修改——这些细节比任何自动工具都管用。