基于Rails+Active Job+Sidekiq+Clockwork+Heroku的任务重复执行排查
定时任务重复执行的可能原因及排查方案
1. 任务幂等性缺失引发竞态重复处理
你的SendInvoiceOverdueRemindersJob逻辑是先查询overdue_reminder_sent: false的发票,再逐个标记为已处理并保存。这种模式存在竞态条件:如果同一任务被多次入队并同时执行,多个进程会同时查询到未标记的发票,进而重复发送邮件。
解决/排查方法:
- 给查询添加行级锁,确保同一时间只有一个进程能处理某张发票:
overdue_invoices = Invoice.overdue.where(overdue_reminder_sent: false).lock(true) - 改用批量原子更新替代逐个保存,避免中间状态的竞态:
# 先筛选符合条件的发票ID eligible_invoice_ids = Invoice.overdue.where(overdue_reminder_sent: false).select { |inv| (Date.today - inv.due_date.to_date).to_i > DAYS_OVERDUE_TRIGGER }.pluck(:id) # 批量标记为已处理 Invoice.where(id: eligible_invoice_ids).update_all(overdue_reminder_sent: true) # 再基于已标记的ID处理邮件发送
2. Clockwork进程重启触发额外执行
Heroku dyno会定期自动重启(每日至少一次),如果Clockwork进程重启时,当天的定时任务时间已过,部分版本的Clockwork会立即触发一次任务,导致当天执行两次。另外需确认是否真的只有一个Clockwork进程在运行(虽heroku ps显示1个,但可能存在进程异常重启后残留的调度)。
解决/排查方法:
- 查看Heroku日志中
SendInvoiceOverdueRemindersJob.perform_later的触发记录,确认是否有多次调度日志,判断是Clockwork重复入队还是Sidekiq重复执行。 - 在Clockwork中添加当日任务执行的幂等校验,比如用Redis记录执行状态:
every(1.day, "SendInvoiceOverdueRemindersJob", at: '08:00') do key = "send_invoice_overdue_reminders_#{Date.today}" unless Redis.current.get(key) SendInvoiceOverdueRemindersJob.perform_later Redis.current.setex(key, 86400, "done") end end
3. Sidekiq重试机制导致邮件任务重复发送
从提供的Action Mailer异常日志来看,邮件发送任务曾抛出异常,而Sidekiq默认会自动重试失败任务。如果主任务没有正确标记发票的overdue_reminder_sent状态,重试的邮件任务会再次发送提醒。
解决/排查方法:
- 查看完整异常日志,定位邮件发送失败的具体原因并修复,避免触发重试。
- 给邮件任务添加幂等校验,比如发送前再次检查发票的
overdue_reminder_sent状态,或使用Sidekiq的唯一任务特性避免重复执行。
4. 旧Heroku Scheduler任务残留
虽已移除Heroku Scheduler插件,但可能之前配置的定时任务未完全删除,导致旧任务仍在触发,与Clockwork的任务重复执行。
解决/排查方法:
- 登录Heroku Dashboard,进入应用的"Scheduler"页面,确认所有旧任务已删除。
- 查看Heroku日志中任务触发的来源,区分是来自
clockwork进程还是旧Scheduler任务。
5. 时区配置不一致
Clockwork设置了America/Chicago时区,但Heroku系统默认使用UTC时区,需确认Clockwork的时区配置是否生效,是否因时区计算错误导致任务在一天内被多次触发。
解决/排查方法:
- 在Clockwork中添加日志,输出任务触发的时间及时区,确认是否符合预期:
every(1.day, "SendInvoiceOverdueRemindersJob", at: '08:00') do Rails.logger.info "Triggering SendInvoiceOverdueRemindersJob at #{Time.now} (timezone: #{Time.now.zone})" SendInvoiceOverdueRemindersJob.perform_later end - 检查Heroku应用的时区环境变量,确保未覆盖Clockwork的配置。
内容的提问来源于stack exchange,提问作者karns
相关产品推荐
相关产品推荐

