詳解Django定時任務(wù)模塊設(shè)計與實踐
在開發(fā)后臺與任務(wù)相關(guān)的功能中,遇到一個需求:用戶需要能夠為任務(wù)配置定時策略,使任務(wù)定時執(zhí)行某個操作。
需求分析
根據(jù)需求,我們可以拆解成如下幾個步驟:
- 「某個操作」的實現(xiàn)
- 配置為定時任務(wù)
- 定時策略可配置
- 用戶體驗好
其中步驟 1 與本文無關(guān)不提;對于定時任務(wù)的實現(xiàn),在上節(jié)Celery異步任務(wù)隊列 有簡單提到 celery 也支持定時任務(wù)。
Celery 的定時任務(wù)策略配置于代碼中,在啟動 celery 時寫入本地shelve 文件,不利于管理。
因此在 celery 的文檔中也提到一個擴(kuò)展模塊 django-celery-beat ,該模塊將定時任務(wù)的配置寫入 Django 配置的數(shù)據(jù)庫中,當(dāng)程序啟動后可以通過 admin 后臺進(jìn)行管理,并且可以直接通過 ORM 對定時任務(wù)配置進(jìn)行修改,無需修改代碼然后重啟 celery,符合我們預(yù)期。
當(dāng)然還有很多其他庫也能實現(xiàn),因為我們已經(jīng)使用 celery 執(zhí)行異步任務(wù),所以本文還是用 django-celery-beat 解決問題。
Celery 的定時任務(wù)使用的是類似 crontab 的語法,因此在用戶體驗上,要考慮普通用戶的學(xué)習(xí)成本,可以提供一些常用的配置,例如每周的工作日每天 1 點執(zhí)行任務(wù);也要考慮后期的擴(kuò)展性,可以提供輸入框方便配置。
設(shè)計與實現(xiàn)
基本用法
定時策略(CrontabSchedule)
CrontabSchedule 支持類 crontab 語法,同樣是 5 個配置域,分別為:
- 分
- 時
- 每周中的天
- 每月中的天
- 每年中的月
每個配置域使用空格隔開。
對每個配置域常用語法:
*: 范圍內(nèi)的所有值M-N: M到N之間的值M-N/X或*/X: 每X分鐘、每X天等等A,B,...,Z: 枚舉的值
舉個例子: 每個工作日1點執(zhí)行: 0 1 1-5 * *
創(chuàng)建定時策略代碼如下:
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='*', ... )
定時任務(wù)
定時任務(wù)可以依賴不同的定時策略,例如 crontab, interval 等,創(chuàng)建時指定 schedule 即可。以 crontab 定時任務(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 為定時任務(wù)的名稱,每個任務(wù)名必須唯一; task 為需要執(zhí)行的 celery 任務(wù)。加上定時策略調(diào)度器,這三個是一個定時任務(wù)所必須的屬性。
定時任務(wù)還有其他配置,如 args / kwargs 對應(yīng)一個 celery 任務(wù)的入?yún)ⅲ?expires 設(shè)置了該定時任務(wù)的過期時間。
Django配置
最基礎(chǔ)的配置只需要在 INSTALLED_APPS 中添加引用,并設(shè)置定時任務(wù)調(diào)度器即可:
settings.py
INSTALLED_APPS = [ ... 'django_celery_beat' ] # 配置 celery 定時任務(wù)使用的調(diào)度器 CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
時區(qū)問題
在使用 django-celery-beat 過程中遇到兩個關(guān)于時區(qū)的問題:
創(chuàng)建的定時任務(wù),實際觸發(fā)時間與配置的時間存在8小時時間差
解決方案:
8小時明顯是因為時區(qū)不同導(dǎo)致,而 django-celery-beat 對時區(qū)的處理似乎總有問題(若不對請指教)。
修改 settings.py 中的時區(qū)配置:
settings.py
# 設(shè)置 Django 大部分應(yīng)用通用的時區(qū) TIME_ZONE = 'Asia/Shanghai' # 關(guān)閉 UTC USE_TZ = False CELERY_ENABLE_UTC = False # 設(shè)置 django-celery-beat 真正使用的時區(qū) CELERY_TIMEZONE = TIME_ZONE # 使用 timezone naive 模式 DJANGO_CELERY_BEAT_TZ_AWARE = False
關(guān)于 timezone naive 與 timezone aware 模式的區(qū)別可以參考文章:Django時區(qū)詳解
簡單來說就是,naive 模式不存儲時區(qū)信息,只存儲經(jīng)過時區(qū)轉(zhuǎn)換后的時間;反之 aware 模式則存儲了 UTC 時間和 UTC 時區(qū)信息。
根據(jù)文檔,在修改了時區(qū)后,需要將已執(zhí)行過的定時任務(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)過這樣配置,我也仍然遇到了任務(wù)不斷執(zhí)行的問題,并且在我多次重啟 celery 后不再復(fù)現(xiàn),因此本配置可能還有問題。
數(shù)據(jù)庫中, CrontabSchedule 的 timezone 配置始終是 UTC
解決方案:
查看 CrontabSchedule 模型的源碼,找到數(shù)據(jù)庫中 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.'),
)
由于我們在創(chuàng)建 CrontabSchedule 實例時并未指定 timezone ,因此在創(chuàng)建任務(wù)時,添加該字段的配置即可:
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è)計
本節(jié)內(nèi)容僅供參考,不一定適用其他場景。
前端
設(shè)計前端定時任務(wù)配置項,包含一個開關(guān),一個三選一單選組件,以及一個輸入框:

為了方便非技術(shù)人員設(shè)置定時任務(wù),優(yōu)化用戶體驗,定時任務(wù)除了「自定義」的輸入模式,還有一個「每天」與「每周」的選項:
- 每天:0 1 1-5 * *
- 每周:0 1 1 * *
單選框與字符串雙向綁定,在后端返回上面兩個字符串之一時選中每天或每周,否則選中自定義選項。
后端
假設(shè)對于我的業(yè)務(wù)來說,前端需要的任務(wù)數(shù)據(jù)字段為:
{
"task_id": 1,
"is_periodic_task": true,
"periodic_task_id": 1,
"crontab": "* * * * *"
}
ER 模型如圖:

返回給前端的數(shù)據(jù)中,若 periodic_task 不為空,則 is_periodic_task 為 True ,并通過 periodic_task.crontab_id 獲取到 CrontabSchedule 實例,轉(zhuǎn)化為字符串返回。
要注意, CrontabSchedule 的 __str__ 方法除了返回 crontab 配置,還會返回時區(qū)等信息,而這些信息前端展示時并不需要。
因此可以新建一個方法:
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)
)
序列化時調(diào)用該方法返回給前端即可。
修改任務(wù)
修改任務(wù)包括以下三種情況
- 從定時任務(wù)改為非定時任務(wù)
- 從非定時任務(wù)改為定時任務(wù)
- 在定時任務(wù)基礎(chǔ)上修改定時策略
對應(yīng)流程圖如下:
1:

2, 3:

圖中「修改配置中的」指前端傳來的修改請求中的新配置信息
具體代碼就不贅述,只提一下暫停定時任務(wù)的方法:
修改 PeriodicTask.objects.enabled 為 False/0 即可
>>> periodic_task.enabled = False >>> periodic_task.save()
版本說明

參考
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/
http://www.dbjr.com.cn/article/166085.htm
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
使用Python實現(xiàn)插入100萬條數(shù)據(jù)到MySQL數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了如何使用Python實現(xiàn)插入100萬條數(shù)據(jù)到MySQL數(shù)據(jù)庫,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-04-04
python使用itchat實現(xiàn)手機(jī)控制電腦
這篇文章主要為大家詳細(xì)介紹了python使用itchat實現(xiàn)手機(jī)控制電腦,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-02-02
一文詳細(xì)介紹Python中的OrderedDict對象
OrderedDict是Python標(biāo)準(zhǔn)庫collections模塊的一部分,下面這篇文章主要給大家介紹了關(guān)于Python中OrderedDict對象的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08

