存储过程不自动开启事务,需显式用BEGIN TRAN和COMMIT/ROLLBACK控制;必须配合TRY...CATCH、SET XACT_ABORT ON及@@TRANCOUNT检查,确保错误时正确回滚并记录详情。

CREATE PROCEDURE 本身不自动开启事务,必须显式用 BEGIN TRAN 和 COMMIT/ROLLBACK 控制,否则每条语句独立提交——这是最常被忽略的前提。
带事务的存储过程必须用 TRY...CATCH 包裹
裸写 BEGIN TRAN + IF @@ERROR 已过时且不可靠:@@ERROR 只捕获上一条语句的错误,中间插入的 PRINT、变量赋值等会清空它;而语法错误(如表名错)根本不会触发 @@ERROR。
正确做法是用 BEGIN TRY / BEGIN CATCH 结构,配合 @@TRANCOUNT 判断是否还有未提交事务:
-
SET XACT_ABORT ON必须放在开头——它让运行时错误(如主键冲突、除零)自动终止整个事务,避免部分执行 -
BEGIN TRY块里放所有业务逻辑(INSERT/UPDATE/DELETE) -
BEGIN CATCH里先检查IF @@TRANCOUNT > 0,再ROLLBACK TRAN,否则可能报“没有活动事务”错误 - 用
ERROR_MESSAGE()、ERROR_LINE()记录具体失败位置,别只靠PRINT
CREATE PROCEDURE usp_TransferMoney
@FromID INT,
@ToID INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- 关键:让运行时错误直接炸掉整个事务
<pre class='brush:php;toolbar:false;'>BEGIN TRY
BEGIN TRAN;
UPDATE Accounts SET Balance = Balance - @Amount WHERE ID = @FromID;
UPDATE Accounts SET Balance = Balance + @Amount WHERE ID = @ToID;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
-- 抛出完整错误信息,方便排查
DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @ErrLine INT = ERROR_LINE();
RAISERROR('转账失败,行号 %d: %s', 16, 1, @ErrLine, @ErrMsg);
END CATCHEND;
事务名和保存点(SAVE TRAN)慎用
给 BEGIN TRAN 加名字(如 BEGIN TRAN tran1)或用 SAVE TRAN tran2 看似灵活,实际带来三个麻烦:
- 嵌套事务中,
COMMIT TRAN tran1不等于真正提交——只有最外层COMMIT才生效,内层只是减少@@TRANCOUNT计数 -
SAVE TRAN创建的还原点不能跨批处理存在,存储过程中一旦发生错误并ROLLBACK TRAN savepoint,后续语句仍可能因事务已中断而失败 - 命名事务在分布式事务或链接服务器场景下容易冲突,SQL Server 2019 默认不支持跨实例事务名传递
除非明确需要部分回滚(例如批量导入中跳过单条脏数据),否则直接用无名事务 + 全局 TRY...CATCH 更稳。
EXECUTE 权限与事务隔离级别需单独控制
创建完存储过程后,调用者能否真正执行事务,取决于两件事:
- 用户必须有该存储过程的
EXECUTE权限,仅SELECT或UPDATE权限不够——事务控制权在过程内部,权限校验发生在入口 - 事务行为受会话级
SET TRANSACTION ISOLATION LEVEL影响,但存储过程里不能直接改它;如果业务要求可串行化,得在调用前显式设置:SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; EXEC usp_TransferMoney ... - 临时表(
#temp)可在事务中安全使用,但表变量(@table)不参与事务回滚——删了就真没了
调试时最容易漏掉的三件事
本地测试通过不代表生产可靠,这三个点几乎每次部署都出问题:
-
SET NOCOUNT ON没加:客户端收到大量 “X 行受影响” 消息,某些 ORM(如 Entity Framework)会误判为结果集,抛出Invalid operation on closed connection - 参数未声明
NULL允许性:比如@Amount DECIMAL(18,2)缺少= NULL,调用时传NULL会直接失败,而不是进CATCH - 忘记检查目标对象是否存在:存储过程里引用的表或列,若在部署后被重构(如字段重命名),
CREATE PROCEDURE会成功,但首次执行才报错——用sys.dm_exec_describe_first_result_set可提前验证元数据
事务不是开关,是状态机。从 BEGIN TRAN 到最终 COMMIT 或 ROLLBACK,中间任何一环没兜住,数据就可能卡在半途。尤其在 SQL Server 2019 的乐观并发控制(如 SNAPSHOT 隔离)下,事务行为和旧版本已有差异,别依赖经验直觉。

















