Django+APScheduler部署Gunicorn多Worker时任务重复执行问题求助
解决Django+Gunicorn多Worker下APScheduler任务重复执行的问题
问题根源
Gunicorn的每个Worker都是独立进程,会各自触发Django AppConfig的ready()方法,导致每个Worker都初始化并启动APScheduler,最终任务被多次执行。
可行解决方案
方案1:TCP Socket绑定互斥(你提到的方案)
利用同一TCP端口只能被单个进程绑定的特性,让只有第一个成功绑定端口的Worker启动调度器,其他Worker绑定失败则跳过。
修改你的调度模块代码:
from apscheduler.schedulers.background import BackgroundScheduler from RENDER.views import dailypuzzlefunc import socket def start(): # 选择一个本地未被占用的端口(比如9001,可自行修改) lock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # 设置SO_REUSEADDR避免进程退出后端口处于TIME_WAIT状态无法立即复用 lock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定本地端口,只有第一个进程能成功 lock_socket.bind(('127.0.0.1', 9001)) # 绑定成功,启动调度器 scheduler = BackgroundScheduler() scheduler.add_job(dailypuzzlefunc, 'cron', day="*", max_instances=2, id='dailyscheduler') scheduler.start() except socket.error: # 端口已被绑定,说明已有Worker启动了调度器,当前Worker跳过 pass finally: # 不需要保持连接,关闭socket lock_socket.close()
方案2:文件锁互斥
通过文件锁实现进程间互斥,只有获取到锁的Worker才启动调度器,无需占用端口。
修改调度模块代码:
from apscheduler.schedulers.background import BackgroundScheduler from RENDER.views import dailypuzzlefunc import fcntl import os def start(): # 定义锁文件路径(建议放在项目根目录或者临时目录) lock_file_path = '/tmp/django_apscheduler_lock' lock_file = open(lock_file_path, 'w') try: # 获取排他锁,非阻塞模式 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) # 获取锁成功,启动调度器 scheduler = BackgroundScheduler() scheduler.add_job(dailypuzzlefunc, 'cron', day="*", max_instances=2, id='dailyscheduler') scheduler.start() except BlockingIOError: # 锁已被其他进程持有,当前Worker跳过 pass # 不需要手动释放锁,进程退出后锁会自动释放
方案3:基于Django ORM的数据库互斥锁
利用数据库的唯一性约束,创建一个调度器状态表,确保只有一个Worker能初始化调度器,适合依赖数据库的场景。
- 先在你的APP下创建模型:
from django.db import models class SchedulerLock(models.Model): # 唯一标识锁的字段,确保只有一条记录 lock_key = models.CharField(max_length=50, unique=True, default='apscheduler_lock') created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "调度器锁" verbose_name_plural = "调度器锁"
执行迁移
python manage.py makemigrations和python manage.py migrate创建表。修改调度模块代码:
from apscheduler.schedulers.background import BackgroundScheduler from RENDER.views import dailypuzzlefunc from YOUR_APP.models import SchedulerLock from django.db import IntegrityError def start(): try: # 尝试创建锁记录,唯一约束会确保只有第一个进程能成功 SchedulerLock.objects.create() # 创建成功,启动调度器 scheduler = BackgroundScheduler() scheduler.add_job(dailypuzzlefunc, 'cron', day="*", max_instances=2, id='dailyscheduler') scheduler.start() except IntegrityError: # 记录已存在,说明已有Worker启动调度器,当前Worker跳过 pass
注意事项
- 所有方案都不需要修改Gunicorn配置,保持多Worker模式即可,代码变更后也能正常重载(不需要
--preload)。 - 如果使用Socket或文件锁,需要确保部署环境的端口/文件路径没有被其他服务占用,且进程有读写权限。
- 数据库锁方案依赖Django ORM,需要确保数据库连接正常,且迁移已执行。
内容的提问来源于stack exchange,提问作者ControltheAI
相关产品推荐
相关产品推荐

