详解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之坑爹的字符编码
Sep 28 Python
python实现TCP服务器端与客户端的方法详解
Apr 30 Python
python判断给定的字符串是否是有效日期的方法
May 13 Python
Python实现并行抓取整站40万条房价数据(可更换抓取城市)
Dec 14 Python
Python实现1-9数组形成的结果为100的所有运算式的示例
Nov 03 Python
Python实现最大子序和的方法示例
Jul 05 Python
python3+django2开发一个简单的人员管理系统过程详解
Jul 23 Python
docker-py 用Python调用Docker接口的方法
Aug 30 Python
解决python ThreadPoolExecutor 线程池中的异常捕获问题
Apr 08 Python
Python基础教程(一)——Windows搭建开发Python开发环境
Jul 20 Python
使用python求斐波那契数列中第n个数的值示例代码
Jul 26 Python
python利用while求100内的整数和方式
Nov 07 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
php中动态调用函数的方法
2015/03/16 PHP
变量在 PHP7 内部的实现(二)
2015/12/21 PHP
基于php编程规范(详解)
2017/08/17 PHP
thinkPHP5实现数据库添加内容的方法
2017/10/25 PHP
php+iframe 实现上传文件功能示例
2020/03/04 PHP
PHP利用curl发送HTTP请求的实例代码
2020/07/09 PHP
CL vs ForZe BO5 第三场 2.13
2021/03/10 DOTA
JavaScript判断数组重复内容的两种方法(推荐)
2016/06/06 Javascript
详解vue 中使用 AJAX获取数据的方法
2017/01/18 Javascript
js仿拉勾网首页穿墙广告效果
2017/03/08 Javascript
如何将 jQuery 从你的 Bootstrap 项目中移除(取而代之使用Vue.js)
2017/07/17 jQuery
javaScript 连接打印机,打印小票的实例
2017/12/29 Javascript
微信小程序开发背景图显示功能
2018/08/08 Javascript
微信小程序生成二维码的示例代码
2019/03/29 Javascript
Ant-design-vue Table组件customRow属性的使用说明
2020/10/28 Javascript
Python实现向服务器请求压缩数据及解压缩数据的方法示例
2017/06/09 Python
Python实现求两个csv文件交集的方法
2017/09/06 Python
对Python中的@classmethod用法详解
2018/04/21 Python
基于anaconda下强大的conda命令介绍
2018/06/11 Python
mac下pycharm设置python版本的图文教程
2018/06/13 Python
Python格式化输出字符串方法小结【%与format】
2018/10/29 Python
python爬取网易云音乐评论
2018/11/16 Python
对python3标准库httpclient的使用详解
2018/12/18 Python
Opencv+Python实现图像运动模糊和高斯模糊的示例
2019/04/11 Python
python 实现在一张图中绘制一个小的子图方法
2019/07/07 Python
pytorch构建多模型实例
2020/01/15 Python
Python3+Selenium+Chrome实现自动填写WPS表单
2020/02/12 Python
python脚本第一行如何写
2020/08/30 Python
Selenium结合BeautifulSoup4编写简单的python爬虫
2020/11/06 Python
css3 中实现炫酷的loading效果
2019/04/26 HTML / CSS
简述synchronized和java.util.concurrent.locks.Lock的异同
2014/12/08 面试题
Ref与out有什么不同
2012/11/24 面试题
大学团日活动总结书
2015/05/11 职场文书
优秀范文:《但愿人长久》教学反思3篇
2019/10/24 职场文书
JAVA API 实用类 String详解
2021/10/05 Java/Android
Nginx虚拟主机的搭建的实现步骤
2022/01/18 Servers