溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

發(fā)布時(shí)間:2020-09-20 06:32:43 來(lái)源:腳本之家 閱讀:234 作者:嚴(yán)北 欄目:開(kāi)發(fā)技術(shù)

在開(kāi)發(fā)后臺(tái)與任務(wù)相關(guān)的功能中,遇到一個(gè)需求:用戶需要能夠?yàn)槿蝿?wù)配置定時(shí)策略,使任務(wù)定時(shí)執(zhí)行某個(gè)操作。

需求分析

根據(jù)需求,我們可以拆解成如下幾個(gè)步驟:

  • 「某個(gè)操作」的實(shí)現(xiàn)
  • 配置為定時(shí)任務(wù)
  • 定時(shí)策略可配置
  • 用戶體驗(yàn)好

其中步驟 1 與本文無(wú)關(guān)不提;對(duì)于定時(shí)任務(wù)的實(shí)現(xiàn),在上節(jié)Celery異步任務(wù)隊(duì)列 有簡(jiǎn)單提到 celery 也支持定時(shí)任務(wù)。

Celery 的定時(shí)任務(wù)策略配置于代碼中,在啟動(dòng) celery 時(shí)寫(xiě)入本地shelve 文件,不利于管理。

因此在 celery 的文檔中也提到一個(gè)擴(kuò)展模塊 django-celery-beat ,該模塊將定時(shí)任務(wù)的配置寫(xiě)入 Django 配置的數(shù)據(jù)庫(kù)中,當(dāng)程序啟動(dòng)后可以通過(guò) admin 后臺(tái)進(jìn)行管理,并且可以直接通過(guò) ORM 對(duì)定時(shí)任務(wù)配置進(jìn)行修改,無(wú)需修改代碼然后重啟 celery,符合我們預(yù)期。

當(dāng)然還有很多其他庫(kù)也能實(shí)現(xiàn),因?yàn)槲覀円呀?jīng)使用 celery 執(zhí)行異步任務(wù),所以本文還是用 django-celery-beat 解決問(wèn)題。

Celery 的定時(shí)任務(wù)使用的是類似 crontab 的語(yǔ)法,因此在用戶體驗(yàn)上,要考慮普通用戶的學(xué)習(xí)成本,可以提供一些常用的配置,例如每周的工作日每天 1 點(diǎn)執(zhí)行任務(wù);也要考慮后期的擴(kuò)展性,可以提供輸入框方便配置。

設(shè)計(jì)與實(shí)現(xiàn)

基本用法

定時(shí)策略(CrontabSchedule)

CrontabSchedule 支持類 crontab 語(yǔ)法,同樣是 5 個(gè)配置域,分別為:

  • 時(shí)
  • 每周中的天
  • 每月中的天
  • 每年中的月

每個(gè)配置域使用空格隔開(kāi)。

對(duì)每個(gè)配置域常用語(yǔ)法:

  • * : 范圍內(nèi)的所有值
  • M-N : M到N之間的值
  • M-N/X*/X : 每X分鐘、每X天等等
  • A,B,...,Z : 枚舉的值

舉個(gè)例子: 每個(gè)工作日1點(diǎn)執(zhí)行: 0 1 1-5 * *

創(chuàng)建定時(shí)策略代碼如下:

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='*',
... )

定時(shí)任務(wù)

定時(shí)任務(wù)可以依賴不同的定時(shí)策略,例如 crontab, interval 等,創(chuàng)建時(shí)指定 schedule 即可。以 crontab 定時(shí)任務(wù)為例:

>>> 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 為定時(shí)任務(wù)的名稱,每個(gè)任務(wù)名必須唯一; task 為需要執(zhí)行的 celery 任務(wù)。加上定時(shí)策略調(diào)度器,這三個(gè)是一個(gè)定時(shí)任務(wù)所必須的屬性。

定時(shí)任務(wù)還有其他配置,如 args / kwargs 對(duì)應(yīng)一個(gè) celery 任務(wù)的入?yún)ⅲ?expires 設(shè)置了該定時(shí)任務(wù)的過(guò)期時(shí)間。

Django配置

最基礎(chǔ)的配置只需要在 INSTALLED_APPS 中添加引用,并設(shè)置定時(shí)任務(wù)調(diào)度器即可:

settings.py

INSTALLED_APPS = [
 ...
 'django_celery_beat'
]

# 配置 celery 定時(shí)任務(wù)使用的調(diào)度器
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

時(shí)區(qū)問(wèn)題

在使用 django-celery-beat 過(guò)程中遇到兩個(gè)關(guān)于時(shí)區(qū)的問(wèn)題:

創(chuàng)建的定時(shí)任務(wù),實(shí)際觸發(fā)時(shí)間與配置的時(shí)間存在8小時(shí)時(shí)間差

解決方案:

8小時(shí)明顯是因?yàn)闀r(shí)區(qū)不同導(dǎo)致,而 django-celery-beat 對(duì)時(shí)區(qū)的處理似乎總有問(wèn)題(若不對(duì)請(qǐng)指教)。

修改 settings.py 中的時(shí)區(qū)配置:

settings.py

# 設(shè)置 Django 大部分應(yīng)用通用的時(shí)區(qū)
TIME_ZONE = 'Asia/Shanghai'
# 關(guān)閉 UTC
USE_TZ = False
CELERY_ENABLE_UTC = False
# 設(shè)置 django-celery-beat 真正使用的時(shí)區(qū)
CELERY_TIMEZONE = TIME_ZONE
# 使用 timezone naive 模式
DJANGO_CELERY_BEAT_TZ_AWARE = False

關(guān)于 timezone naive 與 timezone aware 模式的區(qū)別可以參考文章:Django時(shí)區(qū)詳解

簡(jiǎn)單來(lái)說(shuō)就是,naive 模式不存儲(chǔ)時(shí)區(qū)信息,只存儲(chǔ)經(jīng)過(guò)時(shí)區(qū)轉(zhuǎn)換后的時(shí)間;反之 aware 模式則存儲(chǔ)了 UTC 時(shí)間和 UTC 時(shí)區(qū)信息。

根據(jù)文檔,在修改了時(shí)區(qū)后,需要將已執(zhí)行過(guò)的定時(shí)任務(wù)的 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: 就算是經(jīng)過(guò)這樣配置,我也仍然遇到了任務(wù)不斷執(zhí)行的問(wèn)題,并且在我多次重啟 celery 后不再?gòu)?fù)現(xiàn),因此本配置可能還有問(wèn)題。

數(shù)據(jù)庫(kù)中, CrontabScheduletimezone 配置始終是 UTC

解決方案:

查看 CrontabSchedule 模型的源碼,找到數(shù)據(jù)庫(kù)中 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.'),
 )

由于我們?cè)趧?chuàng)建 CrontabSchedule 實(shí)例時(shí)并未指定 timezone ,因此在創(chuàng)建任務(wù)時(shí),添加該字段的配置即可:

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'
... )

*業(yè)務(wù)前后端設(shè)計(jì)

本節(jié)內(nèi)容僅供參考,不一定適用其他場(chǎng)景。

前端

設(shè)計(jì)前端定時(shí)任務(wù)配置項(xiàng),包含一個(gè)開(kāi)關(guān),一個(gè)三選一單選組件,以及一個(gè)輸入框:

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

為了方便非技術(shù)人員設(shè)置定時(shí)任務(wù),優(yōu)化用戶體驗(yàn),定時(shí)任務(wù)除了「自定義」的輸入模式,還有一個(gè)「每天」與「每周」的選項(xiàng):

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

單選框與字符串雙向綁定,在后端返回上面兩個(gè)字符串之一時(shí)選中每天或每周,否則選中自定義選項(xiàng)。

后端

假設(shè)對(duì)于我的業(yè)務(wù)來(lái)說(shuō),前端需要的任務(wù)數(shù)據(jù)字段為:

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

ER 模型如圖:

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

返回給前端的數(shù)據(jù)中,若 periodic_task 不為空,則 is_periodic_taskTrue ,并通過(guò) periodic_task.crontab_id 獲取到 CrontabSchedule 實(shí)例,轉(zhuǎn)化為字符串返回。

要注意, CrontabSchedule__str__ 方法除了返回 crontab 配置,還會(huì)返回時(shí)區(qū)等信息,而這些信息前端展示時(shí)并不需要。

因此可以新建一個(gè)方法:

def get_crontab_str(contab) -> str:
 """
 獲取前端配置需要的 5 項(xiàng)值
 :param contab: CrontabSchedule對(duì)象
 :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)
 )

序列化時(shí)調(diào)用該方法返回給前端即可。

修改任務(wù)

修改任務(wù)包括以下三種情況

  • 從定時(shí)任務(wù)改為非定時(shí)任務(wù)
  • 從非定時(shí)任務(wù)改為定時(shí)任務(wù)
  • 在定時(shí)任務(wù)基礎(chǔ)上修改定時(shí)策略

對(duì)應(yīng)流程圖如下:

1:

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

2, 3:

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

圖中「修改配置中的」指前端傳來(lái)的修改請(qǐng)求中的新配置信息

具體代碼就不贅述,只提一下暫停定時(shí)任務(wù)的方法:

修改 PeriodicTask.objects.enabledFalse/0 即可

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

版本說(shuō)明

詳解Django定時(shí)任務(wù)模塊設(shè)計(jì)與實(shí)踐

參考

http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html
https://django-celery-beat.readthedocs.io/en/latest/
https://docs.djangoproject.com/en/2.2/topics/i18n/timezones/
https://www.jb51.net/article/166085.htm

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI