Celery任务重复执行是分布式环境下缺乏协调的必然结果,根本原因是celery-beat无状态、不校验任务运行状态,多实例或网络延迟导致重复投递;防重关键在于执行前用Redis原子判断(如celery_once)或手写带过期与安全解锁的分布式锁,并合理配置prefetch和acks_late。

多节点部署时,Celery 任务重复执行不是“偶发bug”,而是分布式环境下缺乏协调的必然结果——只要两个 worker 同时从 broker 拿到同一个任务,就会各干各的。防止重复的关键不在于“让任务不进队列”,而在于“让第二个执行者主动放弃”。
为什么定时任务最容易重复?
celery-beat 调度器本身不检查任务是否已在运行。它只按时间点发一次消息,但若多个 beat 实例(比如 Docker 多副本、手动启了两个进程)同时运行,或网络延迟导致消息重复投递,broker 队列里就可能出现完全相同的 task_id 或参数组合。
- 现象:
celery -A myproject beat启动了两次,日志里看到Writing entries...出现两遍 - 后果:同一
@shared_task在 1 秒内被触发两次,数据库写入翻倍、邮件发两封、计数错乱 - 根本原因:beat 是无状态调度器,它不和 worker 或其他 beat 通信,也不查 Redis 里有没有待处理同名任务
用 celery_once 实现自动去重
celery_once 是目前最轻量且可靠的方案,它在任务入队前就用 Redis 键做原子性判断,避免竞争窗口。它不改 Celery 核心逻辑,只加一层“准入检查”。
- 安装:
pip install celery_once(要求 Celery ≥ 4.0) - 配置必须加在
celery.py或settings.py中:CELERY_ONCE = {'backend': 'celery_once.backends.Redis', 'settings': {'url': 'redis://127.0.0.1:6379/0'}} - 任务定义时指定 base 和去重 key:
@shared_task(base=QueueOnce, once={'keys': ['user_id']}),这样相同user_id的调用会被拒绝排队 - 注意:
once={'graceful': True}让重复任务静默丢弃;设为False则抛AlreadyQueued异常,需你自己捕获
自己手写 Redis 分布式锁要注意什么?
如果不想引入新包,可用 redis-py + set nx ex 手写锁,但容易踩坑:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
- 锁 key 必须包含足够区分度,比如
f"task:send_email:{user_id}",不能只用"send_email_lock" - 必须设
ex过期时间(如 600 秒),否则 worker 崩溃后锁永远不释放 - 解锁不能直接
del,得用 Lua 脚本比对 value 再删,否则可能误删别人持有的锁 - 别在
try/finally里解锁——万一任务执行超时,锁已过期,finally会删掉下一个 worker 刚设的锁
并发高时,worker 预取和 ack 策略也影响重复
默认 prefetch_count=4,一个 worker 会提前从 broker 拉 4 个任务到内存。如果它崩溃,这些任务没来得及 ack,broker 会重新分发,造成重复。
- 解决方法:在配置中设
CELERY_WORKER_PREFETCH_MULTIPLIER = 1,并开启CELERY_TASK_ACKS_LATE = True - 这样每个任务都等执行完才确认,哪怕 worker 挂了,任务也不会丢失或重复
- 代价是吞吐略降,但对防重来说值得——尤其涉及支付、发信等关键操作
真正难的不是加锁或配参数,而是想清楚“重复”的定义:是同一参数组合不能并发执行?还是同一任务 ID 绝对只能跑一次?前者用 celery_once 的 keys,后者得靠全局唯一 task_id + 结果缓存查重。选错粒度,锁就形同虚设。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










