后台任务队列的核心价值在于把耗时操作从请求链路中剥离,让接口快速响应,同时把计算压力交给独立进程处理。Resque 是 Ruby 社区广泛使用的队列框架,它只依赖 Redis 作为调度中枢,不引入额外的消息中间件。任务入队时,Resque 会把任务信息序列化后推入 Redis 的列表结构;Worker 进程则通过阻塞读取等待任务到来。这个过程天然支持多 Worker 并发消费,也在一定程度上解耦了生产者与消费者。本文围绕任务生命周期、高可用设计、失败恢复和部署实践展开。

Redis 数据结构与任务生命周期
Resque 在 Redis 中默认使用列表来维护队列。当调用 Resque.enqueue 时,Ruby 会通过 MultiJson 将任务序列化为一个包含类名、参数、队列名的 JSON 字符串,然后执行 LPUSH 写入 resque:queue:队列名 这个键。列表左侧写入、右侧取出,因此队列遵循 FIFO 顺序。Worker 启动后会根据 QUEUE 环境变量确定监听哪些队列,并使用 BRPOP 阻塞式读取任务。BRPOP 的超时时间通常设置为 0,表示无限等待,这样可以避免 Worker 在没有任务时空转。
Resque 的任务执行过程还包括 resque:worker 状态键、resque:failed 失败列表以及任务处理中的临时键。Worker 会定期更新自己的心跳时间,并把当前正在执行的任务写入 resque:working 相关结构。当 Worker 崩溃或被 kill 时,父进程可以依据心跳信息判定超时,重新把未完成的任务放回队列。这个设计让任务不会因为单点进程异常而丢失,但前提是 Redis 本身没有宕机且任务具备幂等性。
值得注意,Resque 使用 Redis 原子操作完成出队,不会出现两个 Worker 同时拿到同一个任务的问题。LPOP 或 BRPOP 都具备原子性,因此不需要额外的分布式锁。对于需要严格顺序的任务,可以把它们放入同一个队列,由单个 Worker 消费;如果不需要顺序,可以增加 Worker 数量并让多个队列并行处理。队列优先级可以通过调整 Worker 启动时的队列顺序实现,排在前面的队列会优先被消费。
Worker 容错与 Redis 高可用保障
Resque Worker 默认使用 fork 子进程来执行任务。每个任务执行前,Worker 主进程 fork 出一个子进程,子进程运行 Job 代码,父进程监控子进程退出状态。这样即使任务代码触发未捕获异常或内存泄漏,也只会影响当前子进程,不会拖垮整个 Worker。父进程捕获到失败后会记录错误、退出码和堆栈到失败队列,并继续处理下一个任务。为了避免僵尸进程,父进程需要通过 Process.wait 回收子进程。
高可用的另一个关键是 Redis。单机 Redis 一旦宕机,整个队列系统就会停摆。生产环境建议至少使用 Redis 主从加哨兵。Sentinel 会监控主节点,当主节点不可达时自动执行故障转移,把从节点提升为主节点。Resque 客户端通过 Sentinel 感知新主节点,避免手工修改连接地址。配置上可以使用 Redis.new 传入哨兵地址列表,并设置 role: :master。如果对数据可靠性要求更高,可以开启 AOF 持久化,并设置 appendfsync everysec,在主从切换时不至于丢失最近一秒的任务数据。
此外,Worker 心跳间隔和超时阈值需要与 Redis Sentinel 的 down-after-milliseconds 保持协调。假如 Worker 心跳是 5 秒,而 Sentinel 在 3 秒内判定主节点不可达并切换,此时 Worker 可能正在从旧主节点读取任务,会出现连接重置错误。给 Resque 客户端配置重试逻辑,并让 Worker 在 Redis 连接失败时暂停拉取而不是直接退出,是提升整体稳定性的一个细节。
失败重试、幂等与监控告警
任务失败后不能一丢了之。Resque 默认把失败任务写入 resque:failed 列表,可以在 Web 界面查看异常信息。更实用的做法是引入 resque-retry 插件,在 Job 类上声明重试次数、延迟时间和捕获异常类型。重试逻辑会在原任务失败后不立即入队,而是等待延迟时间再重新入队,避免瞬时故障导致连续失败。延迟队列可以由 Resque Scheduler 支持,它通过排序集合存放延迟任务,到时间后移动到正式队列。
重试并不能解决所有问题,尤其是任务执行了一半产生的副作用。邮件发送、扣减库存、报表生成等操作必须设计成幂等,或者至少能安全重复执行。例如邮件发送任务可以在业务表记录 message_id,发送前检查是否已经成功;库存扣减使用 UPDATE ... WHERE stock >= ? 的原子条件更新,而不是先查再改。幂等性保证了 Worker 崩溃、Redis 主从切换、人工重放等场景下不会产生重复副作用。这个设计比单纯依赖 Resque 的失败重试更重要。
监控是发现瓶颈和故障的第一道防线。可以通过 Resque Web 查看各队列长度、Worker 状态、失败任务数量和任务耗时。生产环境建议把关键指标接入 Prometheus 或类似系统,监控队列积压、任务失败率、Worker 心跳丢失次数。当队列长度超过阈值时告警,可以防止任务堆积导致服务雪崩。Redis 的 LLEN 命令可以定期采样,也可以用 Resque 提供的 API 直接读取队列大小。告警策略应与业务容忍度匹配,例如邮件队列允许 5 分钟延迟,但支付回调队列必须控制在 30 秒以内。
多 Worker 部署与配置示例
实际部署时,一个 Worker 进程通常绑定一个或多个队列。低峰期可以只启动 3 个 Worker 处理日志、邮件和报表,高峰期通过容器编排水平扩展 Worker 数量。Worker 是无状态的,因此可以安全地增加或减少。队列命名建议按业务域拆分,例如 order_callbacks、mailers、image_compress,而不是把所有任务都塞进 default。拆分后可以针对不同队列设置不同的 Worker 数量和失败策略。
下面是一个典型的 Ruby Job 定义与入队调用:
class ImageCompressJob
@queue = :image_compress
def self.perform(user_id, file_path)
user = User.find(user_id)
image = ImageProcessor.new(file_path).compress
user.update!(compressed_url: image.url)
rescue StandardError => e
Rails.logger.error("图片压缩失败 user_id=#{user_id} file=#{file_path}: #{e.message}")
raise e
end
end
启动 Worker 时,需要指定监听的队列顺序。排在前面的队列优先处理,适合把重要业务队列放在前面:
QUEUE=order_callbacks,mailers,image_compress bundle exec rake resque:work
如果使用 Resque Scheduler,需要单独启动调度进程,并定义定时任务:
require 'resque/scheduler'
require 'resque/scheduler/server'
Resque.schedule = YAML.load_file(Rails.root.join('config', 'resque_schedule.yml'))
daily_report: every: 1d class: DailyReportJob queue: reports args: []
上面的 YAML 片段展示了每天执行一次报表任务。为了避免调度进程单点,可以启动多个调度进程,Resque Scheduler 内部通过 Redis 锁保证同一时刻只有一个调度器在生效。Worker 和 Scheduler 都可以使用 systemd 或容器的健康检查来守护。
配置 Redis 连接时,建议使用环境变量,并为不同环境设置独立命名空间,避免开发和测试数据相互污染:
Resque.redis = Redis.new(
host: ENV['REDIS_HOST'] || '127.0.0.1',
port: ENV['REDIS_PORT'] || 6379,
password: ENV['REDIS_PASSWORD'],
db: 0
)
Resque.redis.namespace = "resque:#{Rails.env}"
当接入 Redis Sentinel 时,连接方式略有不同:
SENTINELS = [
{ host: '10.0.0.11', port: 26379 },
{ host: '10.0.0.12', port: 26379 },
{ host: '10.0.0.13', port: 26379 }
].freeze
Resque.redis = Redis.new(
url: 'redis://mymaster',
sentinels: SENTINELS,
role: :master
)
高可用部署还需要关注 Redis 配置,例如合理设置 maxmemory-policy。任务队列不应该因为内存不足而淘汰键,建议使用 noeviction 策略并配合监控。如果 Redis 只用于队列,可以关闭 RDB 快照或降低频率,减少 fork 开销;但 AOF 建议开启,保证故障恢复时数据尽量完整。最终目标是让任务入队、执行、失败重试、故障恢复形成一个可观测的闭环。