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. 抢到锁再执行业务

就能彻底解决:

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

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

相关推荐
IvorySQL1 小时前
PostgreSQL 日报|恢复期 COPY TO 未日志表修复(10 月 21 日)
数据库·postgresql·区块链
贝猫说python2 小时前
大模型比赛2.3智能体开发(工具调用智能问答助手)
数据库·oracle
许彰午4 小时前
28-ORA-01110数据文件读取错误
数据库·oracle
凯凯姜4 小时前
MySQL 联合索引设计与索引失效
数据库·mysql
猫咪很可爱4 小时前
Oracle TEMP 表空间不足(ORA-01652)问题分析与优化实践
数据库·oracle
九零HTTP6 小时前
10月21号-CSDN配菜-IP池的5个坑
服务器·数据库·tcp/ip
Data-Miner6 小时前
需要做财务数据分析,有什么好用的工具?从准备到出报告的实操指南
大数据·数据库·数据分析
袋鼠云数栈7 小时前
让 SQL 解析不再重复造轮子:SqlParserX 正式开源
数据库·人工智能·sql·开源·数据治理
这个DBA有点耶7 小时前
期货 CTP 系统 Oracle RAC 迁移国产库实战:读写分离架构设计与性能对标
数据库·架构·dba
数据库安全7 小时前
AI数据访问安全 |如何控制AI访问权限,防止越权
数据库·安全