APScheduler+Redis 分布式定时任务:解决多实例任务重复执行

在实际项目开发中,定时任务几乎是标配:数据统计、报表生成、订单超时关闭、缓存刷新、消息重试...... 我们最常用的就是 APScheduler 这个 Python 定时任务框架。

但一上生产环境就会遇到大坑:为了高可用部署了多个实例(多进程 / 多服务器),同一个定时任务被重复执行多次,导致数据重复、业务异常、接口雪崩。

这篇文章就给大家带来最实用的解决方案:APScheduler + Redis 实现分布式锁,从根本上解决多实例重复执行问题。代码可直接复制到项目使用,全程干货,非常适合 Django、Flask、FastAPI 后端项目部署。

一、为什么多实例部署会重复执行?

先把原理讲清楚,方便大家理解为什么要加锁。

APScheduler 本身是单机定时任务,它的调度器是基于内存的。

  • 单进程运行:任务正常执行一次。
  • 多实例(多服务器)运行:每个实例都有独立的调度器,互不感知。
  • 到了执行时间,所有实例同时触发任务 → 重复执行

这不是 APScheduler 的 bug,而是缺少分布式协调机制 。要解决它,必须引入分布式锁: 同一时间,只有抢到锁的实例才能执行任务,其他实例直接跳过。

二、技术选型:为什么用 Redis 锁?

市面上分布式锁方案很多:ZooKeeper、etcd、MySQL、Redis。 为什么后端项目首选 Redis?

  1. 轻量、简单,几乎所有项目都自带 Redis;
  2. 性能极高,不影响任务执行速度;
  3. 支持过期自动释放,防止死锁;
  4. 实现成本极低,几行代码就能搞定。

本文最终架构: 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、是否抢到锁。

八、为什么这套方案稳定可靠?

  1. 无中心架构:不依赖额外服务,Redis 几乎不会挂;
  2. 高性能:抢锁只是一次 Redis 操作;
  3. 自动容错:锁过期自动释放,不会死锁;
  4. 通用:支持所有 Python Web 框架;
  5. 生产可用:中小企业分布式定时任务标准方案。

九、总结

多实例部署下,APScheduler 重复执行是后端高频痛点 。 最简单、最稳定、成本最低的方案就是: APScheduler + Redis 分布式锁

本文提供的代码是生产级可用的,你只需要:

  1. 复制 Redis 分布式锁工具类
  2. 在任务函数中抢锁
  3. 抢到锁再执行业务

就能彻底解决:

  • 多进程重复执行
  • 多服务器重复执行
  • 定时任务重复写入数据
  • 报表重复推送
  • 接口重复调用

这套方案我在多个真实项目中长期使用,稳定运行无压力。

相关推荐
传说故事2 小时前
数据库中一些常用英文单词含义
数据库·oracle
夏贰四2 小时前
中小企业搭建业务中台如何控成本?业务中台轻量化落地分几步实施?
数据库·业务中台
林焱RPA2 小时前
影刀RPA200篇纪念版:最实用的20个自动化场景大盘点
数据库
名字还没想好☜2 小时前
Go 的 time.After 在 select 循环里内存泄漏:定时器堆积原理与 timer.Reset 正确姿势
java·数据库·golang·go·goroutine
香菜TTT4 小时前
Kafka_深度解析_从架构原理到生产实践
分布式·架构·kafka·linq
每天都要进步哦4 小时前
SQL单行函数详解:字符、数字、日期、转换与通用函数全解析
数据库·mysql
不剪发的Tony老师4 小时前
VeloxDB:一款免费开源的轻量级数据库管理工具
数据库·sql
dotnet904 小时前
批量生成所有表的修改脚本
数据库·sql·oracle
@Mike@5 小时前
06-数据库学习笔记(存储模型与数据压缩)
数据库·笔记·学习