Python定时任务框架APScheduler

定时任务库对比

推荐阅读 Python timing task - schedule vs. Celery vs. APScheduler

大小优点缺点适用场景
Schedule轻量级易用无配置不能动态添加任务或持久化任务简单任务
Celery重量级①任务队列
②分布式
①不能动态添加定时任务到系统中,如Flask(Django可以)
②设置起来较累赘
任务队列
APScheduler相对重量级①灵活,可动态增删定时任务并持久化
②支持多种存储后端
③集成框架多,用户广
重量级,学习成本大通用
Rocketry轻量级易用功能强尚未成熟,文档不清晰通用




简介

APScheduler 全称Advanced Python Scheduler,高级Python调度器。

APScheduler 是一个基于 Quartz 的 Python 定时任务框架,功能:

  • 可发起基于日期、固定时间间隔以及Corntab的任务
  • 动态管理任务
  • 任务可持久化
  • 多种调用器

如果将任务存储在数据库中,那么关闭调度程序后任务状态依然维持。

当调度器重启后,它将执行离线时所有应该执行的任务。




安装

本文版本为 3.8.0

pip install apscheduler




初试

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


if __name__ == '__main__':
    scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
    scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
    scheduler.start()

效果




原理

APScheduler有四种组件:

  • Triggers:触发器,按照时间间隔或其他条件触发任务
  • Job stores:任务存储器,默认存在内存中,也可存进数据库等
  • Executors:执行器
  • Schedulers:调度器,将其余部分绑定在一起
间隔轮询
返回运行结果
增删改查
调度器
触发器
满足运行条件
执行器
任务存储器




触发器 Triggers

指定日期

date:指定日期执行一次任务

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def show():
    """打印当前日期时间"""
    print(datetime.datetime.now())


now = datetime.datetime.now()  # 当前时间
next_run_time = now + datetime.timedelta(seconds=3)  # 3秒后
print(now)
print(next_run_time)
scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_job(show, 'date', next_run_time=next_run_time)
scheduler.start()

效果



时间间隔

interval:时间间隔执行任务

参数含义
seconds
minutes分钟
hours小时
days
weeks
start_date开始时间
end_date结束时间
jitter最多延迟多少秒

代码

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now(name):
    """打印当前时间"""
    print('{:<15} {}'.format(name, str(datetime.datetime.now().strftime('%H:%M:%S'))))


scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_job(now, 'interval', seconds=1, args=['间隔1秒'])  # 1s
scheduler.add_job(now, 'interval', minutes=0.1, args=['间隔0.1分钟'])  # 6s
scheduler.add_job(now, 'interval', hours=0.001, args=['间隔0.001小时'])  # 3.6s
scheduler.add_job(now, 'interval', days=0.0001, args=['间隔0.0001天'])  # 8.64s
scheduler.add_job(now, 'interval', weeks=0.00001, args=['间隔0.00001周'])  # 6.048s
scheduler.start()

效果



日期策略

cron:日期策略,最灵活强大的策略

参数含义
year年(4位数)
month月(1-12)
day日(1-31)
week周(1-53)
day_of_week周几(0-6)
hour时(0-23)
minute分(0-59)
second秒(0-59)
start_date最早开始时间
end_date最晚结束时间
timezone时区
jitter最多延迟多少秒

以上参数结合使用字符串表达式

表达式含义
*每个值都执行
*/a每隔 a 执行一次
a-b在 a-b 之间任一时间执行,如1-3则匹配1、2、3
a-b/c在 a-b 之间每隔 c 执行一次
xth y第 x 个星期 y 执行
last x最后一个星期 x 执行
last每月最后一天执行
x,y,z表达式组合

代码

import datetime
from apscheduler.schedulers.background import BackgroundScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BackgroundScheduler()
jobs = [
    scheduler.add_job(now, 'cron', second='*/1'),  # 间隔1s
    scheduler.add_job(now, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3'), # 6/8/11/12月的第三个星期五的00:00/01:00/02:00/03:00执行
    scheduler.add_job(now, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2199-05-30'), # 每周一到周五的05:30执行,直到2199-05-30
]
scheduler.start()
for job in jobs:
    print(job.next_run_time)  # 下次执行时间
while True:
    pass




通过装饰器添加任务

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程


@scheduler.scheduled_job('cron', second='*/1')
def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler.start()




根据 Cron表达式 构造:分钟 小时 日 月 星期验证网站

import datetime
from apscheduler.triggers.cron import CronTrigger
from apscheduler.schedulers.background import BackgroundScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BackgroundScheduler()
jobs = [
    scheduler.add_job(now, CronTrigger.from_crontab('*/1 * * * *')),  # 间隔1分钟
    scheduler.add_job(now, CronTrigger.from_crontab('0 0 1-15 may-aug *')),  # 5月到8月的1到15日的00:00
    scheduler.add_job(now, CronTrigger.from_crontab('0 20 * * *')),  # 每天20:00
]
scheduler.start()
for job in jobs:
    print(job.next_run_time)  # 下次执行时间
while True:
    pass

更细粒度日期策略查阅:

  1. crontab 命令详解
  2. crontab 例子




调度器 Schedulers

调度器启动后无法更改配置

阻塞调度器

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

n = 0  # 执行次数


def now():
    """打印当前时间"""
    global n
    n += 1
    print(datetime.datetime.now().strftime('%H:%M:%S'))
    if n == 5:
        scheduler.shutdown(wait=False)  # 关闭调度器


scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
scheduler.start()
print('Hello World!')  # 最后才执行

效果



后台调度器

import time
import datetime
from apscheduler.schedulers.base import STATE_STOPPED
from apscheduler.schedulers.background import BackgroundScheduler

n = 0  # 执行次数


def now():
    """打印当前时间"""
    global n
    n += 1
    print(datetime.datetime.now().strftime('%H:%M:%S'))
    if n == 5:
        scheduler.shutdown(wait=False)  # 关闭调度器


scheduler = BackgroundScheduler()  # 后台调度器,不会阻塞主进程
scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
scheduler.start()
print('Hello World!')  # 马上执行
while True:
    time.sleep(1)
    if scheduler.state == STATE_STOPPED:
        break

效果



配置调度器

通过代码配置

from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor

jobstores = {
    'mongo': MongoDBJobStore(),
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
executors = {
    'default': ThreadPoolExecutor(20),
    'processpool': ProcessPoolExecutor(5)
}
job_defaults = {
    'coalesce': False,
    'max_instances': 3
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults,
                                timezone='Asia/Shanghai')

通过字典配置

from apscheduler.schedulers.background import BackgroundScheduler

gconfig = {
    'apscheduler.jobstores.mongo': {
        'type': 'mongodb'
    },
    'apscheduler.jobstores.default': {
        'type': 'sqlalchemy',
        'url': 'sqlite:///jobs.sqlite'
    },
    'apscheduler.executors.default': {
        'class': 'apscheduler.executors.pool:ThreadPoolExecutor',
        'max_workers': '20'
    },
    'apscheduler.executors.processpool': {
        'type': 'processpool',
        'max_workers': '5'
    },
    'apscheduler.job_defaults.coalesce': 'false',
    'apscheduler.job_defaults.max_instances': '3',
    'apscheduler.timezone': 'Asia/Shanghai',
}

scheduler = BackgroundScheduler(gconfig)

修改配置

from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor

scheduler = BackgroundScheduler()

# 添加任务等

jobstores = {
    'mongo': MongoDBJobStore(),
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
executors = {
    'default': ThreadPoolExecutor(20),
    'processpool': ProcessPoolExecutor(5)
}
job_defaults = {
    'coalesce': False,
    'max_instances': 3
}
scheduler.configure(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone='Asia/Shanghai')

默认配置如下

jobstores = {

}
executors = {

}
job_defaults = {
    'misfire_grace_time': 1,
    'coalesce': False,
    'max_instances': 3
}




任务存储器 Job stores

任务存储器的选择有两种,内存或数据库。
内存,简单高效。
数据库,程序崩溃后重启可以从之前中断的地方恢复运行。

内存

默认,重启后任务丢失



SQLAlchemy

安装

pip install sqlalchemy
pip install pymysql

url 定义见 Engine Configuration

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_jobstore('sqlalchemy', url='mysql+pymysql://root:123456@localhost:3306/test', tablename='jobs')
scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
scheduler.start()

效果

add_job() 注释掉,重启程序会继续执行任务



MongoDB

安装

pip install pymongo

代码

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_jobstore('mongodb', database='apscheduler', collection='jobs')
scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
scheduler.start()

效果



Redis

安装

pip install redis

代码

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BlockingScheduler()  # 阻塞调度器,会阻塞当前进程
scheduler.add_jobstore('redis', db=0, jobs_key='apscheduler.jobs', run_times_key='apscheduler.run_times')
scheduler.add_job(now, 'interval', seconds=1)  # 间隔1s执行一次now函数
scheduler.start()

效果




执行器 Executors

执行器的选择取决于应用场景。通常默认的 ThreadPoolExecutor 已经在大部分情况下是可以满足我们需求的。如果我们的任务涉及到一些 CPU 密集计算的操作。那么应该考虑 ProcessPoolExecutor

  • ThreadPoolExecutor:线程池执行器(默认)。
  • ProcessPoolExecutor:进程池执行器,适用于 CPU 密集型操作
  • GeventExecutorGevent 程序执行器。
  • TornadoExecutorTornado 程序执行器。
  • TwistedExecutorTwisted 程序执行器。
  • AsyncIOExecutorasyncio 程序执行器。




任务操作

任务操作函数备注
添加add_job()如果使用了任务存储器,可使用参数 replace_existing=True,否则每次开启都会创建任务副本
删除remove_job()remove()删除所有任务可用 remove_all_jobs()
暂停pause_job()pause()
继续resume_job()resume()
修改modify_job()modify()只能改属性,如id、name、max_instances
重设reschedule_job()reschedule()能改具体策略
获取get_job()get_jobs()前者传 job_id,后者传 jobstore
打印print_jobs()

代码

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler


def now():
    """打印当前时间"""
    print(datetime.datetime.now().strftime('%H:%M:%S'))


scheduler = BlockingScheduler()
# 添加任务
job = scheduler.add_job(now, 'interval', id='0001', name='1秒', seconds=1)
job2 = scheduler.add_job(now, 'interval', id='0002', name='1分钟', minutes=1)

# 获取任务
jobs = scheduler.get_jobs()  # 获取任务
print(jobs)

# 删除任务
job2.remove()
# scheduler.remove_job(job_id='0002')
# scheduler.remove_all_jobs()  # 删除所有任务
scheduler.print_jobs()  # 打印任务

# 暂停任务
job.pause()
# scheduler.pause_job(job_id='0001')
scheduler.print_jobs()

# 继续任务
job.resume()
# scheduler.resume_job(job_id='0001')
scheduler.print_jobs()

# 修改任务
job.modify(name='2秒', max_instances=3)
# scheduler.modify_job(job_id='0001', name='2秒', max_instances=3)
scheduler.print_jobs()

# 重设任务
job.reschedule('interval', seconds=2)
# scheduler.reschedule_job(job_id='0001', trigger='interval', seconds=2)
scheduler.print_jobs()

# [<Job (id=0001 name=1秒)>, <Job (id=0002 name=1分钟)>]
# Pending jobs:
#     1秒 (trigger: interval[0:00:01], pending)
# Pending jobs:
#     1秒 (trigger: interval[0:00:01], paused)
# Pending jobs:
#     1秒 (trigger: interval[0:00:01], next run at: 2021-10-13 15:45:47 CST)
# Pending jobs:
#     2秒 (trigger: interval[0:00:01], next run at: 2021-10-13 15:45:47 CST)
# Pending jobs:
#     2秒 (trigger: interval[0:00:02], next run at: 2021-10-13 15:45:48 CST)




调度器操作

调度器操作函数备注
启动start()
停止shutdown()立即停止,参数 wait=False
暂停pause()
继续resume()
监听add_listener()事件大全
更多操作BaseScheduler

代码

import time
import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from apscheduler.schedulers.base import STATE_PAUSED, STATE_STOPPED

n = 0


def now():
    """打印当前时间"""
    global n
    n += 1
    if n == 2:
        raise Exception
    elif n == 3:
        scheduler.pause()  # 暂停
        print('暂停', n)
    elif n == 5:
        scheduler.shutdown(wait=False)  # 停止
        print('停止', n)
    else:
        print(datetime.datetime.now().strftime('%H:%M:%S'), n)


def listener(event):
    """监听的回调函数"""
    if event.exception:
        print('发生异常', n)


scheduler = BackgroundScheduler()
scheduler.add_job(now, 'interval', seconds=1)
scheduler.add_listener(listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)  # 监听
scheduler.start()  # 启动
while True:
    time.sleep(1)
    if scheduler.state == STATE_PAUSED:
        scheduler.resume()  # 继续
        print('继续', n)
    if scheduler.state == STATE_STOPPED:
        break

效果




异常监听

事件编码含义
EVENT_SCHEDULER_STARTED调度器启动
EVENT_SCHEDULER_SHUTDOWN调度器停止
EVENT_SCHEDULER_PAUSED调度器暂停
EVENT_SCHEDULER_RESUMED调度器继续
EVENT_EXECUTOR_ADDED添加了执行器
EVENT_EXECUTOR_REMOVED删除了执行器
EVENT_JOBSTORE_ADDED添加了任务存储器
EVENT_JOBSTORE_REMOVED删除了任务存储器
EVENT_ALL_JOBS_REMOVED删除了所有任务
EVENT_JOB_ADDED添加了任务到作业存储器
EVENT_JOB_REMOVED删除了任务
EVENT_JOB_MODIFIED修改了任务
EVENT_JOB_SUBMITTED提交了任务给执行器
EVENT_JOB_MAX_INSTANCES任务已达到并发执行的最大实例数
EVENT_JOB_EXECUTED任务成功执行
EVENT_JOB_ERROR任务执行过程中发生异常
EVENT_JOB_MISSED错过了一个任务的执行
EVENT_ALL所有事件

代码

from apscheduler.schedulers.background import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR


def error():
    raise Exception


def listener(event):
    """监听的回调函数"""
    if event.exception:
        print('发生异常')


scheduler = BlockingScheduler()
scheduler.add_job(error, 'interval', seconds=1)
scheduler.add_listener(listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)  # 监听
scheduler.start()




任务属性

属性含义
id任务ID
name任务名称
next_run_time下次执行时间
args调用的函数args参数
kwargs调用的函数kwargs参数
coalesce是否合并任务(任务堆积是否只执行一次)
executor执行器
trigger触发器
max_instances最大实例数
misfire_grace_time误差时间
func调用的函数
func_ref
pending是否在等待被添加到任务存储器

代码

from apscheduler.schedulers.background import BackgroundScheduler


def func(*args, **kwargs):
    pass


scheduler = BackgroundScheduler()
job = scheduler.add_job(func, 'interval', seconds=1, name='任务名称', args=[1, 2, 3], kwargs={'a': 1})
scheduler.start()
print(job.id)  # 任务ID
print(job.name)  # 任务名称
print(job.next_run_time)  # 下次执行时间
print(job.args)  # 调用的函数args参数
print(job.kwargs)  # 调用的函数kwargs参数
print(job.coalesce)  # 是否合并任务
print(job.executor)  # 执行器
print(job.trigger)  # 触发器
print(job.max_instances)  # 最大实例数
print(job.misfire_grace_time)  # 误差时间
print(job.func.__name__)  # 调用的函数
print(job.func_ref)
print(job.pending)  # 是否在等待被添加到任务存储器
# 091724921afb43869021bf6b42cf0136
# 任务名称
# 2021-10-14 12:26:54.185792+08:00
# (1, 2, 3)
# {'a': 1}
# True
# default
# interval[0:00:01]
# 1
# 1
# func
# __main__:func
# False




任务执行时间超过时间间隔

任务执行时间超过时间间隔会跳过该次调度,等到下一个周期重新调度

最常见的情况是关闭了调度器,并且在任务应该执行的时间后重启

调度器会对比每个错过的任务和 misfire_grace_time ,来判断是否执行该任务

详细阅读 Missed job executions and coalescing




装饰器

任务执行完后修改参数

  • 修改参数为函数结束时间(默认)
  • 静态修改参数(只改一次)
  • 动态修改参数(调用函数)
import datetime
from typing import Iterable
from functools import wraps

from apscheduler.schedulers.background import BackgroundScheduler


def modify_args(id=None, name=None, args=None):
    """修改APScheduler任务的参数"""

    def decorator(func):
        @wraps(func)
        def wrapper(*_args, **kwargs):
            result = func(*_args, **kwargs)
            job_args = None
            if args is None:  # 默认修改参数为函数结束时间
                job_args = [datetime.datetime.now().strftime('%H:%M:%S')]
            elif isinstance(args, Iterable):  # 静态修改参数
                job_args = args
            elif callable(args):  # 动态修改参数
                job_args = args()
            _name = name if name else func.__name__  # 任务名称默认为装饰器的函数名
            job = next((i for i in scheduler.get_jobs() if i.id == id or i.func.__name__ == _name), None)
            if job and job_args:
                job.modify(args=job_args)
            return result

        return wrapper

    return decorator


def f():
    """随机生成参数"""
    import random
    return [random.randint(0, 10) for _ in range(random.randint(1, 5))]


# @modify_args()
# @modify_args(args=[1, 2, 3])
@modify_args(args=f)
def show(*args):
    """打印"""
    print(args)


scheduler = BackgroundScheduler()
scheduler.start()
job = scheduler.add_job(show, 'interval', seconds=1, args=['开始'])
print(scheduler.get_jobs())
while True:
    pass

效果



同一函数名只允许添加一次任务

继承 Scheduler 并修改 add_job()

from apscheduler.util import undefined
from apscheduler.schedulers.background import BackgroundScheduler


class AddJobOnceBackgroundScheduler(BackgroundScheduler):
    """同一函数名只允许添加一次任务"""

    def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None,
                misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
                next_run_time=undefined, jobstore='default', executor='default',
                replace_existing=False, **trigger_args):
        for job in self.get_jobs():
            if job.func.__name__ == func.__name__:
                return None
        return super(AddJobOnceBackgroundScheduler, self).add_job(func, trigger, args, kwargs, id, name,
                                                                  misfire_grace_time,
                                                                  coalesce, max_instances, next_run_time, jobstore,
                                                                  executor,
                                                                  replace_existing, **trigger_args)


def show(*args):
    """打印"""
    print(args, scheduler.get_jobs())


def f():
    pass


scheduler = AddJobOnceBackgroundScheduler()
scheduler.start()
scheduler.add_job(show, 'interval', seconds=1, args=['A'], name='A')
scheduler.add_job(show, 'interval', seconds=1, args=['B'], name='B')
scheduler.add_job(f, 'interval', seconds=1)
scheduler.add_job(f, 'interval', seconds=1)
while True:
    pass

效果




扩展 APScheduler

Extending APScheduler

如 使用 Redis分布式锁,SQLAlchemy 存储任务在 MySQL

Redis锁 严重依赖系统时钟,且因为是乐观锁,不能保证绝对正确性

安装

pip install sqlalchemy
pip install pymysql
pip install redis
pip install redlock

项目结构

__init__.py


executors.py

import uuid
import logging
import datetime
import traceback

import redlock
from redis import Redis
from apscheduler.job import Job
from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.executors.base import MaxInstancesReachedError

logger = logging.getLogger(__name__)

__all__ = ['RedisLockExecutor']

redis = Redis()

prefix = 'apscheduler'  # Redis键前缀


class RedisLock(redlock.RedLock):
    """Redis分布式锁"""

    def __init__(self, job_id):
        # ttl要大于业务执行时间
        super().__init__(f'{prefix}:job_lock:{job_id}', connection_details=[redis], retry_times=3, ttl=100000)
        self._locked = {}

    def acquire_node(self, node):
        r = super().acquire_node(node)
        if r is not None:
            self._locked[id(node)] = True
        return r

    def release_node(self, node):
        if self._locked.pop(id(node), None) is None:
            return
        super().release_node(node)


class RedisLockExecutor(ThreadPoolExecutor):
    """Redis分布式锁执行器"""

    def submit_job(self, job: Job, run_times):
        """提交待执行任务"""
        try:
            super().submit_job(job, run_times)
        except redlock.RedLockError:
            job.id = f'{job.id}.ignore'
            # traceback.print_exc()
        except MaxInstancesReachedError:
            job.id = f'{job.id}.ignore'
        except Exception:
            job.id = f'{job.id}.ignore'
            # traceback.print_exc()

    def _do_submit_job(self, job: Job, run_times):
        """执行任务"""
        with RedisLock(job.id):
            now = datetime.datetime.now(self._scheduler.timezone)
            job_next_run = job.trigger.get_next_fire_time(run_times[-1], now)
            if job_next_run:
                ttl = int(job_next_run.timestamp() - now.timestamp())
                ttl = 10 if ttl <= 0 else ttl
            else:
                ttl = 3600
            job_exec_key = f'{prefix}:job_lock:exec:{job.id}'
            lock_key = uuid.uuid4().hex
            if redis.set(job_exec_key, lock_key, nx=True, ex=ttl):  # 不存在锁时加锁
                # if redis.set(job_exec_key, lock_key, nx=True) or redis.ttl(job_exec_key) == -1:  # 不存在锁时加锁 或 锁存在但ttl为-1
                # redis.expire(name=job_exec_key, time=ttl)
                pass
            else:
                raise Exception(job.id)
        super()._do_submit_job(job, run_times)

jobstores.py

from apscheduler.job import Job
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore


class LockedMemoryJobStore(MemoryJobStore):
    """Redis分布式锁-内存存储器"""

    def update_job(self, job: Job):
        if job.id and job.id.endswith('.ignore'):
            return
        super().update_job(job)

    def remove_job(self, job_id):
        if job_id and job_id.endswith('.ignore'):
            job_id = job_id[: -7]
            job = self.lookup_job(job_id)
            job.id = job_id
        super().remove_job(job_id)


class LockedSQLAlchemyJobStore(SQLAlchemyJobStore):
    """Redis分布式锁-SQLAlchemy存储器"""

    def update_job(self, job: Job):
        if job.id and job.id.endswith('.ignore'):
            return
        super().update_job(job)

    def remove_job(self, job_id):
        if job_id and job_id.endswith('.ignore'):
            return
        super().remove_job(job_id)

schedulers.py

from apscheduler.schedulers.background import BackgroundScheduler

from scheduler.executors import RedisLockExecutor
from scheduler.jobstores import LockedSQLAlchemyJobStore

__all__ = ['scheduler', 'get_next_run_time']

jobstores = {
    'default': LockedSQLAlchemyJobStore(
        url='mysql+pymysql://root:123456@localhost:3306/test?charset=utf8',
        tablename='apscheduler_jobs',
        engine_options={'pool_pre_ping': True, 'pool_recycle': 1800},
    )
}
executors = {
    'default': RedisLockExecutor(max_workers=5),
}
job_defaults = {
    'coalesce': False,
    # 'max_instances': 3,
    'misfire_grace_time': None  # 3600,
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults,
                                timezone='Asia/Shanghai')


def get_next_run_time(job_id):
    """获取任务下次执行时间"""
    return scheduler.get_job(job_id).next_run_time

main.py

import time
import random
import datetime
from uuid import uuid4

from scheduler.schedulers import scheduler

from apscheduler.triggers.cron import CronTrigger


def now(id):
    """打印当前时间"""
    secs = random.randint(0, 10)
    print(id, datetime.datetime.now().strftime('%H:%M:%S'), secs)
    time.sleep(secs)


scheduler.start()
jobs = scheduler.get_jobs()
print(f'已启动APScheduler,任务有:{jobs}')
print(f'scheduler.state={scheduler.state}')
for job in jobs:
    delta_time = job.next_run_time.timestamp() - time.time()
    if delta_time < 0:
        job.resume()  # 恢复任务
        delta_time = job.next_run_time.timestamp() - time.time()
    print(f'id={job.id} name={job.name} coalesce={job.coalesce} pending={job.pending} '
          f'max_instances={job.max_instances} misfire_grace_time={job.misfire_grace_time} '
          f'next_run_time={job.next_run_time} delta_time={delta_time:.0f} ')
n = 1
while True:
    id = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    # id = uuid4().hex
    job = scheduler.add_job(now, 'interval', seconds=5, id=id, args=[id])  # 间隔任务
    # job = scheduler.add_job(now, CronTrigger.from_crontab('57 11 * * *'), id=id, args=[id])  # 指定时间
    print(f'{job.id} added')
    time.sleep(n)
    n *= 2
    pass

通过配置启动时,不要把type带上
配置为yaml时,None要换成null

scheduler:
    timezone: Asia/Shanghai
    apscheduler.job_defaults.misfire_grace_time: 3
    apscheduler.jobstores.default:
      class: app.scheduler.jobstores:LockedSQLAlchemyJobStore
      url: mysql+pymysql://用户名:密码@host:port/数据库?charset=utf8
      tablename: ap_scheduler_jobs
      engine_options:
        pool_pre_ping: true
        pool_recycle: 1800
    apscheduler.executors.default:
      class: app.scheduler.executors:RedisLockExecutor
      max_workers: 5

TODO:完善分布式锁

  1. 加锁并设过期时间 SET lock_resource_name $uuid NX PX $expire_time,同时启动守护线程为快要过期单还没执行完毕的客户端的锁续命;
  2. 客户端执行业务逻辑操作共享资源;
  3. 通过 Lua 脚本释放锁,先 get 判断锁是否是自己加的,再执行 DEL




封装

import time
import datetime
from apscheduler.schedulers.base import STATE_STOPPED
from apscheduler.schedulers.background import BackgroundScheduler

n = 0  # 执行次数


def now():
    """打印当前时间"""
    global n
    n += 1
    print(datetime.datetime.now().strftime('%H:%M:%S'))
    if n == 5:
        scheduler.shutdown(wait=False)  # 关闭调度器


def get_next_run_time(job_id):
    """获取任务下次执行时间"""
    return scheduler.get_job(job_id).next_run_time


scheduler = BackgroundScheduler()
job = scheduler.add_job(now, 'interval', seconds=1)
scheduler.start()
print(get_next_run_time(job.id))
while True:
    time.sleep(1)
    if scheduler.state == STATE_STOPPED:
        break




常见问题

为什么调度器不执行任务?

主要原因有:

  1. 在没有启用线程的情况下,在 uWSGI 工作进程中运行调度程序
  2. 使用 BackgroundScheduler,任务在调度器运行后才添加
from apscheduler.schedulers.background import BackgroundScheduler


def myjob():
    print('hello')


scheduler = BackgroundScheduler()
scheduler.start()
scheduler.add_job(myjob, 'cron', hour=0)  # 添加任务后就立即退出了,没有机会执行任务




为什么会ValueError?

  • 用了 lambda 函数
  • 用了绑定方法
  • 用了内嵌函数
  • ……




怎么在 uWSGI 中使用 APScheduler ?

uWSGI 禁用了全局解释器锁,同时也禁用了对 APScheduler 的运行至关重要的线程。要解决这个问题,需要使用 --enable-threads 开关重新启用 GIL。




如何在多个工作进程间共享一个任务存储

不能。

在多个进程间共享持久任务存储会让调度器执行出错,如重复执行或丢失作业等。这是因为 APScheduler 目前没有进程间同步和信号机制,无法在添加、修改或从任务存储中删除作业时通知调度程序。

解决办法:在一个专门的进程中运行调度器,并通过某种远程访问机制如 RPyC、gRPC 或 HTTP服务器来连接它。源码库中包含了一个由客户端访问的基于RPyC的服务的例子




如何在 web 应用程序中使用 APScheduler ?

Django:django-apscheduler

Flask:Flask-APScheduler

Pyramid:pyramid_scheduler

除此之外,通常使用 BackgroundScheduler 运行 APScheduler

如果运行的是异步 web 框架,如 aiohttp,需要使用不同的调度器来利用框架的异步特性。

推荐阅读:

  1. Python 定时任务框架 APScheduler 详解(分布式 APScheduler部分)




APScheduler 有图形用户界面吗?




遇到的坑

1. RuntimeError: cannot join current thread

scheduler.shutdown(wait=False)

2. pkg_resources.DistributionNotFound

pip install xxx

3. sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (2013, ‘Lost connection to MySQL server during query’)

调用 scheduler.get_job(job_id).next_run_time 时报错

传递参数给 engine_options 时加上 {'pool_pre_ping': True}

4. 报错JobLookupError

5. 报错 Run time of job “xxx” was missed by 0:00:05

修改启动配置

job_defaults = {
    'coalesce': False,
    'misfire_grace_time': 3600,
}

并设置所有任务

scheduler.start()
jobs = scheduler.get_jobs()
for job in jobs:
    job.modify(coalesce=False, misfire_grace_time=3600)

如果希望任务不被跳过,永远执行,可以将 misfire_grace_time 设为 None




参考文献

  1. APScheduler Documentation
  2. APScheduler GitHub
  3. Python3定时任务四种实现方式
  4. python定时任务最强框架APScheduler详细教程
  5. Python时间访问和转换库time,含时间戳和时间互转
  6. APScheduler原理及用法
  7. Python 定时任务框架 APScheduler 详解
  8. Python任务调度模块APScheduler
  9. APScheduler中两种调度器的区别及使用过程中要注意的问题
  10. 报错redis.exceptions.ResponseError: value is not an integer or out of range
  11. Redis RedLock 是完美的分布式锁吗
  12. Dramatiq Documentation
  13. APScheduler Jobstore Oracle drops “OperationalError ORA-03114” after adding a job (add_job)
  14. Redis 分布式锁没这么简单,网上大多数都有 bug
  15. 解决apscheduler报错:Run time of job …… next run at: ……)” was missed
  16. 分布式锁用Redis还是Zookeeper?
  17. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!
  18. 分布式锁Redis、zookeeper、etcd怎样抉择?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XerCis

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值