celery异步框架
celery
介绍
https://github.com/celery/celery/
https://docs.celeryq.dev/en/stable/
-
celery是一个分布式异步任务框架,是一个灵活且可靠的,处理大量消息的分布式系统,可以在多个节点之间处理某个任务,是一个专注于实时处理的任务队列,支持任务调度,所以 celery 本质上是一个分布式的异步任务调度框架,类似于 Apache 的 airflow,只是用来调度任务的,但它本身并不具备存储任务的功能,而调度任务的时候肯定是要把任务存起来的。因此要使用 celery 的话,还需要搭配一些具备存储、访问功能的工具,比如:消息队列、Redis缓存、数据库等等。
-
celery能做什么
- 定时任务
- 异步任务
- 延迟任务
celery的使用场景
# 1 异步任务 -一些耗时的操作可以交给celery异步执行,而不用等着程序处理完才知道结果。 -视频转码、邮件发送、消息推送等等 # 2 定时任务 -定时推送消息、定时爬取数据、定时统计数据等 # 3 延迟任务 -提交任务后,等待一段时间再执行某个任务
1 Celery 架构,它采用典型的生产者-消费者模式,主要由以下部分组成: 2 Celery Beat,任务调度器,Beat 进程会读取配置文件的内容,周期性地将配置中到期需要执行的任务发送给任务队列。 3 Producer:需要在队列中进行的任务,一般由用户、触发器或其他操作将任务入队,然后交由 workers 进行处理。调用了 Celery 提供的 API、函数或者装饰器而产生任务并交给任务队列处理的都是任务生产者。 4 Broker,即消息中间件,在这指任务队列本身,Celery 扮演生产者和消费者的角色,brokers 就是生产者和消费者存放/获取产品的地方(队列)。 5 Celery Worker,执行任务的消费者,从队列中取出任务并执行。通常会在多台服务器运行多个消费者来提高执行效率。 6 Result Backend:任务处理完后保存状态信息和结果,以供查询。Celery 默认已支持 Redis、RabbitMQ、MongoDB、Django ORM、SQLAlchemy 等方式。 实际应用中,用户从 Web 前端发起一个请求,我们只需要将请求所要处理的任务丢入任务队列 broker 中,由空闲的 worker 去处理任务即可,处理的结果会暂存在后台数据库 backend 中。我们可以在一台机器或多台机器上同时起多个 worker 进程来实现分布式地并行处理任务。
快速使用
# 0 创建Python项目 # 1 创建虚拟环境 # 2 安装celery pip install celery # 3 安装redis(消息队列和结果存储使用redis) pip install redis # 4 安装eventlet(win 平台,如果是mac,linux不需要) pip install eventlet
celery_demo/main.py 主文件
from celery import Celery import time broker = "redis://127.0.0.1:6379/1" backend = "redis://127.0.0.1:6379/2" # 创建app对象 app = Celery("test", broker=broker, backend=backend) @app.task def add(x, y): time.sleep(2) return x + y @app.task def send_msg(mobile, code): print(f"手机号:{mobile},发送短信{code}成功") return "发送成功!"
celery_demo/task_add.py
from main import add, send_msg # 同步执行任务 res = add(4, 5) print(res) # 提交到broker消息队列中,异步执行 res = add.delay(4, 5) print(res) # c1b10e79-37a5-41c5-8fc5-5a189bce1951 # 返回的是任务的id号,任务被提交到消息中间件 broker redis
查看提交的任务
让worker执行任务
# win启动 celery -A main worker -l info -P eventlet # mac linux celery -A main worker -l info
结果存储查看结果
# 1 直接看redis 有数据 # 2 通过代码,拿到结果 from main import app from celery.result import AsyncResult id = 'c1b10e79-37a5-41c5-8fc5-5a189bce1951' if __name__ == '__main__': result = AsyncResult(id=id, app=app) if result.successful(): result = result.get() print(result) # 9 因为之前执行的是add elif result.failed(): print('任务失败') elif result.status == 'PENDING': print('任务等待中被执行') elif result.status == 'RETRY': print('任务异常后正在重试') elif result.status == 'STARTED': print('任务已经开始被执行')
celery包结构
目录结构
celery_demo ├── celery_task # celery包 │ ├── __init__.py # 包文件 │ ├── celery.py # celery连接和配置相关文件,且名字必须叫celery.py │ ├── user_tasks.py # 所有任务函数 │ └── order_tasks.py # 所有任务函数 ├── add_task.py # 添加任务 └── get_result.py # 获取结果
celery.py
from celery import Celery broker = "redis://127.0.0.1:6379/1" backend = "redis://127.0.0.1:6379/2" app = Celery( "testcelery", backend=backend, broker=broker, include=["celery_task.order_tasks", "celery_task.user_tasks"], )
user_tasks.py
from .celery import app import time # 用户相关任务 @app.task def add(x, y): time.sleep(3) return x + y
order_task.py
from .celery import app # 订单相关任务 # 下单成功,发送短信 @app.task def send_sms(mobile, code): print(f"手机号:{mobile},发送成功{code}") return "ok,发送成功!"
add_task.py 提交任务
from celery_task.order_tasks import send_sms # 1 同步调用 # res=send_sms('111111',888) # print(res) # 2 提交到任务队列被 worker执行 res = send_sms.delay("15666777888", 9999) print(res) # 39a9a1f8-9907-4589-a5ba-6b1b291b42ab
get_result.py 查看任务结果
from celery_task.celery import app from celery.result import AsyncResult id = "39a9a1f8-9907-4589-a5ba-6b1b291b42ab" if __name__ == "__main__": result = AsyncResult(id=id, app=app) if result.successful(): result = result.get() print(result) elif result.failed(): print("任务失败") elif result.status == "PENDING": print("任务等待中被执行") elif result.status == "RETRY": print("任务异常后正在重试") elif result.status == "STARTED": print("任务已经开始被执行")
celery异步-延迟-定时任务
异步任务
任务名.delay(参数)
延迟任务
任务名.apply_async(args=[参数],eta=时间对象)
from celery_task.user_tasks import add from datetime import datetime, timedelta # datetime.utcnow() 取utc时间---》默认使用utc时间 # 当前时间加了30s eta = datetime.utcnow() + timedelta(seconds=30) # eta 要放时间对象 res = add.apply_async(args=[5, 6], eta=eta) print(res) # f2dc3e99-4232-48dd-860b-cfe4bb2fe8b7
等了30秒返回结果
定时任务
用beat启动
要写配置文件 在celery中修改
# 时区 app.conf.timezone = 'Asia/Shanghai' # 是否使用UTC app.conf.enable_utc = False # 任务的定时配置 from datetime import timedelta from celery.schedules import crontab app.conf.beat_schedule = { 'add-task': { 'task': 'celery_task.user_task.add', 'schedule': timedelta(seconds=3), # 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点 'args': (300, 150), }, 'send-sms-task': { 'task': 'celery_task.order_task.send_sms', # 'schedule': timedelta(seconds=30), 'schedule': crontab(hour=11,minute=20), # 每天11点20执行 'args': ('189232222',888), }, }
启动beat
只要启动了beat就自动会按照设置的时间提交任务
celery -A celery_task beat -l debug
启动worker
celery -A celery_task worker -l info -P eventlet
django中使用celery
通用方案
1 把项目结构的包直接放到项目根路径中
2 在视图函数中提交任务(异步、延迟、定时)
3 启动worker
4 运行django,正常使用接口
from celery_task.user_tasks import add class CeleryView(GenericViewSet): def list(self, request, *args, **kwargs): res = add.delay(4, 5) return APIResponse(msg=str(res)) # 启动worker celery -A celery_task worker -l info -P eventlet # 运行django,正常使用接口即可 python manage.py runserver
注意:
要在celery.py中配置django的环境变量让celery能识别到
from celery import Celery import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev") broker = "redis://127.0.0.1:6379/1" backend = "redis://127.0.0.1:6379/2" app = Celery( "testcelery", backend=backend, broker=broker, include=["celery_task.order_tasks", "celery_task.user_tasks"], )
启动的时候直接用包名启动即可,在根路径
(luffy) PS D:\2023propygo\luffy_api>
celery -A celery_task worker -l info -P eventlet
celery官方方案
# 1 安装模块 pip install Django==3.2.22 pip install celery pip install redis pip install eventlet #在windows环境下需要安装eventlet包
luffy_api/celery.py
from celery import Celery import django import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev") django.setup() app = Celery('testcelery') app.config_from_object("django.conf:settings",namespace="CELERY") app.autodiscover_tasks()
common_settings.py/dev.py
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/1' # BACKEND配置,使用redis CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2' CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' # 结果序列化方案 CELERY_RESULT_SERIALIZER = 'json' # 任务结果过期时间,秒 CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 # 时区配置 CELERY_TIMEZONE = 'Asia/Shanghai'
luffy_api/user/tasks.py
from celery import shared_task @shared_task def add(x, y): return x + y
views.py
class CeleryTestView(GenericViewSet): def list(self, request, *args, **kwargs): res = add.delay(1,2) return APIResponse(msg=str(res))
启动的时候要使用app名字.celery,比如:
(luffy) PS D:\2023propygo\luffy_api> celery -A luffy_api.celery worker -l info -P eventlet
并发量和qps
# 1 并发量是此刻有多少并发--》请求 # 2 qps:每秒钟响应的数量 # 并发量定了:10,这个接口2s钟响应 -2s能处理10个用户请求 -1s能处理5个用户请求 -qps就是5 -提高qps,如何做? -1 提高并发--》提不了了 -2 提供响应速度--》0.5s响应回去 -3 1s钟就能处理20请求--》qps就是20 # 使用异步,提高项目的qps
定时更新缓存
加入缓存之后,数据库改了,缓存中也要改,缓存双写一致性
celery_task/home_tasks.py
查询出来的数据不带前缀,需要自己手动拼
from .celery import app from home.models import Banner from home.serializer import Bannerserializer from django.conf import settings from django.core.cache import cache @app.task def update_banner(): queryset = ( Banner.objects.all() .filter(is_delete=False, is_show=True) .order_by("orders")[0 : settings.BANNER_COUNT] ) serializer = Bannerserializer(instance=queryset, many=True) for i in serializer.data: i["image"] = "http://127.0.0.1:8000" + i["image"] cache.set("banner_list", serializer.data) return "轮播图缓存更新成功"
celery_task/celery.py
定时任务
包结构要在app的include中注册,才能被检索到
from celery import Celery import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev") broker = "redis://127.0.0.1:6379/1" backend = "redis://127.0.0.1:6379/2" app = Celery( "testcelery", backend=backend, broker=broker, include=["celery_task.home_tasks"], ) # 时区 app.conf.timezone = "Asia/Shanghai" # 是否使用UTC app.conf.enable_utc = False # 任务的定时配置 from datetime import timedelta from celery.schedules import crontab app.conf.beat_schedule = { "banner-task": { "task": "celery_task.home_tasks.update_banner", "schedule": timedelta(seconds=30), "args": (), }, }
启动任务
celery -A celery_task beat -l info
celery -A celery_task worker -l info
task和share_task的区别
django-celery中有两个装饰函数。一个是@task,另一个是@share_task。两者区别在于,前者只能自己这个APP使用。后者是一个全局的配置,多个初始化的APP都可以使用。
task
装饰函数,将函数当成celery的任务函数
import time from celery import Celery broker = "redis://127.0.0.1:6379/1" backend = "redis://127.0.0.1:6379/2" app = Celery( "testcelery", backend=backend, broker=broker, ) @app.task def add(x, y): time.sleep(10) return x + y
share_task
-
装饰函数,将函数当成celery的任务函数
-
不依赖某个celery对象,而是加载到内存之后自动添加到celery对象中
-
与多个celery对象进行关联
from celery import shared_task @shared_task def add(x, y): return x + y
官方方案配置定时任务
dev.py
一定要是tasks才行
CELERY_BEAT_SCHEDULE = { 'every_1_minutes': { 'task': 'home.tasks.add', 'schedule': timedelta(seconds=2), 'args': (1,2) }, }
tasks.py
from celery import shared_task @shared_task def add(x, y): return x + y
celery -A luffy_api.celery worker -l info -P eventlet celery -A luffy_api.celery beat -l debug
admin配置定时任务(手动)
pip install django-celery-beat
dev.py
INSTALLED_APPS = [ .... 'django_celery_beat', ] CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_TZ = True
celery.py
# 配置和django设置中一样的时区 from django.conf import settings app.conf.timezone = settings.TIME_ZONE
# 迁移数据库 python manage.py migrate django_celery_beat
#在两个控制台分别启动woker和beat celery -A luffy_api.celery worker -l debug -P eventlet celery -A luffy_api.celery beat -l debug
访问admin
admin监视任务
- 在控制台监控任务执行情况,还不是很方便,最好是能够通过web界面看到任务的执行情况,如有多少任务在执行,有多少任务执行失败了等
- 这个Celery也是可以做到了,就是将任务执行结果写到数据库中,通过web界面显示出来。
- 这里要用到django-celery-results插件。
- 通过插件可以使用Django的orm作为结果存储,这样的好处在于我们可以直接通过django的数据查看到任务状态,同时为可以制定更多的操作
pip install django-celery-results
INSTALLED_APPS = ( ..., 'django_celery_results', ) # 使用django-orm 作为结果存储 CELERY_RESULT_BACKEND = 'django-db'
# 迁移数据库 python manage.py migrate django_celery_results
访问admin
- 后期后台管理可以自己写
- 可以直接使用django的orm取数据
- 也可以放到redis中,自己写接口处理
Flower监控celery任务
如果不想通django的管理界面监控任务的执行,还可以通过Flower插件来进行任务的监控。Flower的界面更加丰富,可以监控的信息更全
Flower 是一个用于监控和管理 Celery 集群的开源 Web 应用程序。它提供有关 Celery workers 和tasks状态的实时信息
Flower可以:
1 实时监控celery的Events
-查看任务进度和历史记录
-查看任务详细信息(参数、开始时间、运行时间等)2 远程操作
-查看workers 状态和统计数据
-关闭并重新启动workers 实例
-控制工作池大小和自动缩放设置
-查看和修改工作实例消耗的队列
-查看当前正在运行的任务
-查看计划任务(预计到达时间/倒计时)
-查看保留和撤销的任务
-应用时间和速率限制
-撤销或终止任务3 Broker 监控
-查看所有 Celery 队列的统计信息
pip install flower
# 方式一 celery -A celery_demo flower --port-5555 # 方式二 celery --broker=redis://127.0.0.1:6379/1 flower # 浏览器访问 http://127.0.0.1:5555/
任务异常自动告警
虽然可以通过界面来监控了,但是我们想要得更多,人不可能天天盯着界面看吧,如果能实现任务执行失败就自动发邮件告警就好了。这个Celery当然也是没有问题的。
通过钩子程序在异常的时候触发邮件通知
tasks.py
SendEmailTask
继承自Task
基类。on_success
:当任务成功完成时被调用,发送成功邮件并打印成功信息。on_failure
:当任务失败时被调用,发送失败邮件并打印失败信息。on_retry
:当任务重试时被调用,打印重试信息(这里不发送邮件)。add
任务继承了SendEmailTask
基类,因此它会在任务成功或失败时触发相应的邮件通知。
from celery import shared_task import time from celery import Task from django.core.mail import send_mail from django.conf import settings # 成功失败邮件告警 class SendEmailTask(Task): def on_success(self, retval, task_id, args, kwargs): info = f'任务成功-- 任务id是:{task_id} , 参数是:{args} , 执行成功 !' send_mail('celery任务监控成功告警', info, settings.EMAIL_HOST_USER, ["ssrheart@outlook.com",]) print('------------成功') def on_failure(self, exc, task_id, args, kwargs, einfo): info = f'任务失败-- 任务id为:{task_id} , 参数为:{args} , 失败 ! 失败信息为: {exc}' send_mail('celery任务监控失败告警', info, settings.EMAIL_HOST_USER, ["ssrheart@outlook.com",]) print('------------失败') def on_retry(self, exc, task_id, args, kwargs, einfo): print(f'任务id位::{task_id} , 参数为:{args} , 重试了 ! 错误信息为: {exc}') @shared_task(base=SendEmailTask, bind=True) def add(a,b): time.sleep(1) return a+b # celery -A celery_demo worker -l debug -P eventlet # celery -A celery_demo beat -l debug # celery -A celery_demo flower --port-5566
补充
这个地方会报错,DNS,IP的错误,使用gevent即可解决
pip install gevent
celery -A
worker -l info -P gevent 参考至:https://blog.csdn.net/qq_18664637/article/details/89478560
django发送邮件
# 1 邮箱开启smtp # 2 django配置文件配置 ### 发送邮件 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.qq.com' # 如果是 qq 改成 smtp.qq.com EMAIL_PORT = 465 EMAIL_HOST_USER = '446367977@qq.com' # 帐号 EMAIL_HOST_PASSWORD = '' # 密码 DEFAULT_FROM_EMAIL = EMAIL_HOST_USER #这样收到的邮件,收件人处就会这样显示 #DEFAULT_FROM_EMAIL = 'heart<'446367977@qq.com>' EMAIL_USE_SSL = True #使用ssl #EMAIL_USE_TLS = False # 使用tls #EMAIL_USE_SSL 和 EMAIL_USE_TLS 是互斥的,即只能有一个为 True # 3 发送邮件 from django.core.mail import send_mail class EmailView(ViewSet): def list(self, request, *args, **kwargs): to_user = request.query_params.get('to_user') send_mail('test1', 'test', settings.EMAIL_HOST_USER, [to_user, ]) return APIResponse(msg=f'邮件已经发送:{str(to_user)}')
异步秒杀方案
# 秒杀功能 - qps要高:承载住很多用户1s内把功能完成 -创建订单 -扣减库存 - 效率要高 # 同步秒杀 -假设秒杀需要10s钟,项目并发量是3,总共5件商品要秒杀 -10s内,只有3个人能进入到系统,并且开始秒杀 # 异步秒杀 -假设秒杀需要10s,项目并发量是3,总共5个商品要秒杀 -使用异步,用户提交后,立马返回 -10s内,可以响应很多很多用户提交秒杀任务:假设提交了100个用户 -这100个用户中只有5个成功
同步秒杀
前端
<template> <div> <Header></Header> <div style="padding: 50px;margin-left: 100px"> <h1>Go语言课程</h1> <img src="http://blog.ssrheart.top/img/202405171633804.png" height="300px" width="300px"> <br> <el-button type="danger" @click="handleSeckill" v-loading.fullscreen.lock="fullscreenLoading">秒杀课程 </el-button> </div> <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br> <Footer></Footer> </div> </template> <script setup> import Header from "../components/Headers.vue" import Footer from "../components/Footer.vue" import {ref} from "vue" import axios from "axios"; const fullscreenLoading = ref(false) const task_id = ref('') let t = null const handleSeckill = () => { //同步秒杀 fullscreenLoading.value = true; axios.post('http://127.0.0.1:8000/api/v1/home/seckill/seckill/', { course_id: '99', }).then(res => { fullscreenLoading.value = false; alert(res.data.msg) }).catch(err => { this.fullscreenLoading = false; alert(err) }) } </script>
后端
class SeckillView(ViewSet): @action(methods=['POST'], detail=False) def seckill(self, request, *args, **kwargs): ''' #1 取出传入的 课程id #2 查询课程 是否还有剩余 1s #2.1 有剩余,开始下单扣减库存 1s #2.2,在订单表中生成一条记录 2s #2.3 秒杀成功返回给前端 #3 课程没有剩余,秒杀失败,返回给前端 ''' course_id = request.data.get('course_id') # print('根据课程id:%s,查询课程是否还有剩余,耗时3s' % course_id) time.sleep(1) res = random.choice([True, False]) if res: # 库存够 print('扣减库存,耗时3s') time.sleep(1) print('下单,耗时4s') time.sleep(2) return APIResponse(msg='恭喜您秒到了') else: return APIResponse(code=101, msg='库存不足,秒杀失败')
异步秒杀
前端
<template> <div> <Header></Header> <div style="padding: 50px;margin-left: 100px"> <h1>Go语言课程</h1> <img src="http://blog.ssrheart.top/img/202405171633804.png" height="300px" width="300px"> <br> <el-button type="danger" @click="handleSeckill" v-loading.fullscreen.lock="fullscreenLoading">秒杀课程 </el-button> </div> <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br> <Footer></Footer> </div> </template> <script setup> import Header from "../components/Headers.vue" import Footer from "../components/Footer.vue" import {ref} from "vue" import axios from "axios"; const fullscreenLoading = ref(false) const task_id = ref('') let t = null const handleSeckill = () => { // 异步秒杀 fullscreenLoading.value = true; axios.post('http://127.0.0.1:8000/api/v1/home/seckill/seckill/',{ course_id: '99' }).then(res => { // 在排队,转圈的,还需要继续显示 alert(res.data.msg) task_id.value = res.data.task_id // 继续发送请求---》查询是否秒杀成功:1 成功 2 没成功 3 秒杀任务还没执行 // 启动定时任务,每隔1s,向后端发送一次请求 t = setInterval(() => { axios.get('http://127.0.0.1:8000/api/v1/home/seckill/get_result/', { params:{ task_id: task_id.value } }).then(res => { // 100 成功,success : 1 成功 0 失败 2 还没开始 if (res.data.success == '1') { // 转圈框不显示 fullscreenLoading.value = false; // 停止定时任务 clearInterval(t) t = null alert(res.data.msg) } else if (res.data.success == '0') { // 转圈框不显示 fullscreenLoading.value = false; // 停止定时任务 clearInterval(t) t = null alert(res.data.msg) } else { // alert(res.msg) console.log(res.msg) } }) }, 1000) }).catch(err => { fullscreenLoading.value = false; alert(err) }) } </script> <style scoped> </style>
后端
class SeckillView(ViewSet): @action(methods=['POST'], detail=False) def seckill(self, request, *args, **kwargs): course_id = request.data.get('course_id') task_id = seckill.delay(course_id) return APIResponse(msg='您正在排队', task_id=str(task_id)) @action(methods=['GET'], detail=False) def get_result(self, request, *args, **kwargs): task_id = request.query_params.get('task_id') a = AsyncResult(id=task_id) if a.successful(): result = a.get() # True 和 False if result: return APIResponse(success='1', msg='秒杀成功') else: return APIResponse(success='0', msg='秒杀失败') elif a.status == 'PENDING': print('任务等待中被执行') return APIResponse(success='2', msg='任务等待中被执行') else: return APIResponse(success='3', msg='秒杀任务正在执行')
tasks.py
from celery import shared_task import time,random @shared_task def seckill(course_id): print('根据课程id:%s,查询课程是否还有剩余,耗时2s' % course_id) time.sleep(2) res = random.choice([True, False]) if res: # 库存够 print('扣减库存,耗时1s') time.sleep(1) print('下单,耗时2s') time.sleep(2) return True else: return False
本文作者:ssrheart
本文链接:https://www.cnblogs.com/ssrheart/p/18199287
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步