Ruby SolidQueue 支持防抖式的 Job 么?

qichunren · October 14, 2025 · Last by laocainiao replied at October 16, 2025 · 429 hits

我的需求场景是这样的,在后台用户可以添加/编辑某个 Post 记录,在 after_commit 后,需要执行一个比较耗时的基于当前表的所有记录的异步缓存任务 PopulateGlobalPostsCacheJob。如果用户在一条条操作记录,这样会依次创建多任务在执行,而实际上只有最后一个任务是有效的。

我想到的办法就是在模型的 after_commit 中,插入一个在 2 分钟后执行的异步任务。如果这时发现有等待中的任务,就取消之前的这个任务。

要是 SolidQueue 能支持这个用法就好了。

没用过,但是扫了一眼文档

https://github.com/rails/solid_queue?tab=readme-ov-file#concurrency-controls

这里的 on_conflict 似乎能支持这个用法

Reply to coderliu

on_conflict 支持的:discard 参数是将后面的入队的 Job 忽略掉,不是这个使用场景。

on_conflict 支持的:discard 是用在这样的场景:例如 在进行全站数据库备份等比较耗时的操作时,保证只有一个任务在进行,任务正在执行时,后续同样的任务入队了就忽略掉。

我想到的办法就是在模型的 after_commit 中,插入一个在 2 分钟后执行的异步任务。如果这时发现有等待中的任务,就取消之前的这个任务。

如果 PopulateGlobalPostsCacheJob 的语义符合要求(执行结果只和执行时间相关,和 Job enqueue 时间无关),这种场景 discard 新的异步任务,让等待中的任务正常执行,结果是相同的。

如楼上所说,感觉可以调整任务来解决,比如 PopulateGlobalPostsCacheJob 写入一个 redis key 带过期时间,后续的任务如果发现过期就执行,否则跳过

还有一个思路,不要盯着新的 Job,而是在执行 PopulateGlobalPostsCacheJob 时,利用队列的 API,检查是否有新的 PopulateGlobalPostsCacheJob 在排队,有的话就跳过当前 Job,不再执行。

https://api.rubyonrails.org/v8.0.3/classes/ActiveJob/Callbacks/ClassMethods.html#method-i-before_perform

@coderliu 这个用法感觉是在吊打 那些基于 MQ 或者 Redis 的任务队列,不是数据库还真不好去做这个查询

Reply to ratazzi

不是数据库还真不好去做这个查询

吊打也说不上吧,只要合理控制对应队列的大小,这种查询对队列后端的压力都不大的。

我觉得 SolidQueue 这种基于数据库的其中一个优势就是 任务一但堆积,用数据库的就比较容易清理,而 MQ 就很难

楼主的需求明明是一个很常见的需求。问了一下高人,这样解决:

class Post < ApplicationRecord
  after_commit :debounce_populate_global_posts_cache, on: [:create, :update]

  private

  def debounce_populate_global_posts_cache
    # 从缓存读取上一个任务的 ID
    old_job_id = Rails.cache.read(:global_posts_cache_job_id)

    if old_job_id
      # 找到旧任务并丢弃(如果存在)
      old_job = SolidQueue::Job.find_by(id: old_job_id)
      old_job&.discard # discard 会标记为 discarded,不会执行
    end

    # 调度新任务,延迟 2 分钟执行
    new_job = PopulateGlobalPostsCacheJob.set(wait: 2.minutes).perform_later

    # 将新任务的 provider_job_id 存入缓存(注意:perform_later 返回 ActiveJob 实例)
    Rails.cache.write(:global_posts_cache_job_id, new_job.provider_job_id, expires_in: 3.minutes) # 缓存稍长于延迟时间,避免过期
  end
end
You need to Sign in before reply, if you don't have an account, please Sign up first.