最常见原因是任务被意外调用两次:一次是显式调用apply_async(),另一次是任务函数在模块顶层或初始化逻辑中被直接执行;其次是autodiscover_tasks路径重复注册、django事务未提交导致重复消费、redis连接复用引发写入重复。

为什么 apply_async() 会触发两次任务
最常见原因是任务被意外调用两次:一次是显式调用 apply_async(),另一次是任务函数本身在模块顶层或初始化逻辑中被直接执行。Django 启动时若在 tasks.py 里写了 my_task.delay() 或 my_task(),Celery worker 加载模块就会立刻执行它——这和你后来发的异步调用完全无关,纯属代码位置错误。
另一个高频原因是使用了 autodiscover_tasks 但路径重复注册:比如 INSTALLED_APPS 里同时写了 'myapp' 和 'myapp.tasks',导致同一组任务被加载两遍,apply_async() 发一次,实际进队列两次。
- 检查
tasks.py文件顶部和__init__.py,删掉任何对任务函数的直接调用(my_task()、my_task.delay()) - 确认
celery.py中app.autodiscover_tasks()的参数只传应用名列表,如['myapp', 'otherapp'],不要传模块路径 - 用
celery -A myproject inspect registered查看实际注册的任务列表,如果看到重复的函数签名(如myapp.tasks.my_task出现两次),说明加载冲突
Django数据库事务未提交导致任务重复消费
Celery 默认使用 task_acks_late=True(尤其在 broker_url 是 Redis 时),意味着任务只有在执行完才确认。但如果任务函数里操作了 Django ORM,而外层视图或管理命令没提交事务,worker 进程可能因超时或重启重发任务——因为 broker 认为它“没成功”。更隐蔽的是,Django 的 atomic 块没结束,post_save 信号触发的任务可能在事务回滚后仍留在队列里。
- 在任务函数开头加日志,打印
self.request.id和当前时间,对比是否同一 ID 出现多次 - 避免在事务内调用
.delay();必须这么做时,改用on_commit():from django.db import transaction transaction.on_commit(lambda: my_task.delay())
- 检查
CELERY_TASK_ACKS_LATE和CELERY_WORKER_PREFETCH_MULTIPLIER,高并发下设为1可减少误重发
Redis作为Broker时的连接复用与任务丢失
用 redis:// 作 broker_url 时,Celery 默认复用连接池。但若 worker 进程 fork 后子进程继承了父进程的 Redis 连接句柄,某些 Redis 客户端(如旧版 redis-py)会因连接状态不一致,把一条任务写入队列两次——现象是日志里看到两个相同 task_id 几乎同时开始执行。
- 升级到
redis>=4.5.0并确保celery>=5.2,它们修复了 fork 后连接重置逻辑 - 显式禁用连接池复用:在
celery.py中配置app.conf.broker_transport_options = { 'max_connections': 20, 'visibility_timeout': 3600, 'retry_policy': {'max_retries': 3} } - 临时验证方法:把
broker_url改成rediss://(启用 SSL)或换用amqp://(RabbitMQ),看问题是否消失——能快速定位是不是 Redis 驱动层的问题
如何用日志和监控快速定位重复源头
光看任务输出日志不够,得把上下文链路打穿:从 HTTP 请求 → 数据库变更 → 信号触发 → Celery 入队 → worker 执行。关键不是“任务执行了”,而是“谁、在什么条件下、第几次把它塞进队列”。
- 在任务函数第一行打全量日志:
logger.info("Task %s start, parent_id=%s, from=%s", self.request.id, self.request.parent_id, self.request.origin) - 给所有
.delay()调用点加上唯一 trace_id,比如从 request.META.get('HTTP_X_REQUEST_ID') 透传过来 - 用
celery -A myproject events实时监听事件流,过滤出task-received和task-started,对比 ID 是否成对出现 - 检查 Redis 队列长度:
redis-cli llen "celery",如果持续非零且任务日志频繁重复,大概率是 consumer 端卡住而非 producer 端误发
重复执行往往不是单一配置错误,而是事务边界、进程模型、网络中间件三者叠加的结果。最容易被忽略的是:你在本地调试时关掉了 DEBUG=False,结果生产环境的数据库连接池、缓存穿透、信号分发行为全都不一样。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











