You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Sidekiq首次执行无法找到ID问题排查与解决需求

问题:Sidekiq订阅者首次接收:message_created事件时无法找到对应记录

问题场景

现有代码中,ChatRoom类的add_message方法创建Message后立即发布:message_created事件,但Sidekiq订阅者Messages::Notify首次接收事件时,无法通过传入的message_id找到对应记录;任务重试时则能正常工作。即使给Message的create!单独包裹事务,问题依然存在。由于发布事件需要ChatRoom类中的额外信息,无法将发布操作移到Message类的after_commit回调中。

相关代码:

class ChatRoom < ApplicationRecord
  #... 
  def add_message(args)
    #...

    message = self.messages.create!(
      #...
    )

    publish(:message_created,
      message_id: message.id,
      notify: notify
    )
  end
end


class Messages::Notify < ApplicationSubscriber
  include Wisper::Publisher
  def message_created(args)
    internal_note = InternalNote.find(args[:message_id])
    #...
  end
end

尝试过的无效方案:

class Message < ApplicationRecord
  def create!(args)
    self.transaction do
      super(args)
    end
  end
end

原因分析

这是典型的事务未提交就触发异步任务的数据库隔离问题:

  1. 即使Message.create!本身执行完成,但如果ChatRoom#add_message被包裹在某个外层事务(比如调用add_message的代码处于ChatRoom的事务操作中)里,此时Message的记录仅在当前数据库连接可见,Sidekiq工作进程使用独立连接,在事务提交前无法读取到这条未提交的记录。
  2. 给Message.create!单独加事务无济于事——内层事务提交后,外层事务未结束的情况下,其他连接依然看不到这条记录,数据库默认的READ COMMITTED隔离级别会限制未提交事务的可见性。

解决方案

方案1:利用ChatRoom的事务回调延迟发布事件

将事件发布逻辑放到ChatRoom的after_commit回调中,确保在整个事务(包括ChatRoom和关联Message的操作)提交后再触发事件,此时记录对Sidekiq进程完全可见:

class ChatRoom < ApplicationRecord
  #... 
  def add_message(args)
    #...

    message = self.messages.create!(
      #...
    )

    # 暂存发布参数,等待当前ChatRoom的事务提交后执行发布
    after_commit do
      publish(:message_created,
        message_id: message.id,
        notify: notify
      )
    end
  end
end

这里的after_commit是ActiveRecord的实例方法,会绑定到当前ChatRoom实例的事务生命周期,确保所有关联操作的事务都提交后再发布事件。

方案2:主动在订阅者中添加存在性检查与短延迟重试

如果无法修改发布时机,可以在订阅者中主动检查记录是否存在,通过短时间重试避免事务未提交的问题:

class Messages::Notify < ApplicationSubscriber
  include Wisper::Publisher
  def message_created(args)
    internal_note = nil
    # 最多重试3次,每次间隔0.5秒
    3.times do
      internal_note = InternalNote.find_by(id: args[:message_id])
      break if internal_note
      sleep 0.5
    end

    # 多次重试后仍找不到则抛出异常,交给Sidekiq自动重试
    raise ActiveRecord::RecordNotFound, "InternalNote with id #{args[:message_id]} not found" unless internal_note
    
    #... 后续业务逻辑
  end
end

这种方法属于临时缓解,适合快速降低错误日志量,但不如方案1从根源解决问题。

方案3:配置Wisper与Sidekiq的适配机制

如果使用wisper-sidekiq适配器,可以结合其提供的事务后触发能力,确保事件在事务提交后再被Sidekiq入队。例如可以自定义发布方法,将事件入队逻辑绑定到事务回调中。

内容的提问来源于stack exchange,提问作者Toma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 00:45:52