一、背景与目标
在中大型业务系统中,存储过程往往被多处调用、层层嵌套,一旦事务和异常处理设计不规范,极易导致以下问题:
-
子存储过程误提交或回滚父事务
-
事务悬挂(未提交 / 未回滚)
-
异常被吞掉,调用方无法感知失败
-
生产环境问题难以定位
因此,必须建立一套统一、强约束、可组合的存储过程设计规范。
本文给出一套行业级 SQL Server 存储过程事务规范,并解释其设计原理。
二、核心设计原则
原则 1:存储过程不擅自管理外部事务
-
存储过程只在没有事务时才开启事务
-
不擅自
COMMIT/ROLLBACK调用方事务 -
支持多层存储过程安全嵌套
存储过程必须是"事务友好型组件",而不是事务的拥有者。
原则 2:事务的开启、提交、回滚必须成对、可控
统一通过一个本地变量标识事务是否由当前存储过程开启:
DECLARE @TranStarted BIT = 0;
原则 3:TRY...CATCH 是强制要求
-
禁止裸写
BEGIN TRAN / COMMIT -
禁止不捕获异常
-
禁止在 CATCH 中继续执行业务 SQL
原则 4:异常必须可追踪、可传播
-
记录错误日志(表或统一过程)
-
对外部调用者:
-
接口型存储过程 → 返回 code / msg
-
业务型存储过程 → THROW 抛出异常
-
三、统一事务控制模板(强制)
标准事务控制骨架
sql
SET NOCOUNT ON;
SET IMPLICIT_TRANSACTIONS OFF;
DECLARE @TranStarted BIT = 0;
BEGIN TRY
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRAN;
SET @TranStarted = 1;
END
-- ======================
-- 业务逻辑
-- ======================
IF @TranStarted = 1
BEGIN
COMMIT;
END
END TRY
BEGIN CATCH
IF @TranStarted = 1 AND XACT_STATE() <> 0
BEGIN
ROLLBACK;
END
-- 统一错误处理
EXEC dbo.proc_error_log;
-- 异常传播策略由类型决定
END CATCH
四、两类存储过程的规范区分
1️⃣ 接口型存储过程(返回 code / msg)
适用场景
-
被 Java / Go / .NET 直接调用
-
作为接口的"最终边界"
-
前端或服务端需要明确状态码
规范示例(你给出的 nb_tran_demo1)
sql
CREATE PROCEDURE dbo.nb_tran_demo1
AS
BEGIN
SET NOCOUNT ON;
SET IMPLICIT_TRANSACTIONS OFF;
DECLARE @TranStarted BIT = 0;
BEGIN TRY
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRAN;
SET @TranStarted = 1;
END
-- ======================
-- 业务逻辑
-- ======================
IF @TranStarted = 1
BEGIN
COMMIT;
END
SELECT 200 AS code, '成功' AS msg;
END TRY
BEGIN CATCH
IF @TranStarted = 1 AND XACT_STATE() <> 0
BEGIN
ROLLBACK;
END
EXEC dbo.proc_error_log;
SELECT 501 AS code, '系统处理失败' AS msg;
END CATCH
END
设计要点
-
❌ 不使用
THROW -
✅ 异常被转换为业务可识别结果
-
✅ 保证接口幂等、稳定
2️⃣ 业务型存储过程(必须 THROW)
适用场景
-
被其他存储过程调用
-
业务逻辑拆分、复用
-
不直接面对前端或接口层
规范示例(你给出的 nb_tran_demo)
sql
CREATE PROCEDURE dbo.nb_tran_demo
AS
BEGIN
SET NOCOUNT ON;
SET IMPLICIT_TRANSACTIONS OFF;
DECLARE @TranStarted BIT = 0;
BEGIN TRY
IF @@TRANCOUNT = 0
BEGIN
BEGIN TRAN;
SET @TranStarted = 1;
END
-- ======================
-- 业务逻辑
-- ======================
IF @TranStarted = 1
BEGIN
COMMIT;
END
END TRY
BEGIN CATCH
IF @TranStarted = 1 AND XACT_STATE() <> 0
BEGIN
ROLLBACK;
END
EXEC dbo.proc_error_log;
THROW;
END CATCH
END
设计要点
-
✅ 必须使用
THROW -
❌ 禁止返回
SELECT code -
✅ 由上层决定事务命运
五、XACT_STATE 的强制使用规范
| XACT_STATE() | 含义 | 处理方式 |
|---|---|---|
| 1 | 事务可提交 | 可 COMMIT / ROLLBACK |
| -1 | 事务已损坏 | 只能 ROLLBACK |
| 0 | 无事务 | 禁止操作 |
规范写法
sql
IF @TranStarted = 1 AND XACT_STATE() <> 0 ROLLBACK;
六、错误日志处理规范
统一错误记录过程(示意)
sql
CREATE PROCEDURE dbo.proc_error_log
AS
BEGIN
INSERT INTO sys_error_log (
error_number,
error_message,
error_procedure,
error_line,
create_time
)
VALUES (
ERROR_NUMBER(),
ERROR_MESSAGE(),
ERROR_PROCEDURE(),
ERROR_LINE(),
GETDATE()
);
END
设计原则
-
❌ 禁止在业务过程里散写 ERROR_*()
-
✅ 所有异常集中入口
-
✅ 支持后续告警、审计、分析
七、明确禁止的写法(红线)
❌ 在子存储过程中无条件 COMMIT
❌ CATCH 中继续执行业务 SQL
❌ 使用 @@ERROR
❌ 混用 RAISERROR 和 THROW
❌ 不区分接口型 / 业务型存储过程
八、工程级总结
一条铁律:谁开启事务,谁才有资格提交或回滚事务。