复制Ruby on Rails容器时Sidekiq会重复运行吗?如何避免定时任务重复执行
Sidekiq定时任务多容器部署问题解答
多容器实例会导致重复执行定时任务吗?
会的。每个Rails应用容器启动时都会注册Sidekiq定时任务,要是你部署多个实例,每个实例里的Sidekiq调度器(比如用sidekiq-scheduler的场景)都会独立触发任务,同一个定时任务就会被多次执行。
怎么让任务只在单个容器执行?
推荐两种靠谱的解决办法,结合你提供的Docker Compose配置来调整:
方法一:拆分独立的Sidekiq调度器容器
专门创建一个只负责运行Sidekiq调度器的容器,其他应用容器只处理Web请求或者常规Sidekiq任务,不启动调度功能。
修改你的docker-compose.yml,新增两个Sidekiq相关服务(一个处理常规任务,一个负责调度):
version: '3.8' services: thedatabase: image: postgres:12 volumes: - ./database:/var/lib/postgresql/data environment: POSTGRES_USER: theuser POSTGRES_PASSWORD: thepass networks: - excnetwork theapi: build: context: ./dex-api dockerfile: Dockerfile volumes: - ./dex-api:/web env_file: - ./dex-api/.env ports: - "8081:3000" depends_on: - thedatabase - redis networks: - excnetwork # 让web容器只跑Rails服务器,不启动Sidekiq command: bundle exec rails server -b 0.0.0.0 -p 3000 sidekiq-worker: build: context: ./dex-api dockerfile: Dockerfile volumes: - ./dex-api:/web env_file: - ./dex-api/.env depends_on: - thedatabase - redis networks: - excnetwork # 只运行常规Sidekiq任务处理器,不启动调度器 command: bundle exec sidekiq -C config/sidekiq.yml sidekiq-scheduler: build: context: ./dex-api dockerfile: Dockerfile volumes: - ./dex-api:/web env_file: - ./dex-api/.env # 强制只跑一个实例,避免重复调度 deploy: replicas: 1 depends_on: - thedatabase - redis networks: - excnetwork # 启动Sidekiq调度器,负责定时任务的触发 command: bundle exec sidekiq -C config/sidekiq.yml # 添加环境变量,让代码只在这个容器启动调度器 environment: - ENABLE_SCHEDULER=true redis: image: redis command: 'redis-server --requirepass "yourpassword"' volumes: - ./redis:/var/lib/redis/data networks: - excnetwork volumes: database: dex-api: redis: networks: excnetwork: driver: bridge
然后在Rails的Sidekiq初始化代码里,通过环境变量控制调度器是否启用:
# config/initializers/sidekiq.rb if ENV['ENABLE_SCHEDULER'] == 'true' require 'sidekiq-scheduler' Sidekiq::Scheduler.enabled = true else Sidekiq::Scheduler.enabled = false end
这样只有sidekiq-scheduler容器会触发定时任务,其他实例不会,彻底解决重复执行问题。
方法二:用Redis分布式锁控制任务执行
如果不想拆分容器,可以给定时任务加个分布式锁,只有抢到锁的实例才会执行任务。
示例代码如下:
class MyScheduledJob include Sidekiq::Job def perform # 生成唯一锁键,设置过期时间(要比任务最长执行时间长) lock_key = "scheduled_job:#{self.class.name}" # 只有当锁不存在时才设置成功,返回true acquired_lock = Sidekiq.redis { |conn| conn.set(lock_key, "locked", nx: true, ex: 3600) } return unless acquired_lock begin # 这里写你的任务逻辑 ensure # 任务执行完不管成功失败都释放锁 Sidekiq.redis { |conn| conn.del(lock_key) } end end end
这种方式的缺点是如果任务执行时间超过锁的过期时间,可能会导致重复执行,所以要根据任务实际情况调整过期时间。
总结
优先选方法一,拆分容器的方式架构更清晰,维护起来也更简单,从根源上避免了重复执行的问题。
内容的提问来源于stack exchange,提问作者user1034754
相关产品推荐
相关产品推荐

