You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)完全没必要,只会阻塞当前线程/进程,对防止重复发送没有帮助。

解决办法

  1. 确保调度器只启动一次
    可以用文件锁或者判断主进程的方式,避免多进程重复创建调度器:

    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()
    
  2. 给数据库操作加锁,避免竞态条件
    使用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)
    
  3. 修复异常处理
    不要用except:捕获所有异常,改为捕获具体的Exception类型,并且正确调用错误通知函数。

  4. 移除无用的time.sleep
    time.sleep(100)对防止重复发送没有任何帮助,反而会占用资源,直接删掉。


内容的提问来源于stack exchange,提问作者نور محمد

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 01:27:18