SQL Server Error 266:如何防止存储过程退出时遗留未提交事务?
在调用边界处理遗留事务
SQL Server 2008 R2 没有配置项能保证任意存储过程退出时自动结束遗留事务,也不能将 Error 266 配置为“自动回滚”或“自动提交”。
调用方可以用包住 EXEC 的 TRY/CATCH 捕获 Error 266。对于提前 RETURN 导致事务未关闭的问题,可以增加一层包装过程,统一回滚、记录错误,并向应用报告失败。
连接归还池之前也需要检查和清理,防止遗留事务影响复用该连接的后续请求。数据库包装过程负责正常返回和可捕获的错误,连接管理层负责取消、超时、通信故障等异常路径。
不要用自动 COMMIT 补救。此时无法确认业务操作是否完整,提交可能把只完成一半的数据永久保存下来。
为什么过程内部已有 TRY/CATCH 仍然不够
下面的语句本身不会抛出异常:
IF @UserName = 'Grue' RETURN -100;
如果它出现在 BEGIN TRANSACTION 之后,存储过程会直接返回,后面的 COMMIT 和清理代码都不会执行。T-SQL 的 TRY/CATCH 没有类似 finally 的必经清理块。
存储过程退出时,如果 @@TRANCOUNT 与进入时不同,就会产生 Error 266。此时,造成不匹配的过程已经退出,需要在调用它的外层捕获错误,过程自身的 CATCH 已无法处理。没有外层处理时,出现 Error 266 也不代表遗留事务已经回滚。参见 Error 266 的示例与分析。
事务计数减少也可能产生 Error 266。例如,子过程执行无保存点的 ROLLBACK TRANSACTION,会把调用方的事务一起回滚。因此,266 表示事务边界不匹配,不能直接等同于“漏了一次提交”。
用外层包装过程统一处理
对于可以独立完成的业务操作,可以约定:
- 应用只调用包装过程,进入时没有活动事务。
- 包装过程启动事务,并调用现有业务过程。
- 业务过程返回后,事务计数必须仍为
1,且事务可以提交。 - 遇到错误或约定的失败返回码,包装过程回滚并向应用报错。
下面的示例使用 SQL Server 2008 R2 支持的语法,假设返回码 0 表示成功,其他值表示失败。参数需要按实际过程签名调整。
CREATE PROCEDURE dbo.AddProductToRecentlyViewedSafe
@UserName nvarchar(128),
@ProductID int
AS
BEGIN
SET NOCOUNT ON;
-- 此入口只处理独立业务事务,不接受调用方已有的事务。
IF @@TRANCOUNT <> 0
BEGIN
RAISERROR(
N'This procedure requires no active transaction.',
16, 1
);
RETURN -1;
END;
SET IMPLICIT_TRANSACTIONS OFF;
SET XACT_ABORT ON;
DECLARE @ReturnCode int;
BEGIN TRY
BEGIN TRANSACTION;
EXEC @ReturnCode = dbo.AddProductToRecentlyViewed
@UserName = @UserName,
@ProductID = @ProductID;
IF @@TRANCOUNT <> 1 OR XACT_STATE() <> 1
BEGIN
RAISERROR(
N'Unexpected transaction state after business procedure.',
16, 1
);
END;
IF @ReturnCode <> 0
BEGIN
RAISERROR(
N'Business procedure returned failure code %d.',
16, 1, @ReturnCode
);
END;
COMMIT TRANSACTION;
RETURN 0;
END TRY
BEGIN CATCH
DECLARE @ErrorNumber int;
DECLARE @ErrorProcedure nvarchar(128);
DECLARE @ErrorMessage nvarchar(4000);
SELECT
@ErrorNumber = ERROR_NUMBER(),
@ErrorProcedure =
COALESCE(ERROR_PROCEDURE(), N'(unknown)'),
@ErrorMessage = ERROR_MESSAGE();
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION;
-- 如需写入持久化错误日志,应放在回滚之后。
RAISERROR(
N'AddProductToRecentlyViewed failed. Error %d, procedure %s: %s',
16, 1,
@ErrorNumber,
@ErrorProcedure,
@ErrorMessage
);
RETURN -1;
END CATCH;
END;
如果业务过程执行 BEGIN TRANSACTION 后提前 RETURN,事务计数就会从进入子过程时的 1 变成 2。退出检查产生的 Error 266 会进入包装过程的 CATCH,由包装过程执行完整回滚。TRY/CATCH 文档说明了调用方处理被调用过程错误的规则。
包装过程通过 RAISERROR 字符串消息重新报告捕获到的错误,错误号通常为 50000,原始错误号则保留在消息文本中。SQL Server 2008 R2 不支持 THROW,实际日志应单独保存原始错误信息。
如果需要通过数据库权限强制应用使用包装入口,应只授予应用账号该入口所需的执行权限,避免它通过其他授权直接调用内部过程。具体授权需要结合对象所有者和所有权链检查。
包装过程的适用边界
这个方案能处理提前返回和事务计数不匹配,但不能保证任意业务代码都正确。例如,子过程先错误地提交整个事务,再启动一个新事务,最终计数可能仍然相同。已经提交的数据也无法由外层回滚。
示例会拒绝调用方已有活动事务的情况,不会回滚该事务。入口拒绝调用后,连接管理层仍须处理该连接,不能直接将其放回池中。
配合 XACT_ABORT ON 使用显式回滚
SET XACT_ABORT ON 有助于处理许多运行时错误,但提前 RETURN 属于正常控制流,不会触发事务清理。启用该选项,也不能保证 Error 266 出现时事务一定会自动回滚。
应保留 XACT_ABORT ON,并配合外层 TRY/CATCH 和显式回滚。RAISERROR 本身不遵循 XACT_ABORT,所以示例在重新报错前先执行 ROLLBACK。参见 SET XACT_ABORT 文档。
连接归还池之前,必须确认事务已经结束
统一错误处理需要覆盖 Error 266 之外的异常。其他异常同样可能留下事务,客户端取消和某些连接异常也不会进入普通的 T-SQL CATCH。
清理逻辑可以集中放在事务拦截器、连接包装层或连接池扩展点,无须在每个 DAO 中复制:
- 完整处理调用结果和异常,不能在收到结果集时就判定业务成功。
- 在业务事务边界结束后检查事务状态;调用失败或发现遗留事务时,按既定事务管理方式回滚。
- 清理失败、连接失效或状态无法确认时,通过连接池提供的机制淘汰该物理连接。
- 记录调用入口、请求标识、错误链及清理结果,再结束请求。
清理必须针对原来的物理连接。在另一条连接上执行 ROLLBACK,无法结束问题会话里的事务。
如果事务由 JDBC 或容器管理,应通过对应的事务管理机制完成回滚,避免随意混用 JDBC 事务 API 与 T-SQL 事务控制。微软的 JDBC 事务说明也提醒了混用可能造成的状态问题。
不要把 setAutoCommit(true) 当作清理办法,切换自动提交模式可能提交当前事务。参见 setAutoCommit 文档。
连接池是否会在归还或再次借出连接时自动回滚,取决于驱动、连接池版本和配置,需要用实际组合验证。
不要让高频业务过程顺手提交或回滚
在任意高频过程中加入:
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION;
会把合法的调用方事务也一起回滚。如果问题连接随后没有调用这个过程,清理也不会发生。
改成 COMMIT 更危险。SQL Server 的嵌套事务不能独立提交:内部 COMMIT 通常只将 @@TRANCOUNT 减一,直到最外层提交才真正提交数据。无保存点的 ROLLBACK TRANSACTION 则会回滚整个事务,并把计数归零。参见 ROLLBACK TRANSACTION 文档。
XACT_STATE() 返回的是事务状态,不是事务计数:
| 返回值 | 含义 |
|---|---|
0 | 没有活动用户事务 |
1 | 有活动事务,可以提交 |
-1 | 有活动事务,但只能完整回滚 |
XACT_STATE() = 1 只能说明事务在技术上可以提交,不能说明业务已经完成。函数之间的区别见 XACT_STATE 文档。
遗留事务应在明确不允许存在业务事务的连接边界清理,发现遗留事务时应记录错误。
保存诊断信息并验证故障路径
外层包装过程应在回滚前保存诊断信息,包括 ERROR_NUMBER()、ERROR_PROCEDURE()、ERROR_LINE()、ERROR_MESSAGE()、@@SPID、当前 @@TRANCOUNT 和 XACT_STATE()。错误日志应在回滚后写入,避免随业务事务一起被回滚。
Error 266 可能伴随更早的业务错误出现,应用应保留完整异常链。过程名有助于定位事务计数不匹配的退出边界,但不一定能指出最初出错的代码行。
在 SQL Server 2008 R2 中,还可以配置带筛选条件的 SQL Trace,捕获 Exception 事件中的 Error 266,并关联对应会话的过程调用。持续监控长期未结束的事务,尤其是处于 sleeping 状态却仍持有事务的会话,有助于在阻塞扩大前报警。
上线前,至少用实际 JDBC 驱动和连接池验证以下路径:
BEGIN TRANSACTION后提前RETURN。- 子过程错误地回滚调用方事务。
- SQL 执行异常,以及客户端超时或取消。
- 回滚失败后,物理连接是否确实被淘汰。
每次都要检查同一物理连接的事务状态,并确认它再次借出时没有携带上一次请求的事务。验证必须覆盖连接复用路径,才能确认这类错误不会继续影响后续请求。
备注:内容仅供参考。
