详解Django定时任务模块设计与实践


Posted in Python onJuly 24, 2019

在开发后台与任务相关的功能中,遇到一个需求:用户需要能够为任务配置定时策略,使任务定时执行某个操作。

需求分析

根据需求,我们可以拆解成如下几个步骤:

  • 「某个操作」的实现
  • 配置为定时任务
  • 定时策略可配置
  • 用户体验好

其中步骤 1 与本文无关不提;对于定时任务的实现,在上节Celery异步任务队列 有简单提到 celery 也支持定时任务。

Celery 的定时任务策略配置于代码中,在启动 celery 时写入本地shelve 文件,不利于管理。

因此在 celery 的文档中也提到一个扩展模块 django-celery-beat ,该模块将定时任务的配置写入 Django 配置的数据库中,当程序启动后可以通过 admin 后台进行管理,并且可以直接通过 ORM 对定时任务配置进行修改,无需修改代码然后重启 celery,符合我们预期。

当然还有很多其他库也能实现,因为我们已经使用 celery 执行异步任务,所以本文还是用 django-celery-beat 解决问题。

Celery 的定时任务使用的是类似 crontab 的语法,因此在用户体验上,要考虑普通用户的学习成本,可以提供一些常用的配置,例如每周的工作日每天 1 点执行任务;也要考虑后期的扩展性,可以提供输入框方便配置。

设计与实现

基本用法

定时策略(CrontabSchedule)

CrontabSchedule 支持类 crontab 语法,同样是 5 个配置域,分别为:

  • 每周中的天
  • 每月中的天
  • 每年中的月

每个配置域使用空格隔开。

对每个配置域常用语法:

  • * : 范围内的所有值
  • M-N : M到N之间的值
  • M-N/X*/X : 每X分钟、每X天等等
  • A,B,...,Z : 枚举的值

举个例子: 每个工作日1点执行: 0 1 1-5 * *

创建定时策略代码如下:

from django_celery_beat.models import CrontabSchedule, PeriodicTask
>>> schedule, _ = CrontabSchedule.objects.get_or_create(
... minute='30',
... hour='*',
... day_of_week='*',
... day_of_month='*',
... month_of_year='*',
... )

定时任务

定时任务可以依赖不同的定时策略,例如 crontab, interval 等,创建时指定 schedule 即可。以 crontab 定时任务为例:

>>> import json
>>> from datetime import datetime, timedelta

>>> PeriodicTask.objects.create(
... crontab=schedule,   # we created this above.
... name='Importing contacts',  # simply describes this periodic task.
... task='proj.tasks.import_contacts', # name of task.
... args=json.dumps(['arg1', 'arg2']),
... kwargs=json.dumps({
... 'be_careful': True,
... }),
... expires=datetime.utcnow() + timedelta(seconds=30)
... )

其中 name 为定时任务的名称,每个任务名必须唯一; task 为需要执行的 celery 任务。加上定时策略调度器,这三个是一个定时任务所必须的属性。

定时任务还有其他配置,如 args / kwargs 对应一个 celery 任务的入参; expires 设置了该定时任务的过期时间。

Django配置

最基础的配置只需要在 INSTALLED_APPS 中添加引用,并设置定时任务调度器即可:

settings.py

INSTALLED_APPS = [
 ...
 'django_celery_beat'
]

# 配置 celery 定时任务使用的调度器
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

时区问题

在使用 django-celery-beat 过程中遇到两个关于时区的问题:

创建的定时任务,实际触发时间与配置的时间存在8小时时间差

解决方案:

8小时明显是因为时区不同导致,而 django-celery-beat 对时区的处理似乎总有问题(若不对请指教)。

修改 settings.py 中的时区配置:

settings.py

# 设置 Django 大部分应用通用的时区
TIME_ZONE = 'Asia/Shanghai'
# 关闭 UTC
USE_TZ = False
CELERY_ENABLE_UTC = False
# 设置 django-celery-beat 真正使用的时区
CELERY_TIMEZONE = TIME_ZONE
# 使用 timezone naive 模式
DJANGO_CELERY_BEAT_TZ_AWARE = False

关于 timezone naive 与 timezone aware 模式的区别可以参考文章:Django时区详解

简单来说就是,naive 模式不存储时区信息,只存储经过时区转换后的时间;反之 aware 模式则存储了 UTC 时间和 UTC 时区信息。

根据文档,在修改了时区后,需要将已执行过的定时任务的 last_run_at 重置为 None

python manage.py shell
>>> from django_celery_beat.models import PeriodicTask
>>> PeriodicTask.objects.all().update(last_run_at=None)

修改完成后,重启 celery beat

PS: 就算是经过这样配置,我也仍然遇到了任务不断执行的问题,并且在我多次重启 celery 后不再复现,因此本配置可能还有问题。

数据库中, CrontabScheduletimezone 配置始终是 UTC

解决方案:

查看 CrontabSchedule 模型的源码,找到数据库中 timezone 字段的属性:

class CrontabSchedule(models.Model):
 ...
 timezone = timezone_field.TimeZoneField(
 default='UTC',
 verbose_name=_('Cron Timezone'),
 help_text=_(
  'Timezone to Run the Cron Schedule on. Default is UTC.'),
 )

由于我们在创建 CrontabSchedule 实例时并未指定 timezone ,因此在创建任务时,添加该字段的配置即可:

from django_celery_beat.models import CrontabSchedule
>>> schedule, _ = CrontabSchedule.objects.get_or_create(
... minute='30',
... hour='*',
... day_of_week='*',
... day_of_month='*',
... month_of_year='*',
... timezone='Asia/Shanghai'
... )

*业务前后端设计

本节内容仅供参考,不一定适用其他场景。

前端

设计前端定时任务配置项,包含一个开关,一个三选一单选组件,以及一个输入框:

详解Django定时任务模块设计与实践

为了方便非技术人员设置定时任务,优化用户体验,定时任务除了「自定义」的输入模式,还有一个「每天」与「每周」的选项:

  • 每天:0 1 1-5 * *
  • 每周:0 1 1 * *

单选框与字符串双向绑定,在后端返回上面两个字符串之一时选中每天或每周,否则选中自定义选项。

后端

假设对于我的业务来说,前端需要的任务数据字段为:

{
 "task_id": 1,
 "is_periodic_task": true,
 "periodic_task_id": 1,
 "crontab": "* * * * *"
}

ER 模型如图:

详解Django定时任务模块设计与实践

返回给前端的数据中,若 periodic_task 不为空,则 is_periodic_taskTrue ,并通过 periodic_task.crontab_id 获取到 CrontabSchedule 实例,转化为字符串返回。

要注意, CrontabSchedule__str__ 方法除了返回 crontab 配置,还会返回时区等信息,而这些信息前端展示时并不需要。

因此可以新建一个方法:

def get_crontab_str(contab) -> str:
 """
 获取前端配置需要的 5 项值
 :param contab: CrontabSchedule对象
 :return:
 """
 return '{0} {1} {2} {3} {4}'.format(
 cronexp(contab.minute), cronexp(contab.hour),
 cronexp(contab.day_of_week), cronexp(contab.day_of_month),
 cronexp(contab.month_of_year)
 )

序列化时调用该方法返回给前端即可。

修改任务

修改任务包括以下三种情况

  • 从定时任务改为非定时任务
  • 从非定时任务改为定时任务
  • 在定时任务基础上修改定时策略

对应流程图如下:

1:

详解Django定时任务模块设计与实践

2, 3:

详解Django定时任务模块设计与实践

图中「修改配置中的」指前端传来的修改请求中的新配置信息

具体代码就不赘述,只提一下暂停定时任务的方法:

修改 PeriodicTask.objects.enabledFalse/0 即可

>>> periodic_task.enabled = False
>>> periodic_task.save()

版本说明

详解Django定时任务模块设计与实践

参考

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python简单格式化时间的方法【strftime函数】
Sep 18 Python
Python爬取qq music中的音乐url及批量下载
Mar 23 Python
python如何压缩新文件到已有ZIP文件
Mar 14 Python
python操作excel的包(openpyxl、xlsxwriter)
Jun 11 Python
在python中利用KNN实现对iris进行分类的方法
Dec 11 Python
python、Matlab求定积分的实现
Nov 20 Python
python GUI库图形界面开发之PyQt5工具栏控件QToolBar的详细使用方法与实例
Feb 28 Python
python通用读取vcf文件的类(复制粘贴即可用)
Feb 29 Python
pandas数据拼接的实现示例
Apr 16 Python
Python字典fromkeys()方法使用代码实例
Jul 20 Python
Python:__eq__和__str__函数的使用示例
Sep 26 Python
全网最全python库selenium自动化使用详细教程
Jan 12 Python
Python3中urlencode和urldecode的用法详解
Jul 23 #Python
对python3中的RE(正则表达式)-详细总结
Jul 23 #Python
python正则表达式匹配不包含某几个字符的字符串方法
Jul 23 #Python
python使用百度文字识别功能方法详解
Jul 23 #Python
Python使用type关键字创建类步骤详解
Jul 23 #Python
Python安装selenium包详细过程
Jul 23 #Python
python中列表的切片与修改知识点总结
Jul 23 #Python
You might like
《五等分的花嫁》漫画完结!2020年10月第2期TV动画制作组换血!
2020/03/06 日漫
PHP pathinfo()获得文件的路径、名称等信息说明
2011/09/13 PHP
百度实时推送api接口应用示例
2014/10/21 PHP
深入浅析yii2-gii自定义模板的方法
2016/04/26 PHP
cakephp常见知识点汇总
2017/02/24 PHP
yii2的restful api路由实例详解
2019/05/14 PHP
JavaScript 学习小结(适合新手参考)
2009/07/30 Javascript
jQuery.Validate 使用笔记(jQuery Validation范例 )
2010/06/25 Javascript
jQuery文本框(input textare)事件绑定方法教程
2013/04/24 Javascript
获取非最后一列td值并将title设为该值的方法
2013/10/30 Javascript
Ajax提交与传统表单提交的区别说明
2014/02/07 Javascript
javascript实现禁止复制网页内容汇总
2015/12/30 Javascript
归纳下js面向对象的几种常见写法总结
2016/08/24 Javascript
D3.js实现饼状图的方法详解
2016/09/21 Javascript
jQuery生成假加载动画效果
2016/12/01 Javascript
JavaScript Math对象和调试程序的方法分析
2019/05/13 Javascript
Nodejs中使用puppeteer控制浏览器中视频播放功能
2019/08/26 NodeJs
原生JS与JQ获取元素的区别详解
2020/02/13 Javascript
[06:53]2018DOTA2国际邀请赛寻真——为复仇而来的Newbee
2018/08/15 DOTA
介绍Python中的一些高级编程技巧
2015/04/02 Python
python pyinstaller 加载ui路径方法
2019/06/10 Python
python 3.74 运行import numpy as np 报错lib\site-packages\numpy\__init__.py
2019/10/06 Python
python烟花效果的代码实例
2020/02/25 Python
基于tensorflow for循环 while循环案例
2020/06/30 Python
加拿大百叶窗和窗帘定制网站:Blinds
2017/01/30 全球购物
js正则匹配markdown里的图片标签的实现
2021/03/24 Javascript
致400米运动员广播稿
2014/02/07 职场文书
大学生个人求职口试自我评价
2014/02/16 职场文书
技术合作协议书范本
2014/04/18 职场文书
党员四风问题对照检查材料思想汇报
2014/09/16 职场文书
详细的本科生职业生涯规划范文
2014/09/16 职场文书
教师研修随笔感言
2015/11/18 职场文书
社会实践心得体会范文
2016/01/14 职场文书
MySQL基础(一)
2021/04/05 MySQL
Python绘制分类图的方法
2021/04/20 Python
python 安全地删除列表元素的方法
2022/03/16 Python