文章目录
定时任务库对比
推荐阅读 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
更细粒度日期策略查阅:
调度器 Schedulers
BlockingScheduler
:阻塞调度器,调用start()
会阻塞当前进程BackgroundScheduler
:后台调度器,调用start()
不会阻塞主进程AsyncIOScheduler
:适用于asyncio
的程序GeventScheduler
:适用于gevent
的程序TornadoScheduler
:适用于Tornado
的程序TwistedScheduler
:适用于Twisted
的程序QtScheduler
:适用于Qt
的程序
调度器启动后无法更改配置
阻塞调度器
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
任务存储器的选择有两种,内存或数据库。
内存,简单高效。
数据库,程序崩溃后重启可以从之前中断的地方恢复运行。
MemoryJobStore
:没有序列化,任务存储在内存中,增删改查都是在内存中完成(默认)SQLAlchemyJobStore
:使用SQLAlchemy
这个 ORM 框架作为存储方式MongoDBJobStore
:使用mongodb
作为存储器RedisJobStore
:使用redis
作为存储器RethinkDBJobStore
:使用RethinkDB
作为存储器ZooKeeperJobStore
:使用ZooKeeper
作为存储器
内存
默认,重启后任务丢失
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 密集型操作GeventExecutor
:Gevent
程序执行器。TornadoExecutor
:Tornado
程序执行器。TwistedExecutor
:Twisted
程序执行器。AsyncIOExecutor
:asyncio
程序执行器。
任务操作
任务操作 | 函数 | 备注 |
---|---|---|
添加 | 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
如 使用 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:完善分布式锁
- 加锁并设过期时间
SET lock_resource_name $uuid NX PX $expire_time
,同时启动守护线程为快要过期单还没执行完毕的客户端的锁续命; - 客户端执行业务逻辑操作共享资源;
- 通过
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
常见问题
为什么调度器不执行任务?
主要原因有:
- 在没有启用线程的情况下,在 uWSGI 工作进程中运行调度程序
- 使用
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,需要使用不同的调度器来利用框架的异步特性。
推荐阅读:
- Python 定时任务框架 APScheduler 详解(分布式 APScheduler部分)
APScheduler 有图形用户界面吗?
- django-apscheduler
- apschedulerweb
- Nextdoor scheduler:只支持 Mac 或 Linux
遇到的坑
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
参考文献
- APScheduler Documentation
- APScheduler GitHub
- Python3定时任务四种实现方式
- python定时任务最强框架APScheduler详细教程
- Python时间访问和转换库time,含时间戳和时间互转
- APScheduler原理及用法
- Python 定时任务框架 APScheduler 详解
- Python任务调度模块APScheduler
- APScheduler中两种调度器的区别及使用过程中要注意的问题
- 报错redis.exceptions.ResponseError: value is not an integer or out of range
- Redis RedLock 是完美的分布式锁吗
- Dramatiq Documentation
- APScheduler Jobstore Oracle drops “OperationalError ORA-03114” after adding a job (add_job)
- Redis 分布式锁没这么简单,网上大多数都有 bug
- 解决apscheduler报错:Run time of job …… next run at: ……)” was missed
- 分布式锁用Redis还是Zookeeper?
- 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!
- 分布式锁Redis、zookeeper、etcd怎样抉择?