在实际项目开发中,定时任务几乎是标配:数据统计、报表生成、订单超时关闭、缓存刷新、消息重试...... 我们最常用的就是 APScheduler 这个 Python 定时任务框架。
但一上生产环境就会遇到大坑:为了高可用部署了多个实例(多进程 / 多服务器),同一个定时任务被重复执行多次,导致数据重复、业务异常、接口雪崩。
这篇文章就给大家带来最实用的解决方案:APScheduler + Redis 实现分布式锁,从根本上解决多实例重复执行问题。代码可直接复制到项目使用,全程干货,非常适合 Django、Flask、FastAPI 后端项目部署。

一、为什么多实例部署会重复执行?
先把原理讲清楚,方便大家理解为什么要加锁。
APScheduler 本身是单机定时任务,它的调度器是基于内存的。
- 单进程运行:任务正常执行一次。
- 多实例(多服务器)运行:每个实例都有独立的调度器,互不感知。
- 到了执行时间,所有实例同时触发任务 → 重复执行。
这不是 APScheduler 的 bug,而是缺少分布式协调机制 。要解决它,必须引入分布式锁: 同一时间,只有抢到锁的实例才能执行任务,其他实例直接跳过。
二、技术选型:为什么用 Redis 锁?
市面上分布式锁方案很多:ZooKeeper、etcd、MySQL、Redis。 为什么后端项目首选 Redis?
- 轻量、简单,几乎所有项目都自带 Redis;
- 性能极高,不影响任务执行速度;
- 支持过期自动释放,防止死锁;
- 实现成本极低,几行代码就能搞定。
本文最终架构: APScheduler(定时调度) + Redis(分布式锁)= 安全分布式定时任务
三、环境准备
pip install apscheduler # 定时任务
pip install redis # Redis客户端
pip install python-dotenv # 环境变量(可选)
四、核心实现:Redis 分布式锁工具类
这是整个方案的灵魂,直接拿去用。
利用 Redis 的 SETNX 特性:只有不存在时才能设置成功,代表抢到锁。
# redis_lock.py
import redis
import time
from typing import Optional
# 初始化Redis连接(根据你的项目修改)
redis_client = redis.Redis(
host="localhost",
port=6379,
db=0,
password="", # 有密码填密码
decode_responses=False
)
class RedisDistributedLock:
"""Redis分布式锁,解决多实例定时任务重复执行"""
def __init__(self, lock_key: str, expire_seconds: int = 60):
self.lock_key = lock_key
self.expire_seconds = expire_seconds
self.lock_value = str(time.time())
def acquire(self) -> bool:
"""获取锁:成功返回True,失败返回False"""
return redis_client.set(
self.lock_key,
self.lock_value,
ex=self.expire_seconds,
nx=True # 关键:不存在才设置
)
def release(self):
"""释放锁(可自动过期,不强制要求)"""
try:
redis_client.delete(self.lock_key)
except Exception:
pass
五、APScheduler 集成分布式锁
这是最常用的写法:任务执行前抢锁,抢到才执行。
# scheduler.py
from apscheduler.schedulers.background import BackgroundScheduler
from redis_lock import RedisDistributedLock
import time
import datetime
def test_task():
"""示例定时任务:多实例不会重复执行"""
lock = RedisDistributedLock(
lock_key="scheduler:test_task_lock",
expire_seconds=50 # 稍小于任务间隔
)
# 抢锁:抢不到直接返回
if not lock.acquire():
print(f"【{datetime.datetime.now()}】其他实例已执行,跳过")
return
try:
# ========== 业务逻辑开始 ==========
print(f"【{datetime.datetime.now()}】任务开始执行")
time.sleep(3) # 模拟业务耗时
print(f"【{datetime.datetime.now()}】任务执行完成")
# ========== 业务逻辑结束 ==========
finally:
lock.release()
# 启动调度器
if __name__ == '__main__':
scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
# 每1分钟执行一次
scheduler.add_job(
test_task,
trigger="interval",
minutes=1
)
scheduler.start()
try:
while True:
time.sleep(60)
except (KeyboardInterrupt, SystemExit):
scheduler.shutdown()
六、Django / Flask / FastAPI 中如何使用?
在 Web 项目里,写法几乎一样,只需要在启动文件中加入调度器即可。
示例(FastAPI 风格)
from fastapi import FastAPI
from scheduler import scheduler
app = FastAPI()
@app.on_event("startup")
def start_scheduler():
scheduler.start()
无论你启多少个实例,同一时间只有一个实例会真正执行业务代码。
七、关键配置与避坑要点(非常重要)
想让分布式定时任务稳定运行,必须注意以下几点:
1. 锁的过期时间要合理
- 过期时间 > 任务最大执行时间
- 过期时间 < 任务执行间隔
例如:任务每 60s 执行一次,锁过期设 50s 最安全。
2. 任务必须幂等
即使极端情况下重复执行,也不能产生脏数据:
- 新增用唯一性约束
- 更新用版本号
- 流水号防重
3. 不要使用内存作业存储
多实例下内存作业存储无效,正式项目建议:
- RedisJobStore(推荐)
- MongoDBJobStore
4. 日志一定要区分实例
方便排查问题:输出机器 IP、进程 ID、是否抢到锁。
八、为什么这套方案稳定可靠?
- 无中心架构:不依赖额外服务,Redis 几乎不会挂;
- 高性能:抢锁只是一次 Redis 操作;
- 自动容错:锁过期自动释放,不会死锁;
- 通用:支持所有 Python Web 框架;
- 生产可用:中小企业分布式定时任务标准方案。
九、总结
多实例部署下,APScheduler 重复执行是后端高频痛点 。 最简单、最稳定、成本最低的方案就是: APScheduler + Redis 分布式锁
本文提供的代码是生产级可用的,你只需要:
- 复制 Redis 分布式锁工具类
- 在任务函数中抢锁
- 抢到锁再执行业务
就能彻底解决:
- 多进程重复执行
- 多服务器重复执行
- 定时任务重复写入数据
- 报表重复推送
- 接口重复调用
这套方案我在多个真实项目中长期使用,稳定运行无压力。