酷酷的马 2025-05-19 13:52 采纳率: 0%
浏览 9

Bug 报告:事务回滚未按预期触发(COMMIT 而非 ROLLBACK)

1. 问题描述

在使用 activerecord-sqlserver-adapter 时,事务块内抛出 ActiveRecord::Rollback 异常或直接触发异常后,事务未正确回滚,而是被提交(日志显示 COMMIT TRANSACTION)。此行为与预期不符,导致数据一致性问题。


2. 重现步骤

  1. 使用以下代码逻辑执行事务:
    ActiveRecord::Base.transaction do
      Contract.create!(result) # 假设 result 是有效数据
      AnotherModel.create!(another_result) # 假设 another_result 是有效数据
      raise ActiveRecord::Rollback # 或 raise "Test error"
    end
    
  2. 观察日志输出:
    TRANSACTION (2.3ms)  COMMIT TRANSACTION
    => nil
    
  3. 检查数据库表中的数据是否已被提交(而非回滚)。

3. 预期行为

  • 当事务块内抛出 ActiveRecord::Rollback 或非 ActiveRecord::Rollback 异常时,事务应被回滚(日志显示 ROLLBACK TRANSACTION),且数据库中的数据应无变化。

4. 实际行为

  • 事务被提交(日志显示 COMMIT TRANSACTION),数据被持久化到数据库,未回滚。

5. 环境信息

项目详细信息
activerecord-sqlserver-adapter version7.0.7
Rails version7.0.8.6
Ruby version3.3.3
SQL Server version2014
Database Configuration
数据库连接配置
production:
  sqlserver:
    adapter: sqlserver
    database: my_databse
    username: user
    password: psword!
    host: 192.168.28.102
    port: 1433
    timeout: 5000
    options: "XACT_ABORT=true" # Setting or not setting it here has no effect

6. 日志输出

请提供完整的日志片段(脱敏敏感信息):

[事务开始前的日志]
TRANSACTION (1.7ms)  BEGIN TRANSACTION
[数据库操作日志]
TRANSACTION (2.3ms)  COMMIT TRANSACTION

7. 已尝试的解决方案

  • ✅ 使用 ActiveRecord::Base.transaction 替代手动调用 begin_transaction
  • ✅ 在事务块内抛出 ActiveRecord::Rollback
  • ✅ 在 database.yml 中添加 options: "XACT_ABORT=true"
  • ✅ 升级 activerecord-sqlserver-adapter 到最新版本(运行 bundle update activerecord-sqlserver-adapter)。

8. 附加信息

  • 是否涉及嵌套事务?
    (例如:是否在事务块内调用了其他事务方法?)
  • 是否使用了自定义的 after_commit/after_rollback 回调?
    (这些回调可能隐式提交事务。)
  • 是否在事务块外捕获了异常?
    (例如:rescue 吞掉了异常,导致未重新抛出。)

请求帮助

请社区协助确认以下问题:

  1. 是否是 activerecord-sqlserver-adapter 的已知问题?
  2. 是否与 SQL Server 的 XACT_ABORT 配置相关?
  3. 是否需要进一步调试底层事务管理逻辑?
  • 写回答

4条回答 默认 最新

  • 檀越@新空间 2025-05-19 13:53
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    问题分析

    从问题描述来看,事务未按预期回滚,而是提交了(COMMIT TRANSACTION),这表明事务管理逻辑存在问题。以下是可能的原因和排查方向:

    1. ActiveRecord::Rollback 的处理
      ActiveRecord::Rollback 是一种特殊的异常,用于显式触发事务回滚。如果事务块内部抛出了 ActiveRecord::Rollback,理论上事务应该被回滚。但如果事务未回滚,可能是底层适配器或数据库驱动没有正确处理这种异常。

    2. SQL Server 的事务行为
      SQL Server 的事务行为受 XACT_ABORT 参数的影响。如果 XACT_ABORT 设置为 ON,任何错误都会导致事务立即回滚;如果设置为 OFF,错误可能导致事务继续提交。虽然你在 database.yml 中设置了 options: "XACT_ABORT=true",但实际效果可能未生效。

    3. 适配器版本兼容性
      activerecord-sqlserver-adapter 的某些版本可能存在 bug,特别是在处理事务回滚时。即使升级到最新版本,也可能存在未修复的问题。

    4. 异常捕获和重新抛出
      如果事务块外部捕获了异常但未重新抛出,可能会导致事务意外提交。此外,如果事务块内有其他逻辑隐式提交事务(如回调或手动提交),也会导致类似问题。


    解决方案

    以下是逐步解决问题的方法,按照优先级排序:

    1. 确认 XACT_ABORT 是否生效

    确保 SQL Server 的 XACT_ABORT 参数在连接中生效。可以通过以下方式验证:

    • 在 SQL Server Management Studio 中执行以下命令,检查当前会话的 XACT_ABORT 设置:

      SET XACT_ABORT ON;
      SELECT @@OPTIONS;
      

      如果返回值中包含 ANSI_DEFAULTS(通常为 65536),则说明 XACT_ABORT 已启用。

    • database.yml 中明确设置 XACT_ABORT,并重启应用服务器验证:

      production:
        sqlserver:
          adapter: sqlserver
          database: my_database
          username: user
          password: password!
          host: 192.168.28.102
          port: 1433
          timeout: 5000
          options: "SET XACT_ABORT ON;SET NOCOUNT ON;"
      

    2. 捕获异常并重新抛出

    确保事务块外部没有捕获异常并吞掉。例如:

    begin
      ActiveRecord::Base.transaction do
        Contract.create!(result)
        AnotherModel.create!(another_result)
        raise ActiveRecord::Rollback # 显式触发回滚
      end
    rescue => e
      # 不要直接 rescue,而是重新抛出
      raise e
    end
    

    如果事务块外部捕获了异常但未重新抛出,会导致事务意外提交。

    3. 检查回调逻辑

    检查是否有自定义的 after_commitafter_rollback 回调,这些回调可能会隐式提交事务。例如:

    after_commit :do_something
    def do_something
      # 如果这里提交了事务,会导致意外行为
    end
    

    4. 调试底层事务管理逻辑

    如果上述方法无效,可以深入调试底层事务管理逻辑。以下是调试建议:

    • 打印 SQL Server 的完整日志,观察事务的实际状态。
    • 在事务块内部插入调试语句,确认 ActiveRecord::Rollback 是否被正确捕获。

    5. 升级或更换适配器

    如果问题仍未解决,可以尝试以下方法:

    • 升级 activerecord-sqlserver-adapter 到最新版本。
    • 考虑使用其他数据库适配器(如 tiny_tdsmssql)。

    示例代码

    以下是修正后的事务逻辑示例:

    # 确保事务块内部抛出异常并被捕获
    begin
      ActiveRecord::Base.transaction do
        Contract.create!(result)
        AnotherModel.create!(another_result)
        raise ActiveRecord::Rollback # 显式触发回滚
      end
    rescue => e
      puts "Transaction rolled back due to error: #{e.message}"
      raise e # 重新抛出异常,确保事务被回滚
    end
    
    # 检查回调逻辑
    class AnotherModel < ApplicationRecord
      after_commit :log_commit
    
      private
    
      def log_commit
        # 确保这里不会提交事务
        puts "Commit detected for #{self.class.name}"
      end
    end
    

    总结

    通过以上步骤,可以逐步排查和解决事务未回滚的问题。关键点在于:

    1. 确保 XACT_ABORT 参数生效。
    2. 捕获异常并重新抛出。
    3. 检查回调逻辑,避免隐式提交。
    4. 如果问题仍然存在,考虑升级或更换适配器。

    如果问题仍未解决,请提供更详细的日志和代码片段,以便进一步分析。

    评论

报告相同问题?

问题事件

  • 创建了问题 5月19日