PAYATHON 2026

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 中复制:

  1. 完整处理调用结果和异常,不能在收到结果集时就判定业务成功。
  2. 在业务事务边界结束后检查事务状态;调用失败或发现遗留事务时,按既定事务管理方式回滚。
  3. 清理失败、连接失效或状态无法确认时,通过连接池提供的机制淘汰该物理连接。
  4. 记录调用入口、请求标识、错误链及清理结果,再结束请求。

清理必须针对原来的物理连接。在另一条连接上执行 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 执行异常,以及客户端超时或取消。
  • 回滚失败后,物理连接是否确实被淘汰。

每次都要检查同一物理连接的事务状态,并确认它再次借出时没有携带上一次请求的事务。验证必须覆盖连接复用路径,才能确认这类错误不会继续影响后续请求。

备注:内容仅供参考。