Django中Crontab定时任务重复发送通知问题排查
问题原因及解决办法
核心问题1:多进程环境下重复启动调度器
你的start()函数在Django启动时会被执行,如果部署用了多进程(比如Gunicorn的--workers参数设为大于1,或者uWSGI多进程),每个worker进程都会创建一个BackgroundScheduler实例,到9点时多个调度器同时触发send_statistics任务,自然会发送多条通知。
核心问题2:数据库操作存在竞态条件
即使只有一个调度器,当任务被意外重复触发时,多个线程/进程同时执行send_statistics:
- 进程A查询到
stat.is_send为False,准备发送消息 - 进程B在进程A还没修改并保存
is_send状态前,也查询到stat.is_send为False,同样发送消息 - 最后两个进程都把
is_send改成True,但消息已经发了两次
其他潜在问题
- 异常处理过于宽泛:
except:会捕获所有异常,而且出错后只是写了send_message_telegram(没有调用,应该传入错误信息执行函数),导致错误无法被感知,甚至可能让任务重复执行。 time.sleep(100)完全没必要,只会阻塞当前线程/进程,对防止重复发送没有帮助。
解决办法
确保调度器只启动一次
可以用文件锁或者判断主进程的方式,避免多进程重复创建调度器:import os from apscheduler.schedulers.background import BackgroundScheduler def start(): # 仅在Django主进程启动调度器 if os.environ.get('RUN_MAIN') == 'true': scheduler = BackgroundScheduler({'apscheduler.timezone': 'Asia/Tashkent'}) if not scheduler.get_jobs(): scheduler.add_job(send_statistics, 'cron', hour=9) scheduler.start()给数据库操作加锁,避免竞态条件
使用Django的select_for_update()获取排他锁,确保同一时间只有一个进程能修改数据:from django.db import transaction def send_statistics(): today = date.today() yesterday = today - timedelta(days=1) try: with transaction.atomic(): # 加锁防止其他进程同时读取 stat, created = StatisticModel.objects.select_for_update().get_or_create( date=yesterday, defaults={'is_send': False} ) if not stat.is_send: text = f"<b>*******HISOBOT*******</b>\n" if created else "some text" send_message_telegram(text, SEND_STATISTIC_GROUP_ID) stat.is_send = True stat.save() except Exception as e: # 捕获异常并发送错误通知 send_message_telegram(f"发送统计消息出错:{str(e)}", SEND_STATISTIC_GROUP_ID)修复异常处理
不要用except:捕获所有异常,改为捕获具体的Exception类型,并且正确调用错误通知函数。移除无用的time.sleep
time.sleep(100)对防止重复发送没有任何帮助,反而会占用资源,直接删掉。
内容的提问来源于stack exchange,提问作者نور محمد
相关产品推荐
相关产品推荐

