celery的使用
一. Celery简介
Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度。
Celery的架构由三部分组成,消息中间件(message broker),任务执行单元(worker)和任务执行结果存储(task result store)组成。
- 消息中间件:Celery本身不提供消息服务,但是可以方便的和第三方提供的消息中间件集成。包括,RabbitMQ, Redis等等。
- 任务执行单元:Worker是Celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中。
- 任务结果存储:Task result store用来存储Worker执行的任务的结果,Celery支持以不同方式存储任务的结果,包括AMQP, redis等。
Celery多用来执行异步任务,将耗时的操作交由Celery去异步执行,比如发送邮件、短信、消息推送、音视频处理等。还可以执行定时任务,定时执行某件事情,比如Redis中的数据每天凌晨两点保存至mysql数据库,实现Redis的持久化。
更详细的技术文档,请访问官网 http://www.celeryproject.org/。
任务队列
任务队列是一种在线程或机器间分发任务的机制。
消息队列
消息队列的输入是工作的一个单元,称为任务,独立的职程(Worker)进程持续监视队列中是否有需要处理的新任务。
Celery 用消息通信,通常使用中间人(Broker)在客户端和职程间斡旋。这个过程从客户端向队列添加消息开始,之后中间人把消息派送给职程,职程对消息进行处理。如下图所示:
Celery 系统可包含多个职程和中间人,以此获得高可用性和横向扩展能力。
在使用 Celery 之前请务必理解以下概念:
a. Celery Beat: 任务调度器,Beat 进程会读取配置文件的内容,周期性的将配置中到期需要执行的任务发送给任务队列
b. Celery Worker: 执行任务的消费者,通常会在多台服务器运行多个消费者来提高运行效率。
c. Broker: 消息代理,也是任务队列本身(通常是消息队列或者数据库),通常称为消息中间件,接收任务生产者发送过来的任务消息,存进队列再按序分发给任务消费方。
d. Producer: 任务生产者,调用 Celery API 的函数或者装饰器而产生任务并交给任务队列处理的都是任务生产者。
配置项说明:
Celery 是通过配置文件中的配置项来定制任务的。
CELERY_IMPORTS: 配置导入哥哥任务的代码模块
CELERY_QUEUES: 定义任务执行的各个任务队列(如按照执行时间分slow、fast等),默认有一个队列,暂称为一般任务队列。
CELERY_ROUTES: 配置各个任务分配到不同的任务队列
CELERY_SCHEDULE: 配置各个任务执行的时机参数
CELERY_TIMEZONE: 设置时区
CELERY_ENABLE_UTC: 是否启动时区设置,默认值是True
CELERY_CONCURRENCY: 并发的worker数量
CELERY_PREFETCH_MULTIPLIER: 每次去消息队列读取任务的数量,默认值是4
CELERY_MAX_TASKS_PRE_CHILD: 每个worker执行多少次任务后会死掉
BROKER_URL: 使用redis作为任务队列
CELERY_TASK_RESULT_EXPIRES: 任务执行结果的超时时间
CELERY_TASK_TIME_LIMIT: 单个任务运行的时间限制,超时会被杀死,不建议使用该参数,而用CELERY_TASK_SOFT_TIME_LIMIT
CELERY_RESULT_TACKEND: 使用redis存储执行结果
CELERY_TASK_SERIALIZER: 任务序列化方式
CELERY_RESULT_SERIALIZER: 任务执行结果序列化方式
CELERY_DISABLE_RATE_LIMITS: 关闭执行限速
二. Celery的快速使用
安装celery及配置Redis
pip install celery pip install django-redis # Windows中还需要安装以下模块,用于任务执行单元 pip install eventlet
Celery执行求和
第一步:新建一个py文件——main.py
from celery import Celery # 提交的异步任务,放在这里 broker = 'redis://127.0.0.1:6379/1' # 执行完的结果,放在这里 backend = 'redis://127.0.0.1:6379/2'
# Celery实例化得到对象 app = Celery('test',broker=broker,backend=backend) @app.task def add(a,b): import time time.sleep(3) return a+b
第二步:其他程序提交任务
# task.py from main import add '''提交任务''' # 同步调用 # res = add(3,5) # print(res) # 8 直接出来结果 # 异步调用 res = add.delay(6,8) print(res) # 4d893986-fbfd-40d2-a74d-7b6fa0d544c4 出来的是任务id
第三步:启动worker
# 注意:windows启动worker命令,需要安装eventlet pip install eventlet # 启动worker命令 win: # 4.x之前版本 celery worker -A main -l info -P eventlet # 4.x之后 celery -A main worker -l info -P eventlet mac: celery -A main worker -l info
第四步:worker会执行消息中间件中的任务,把结果存起来
第五步:查看执行结果
# get_result.py from main import app from celery.result import AsyncResult # 填入任务id id ='4d893986-fbfd-40d2-a74d-7b6fa0d544c4' if __name__ == '__main__': a = AsyncResult(id=id,app=app) if a.successful(): result = a.get() print(result) elif a.failed(): print('任务失败') elif a.status == 'PENDING': print('任务等待中') elif a.status == 'RETRY': print('任务异常后正在重试') elif a.status == 'STARTED': print('任务已经开始被执行')
首先在django中,celery要建立以下目录结构:
project ├── celery_task# celery相关文件夹 │ ├── celery.py # celery连接和配置相关文件,必须叫这个名字 │ └── tasks1.py # 所有任务函数 │ └── tasks2.py # 所有任务函数 ├── check_result.py # 检查结果 └── send_task.py # 触发任务
注意:检查结果与触发任务的模块不能写在celery_task模块中,不然会报导入celery的错误。
比如这里建的目录如下:
# celery.py from celery import Celery broker = 'redis://127.0.0.1:6379/1' backend = 'redis://127.0.0.1:6379/2' app = Celery('test',broker=broker,backend=backend,include=['celery_task.add_task','celery_task.send_email'])
第二步:在包内部任务文件中写函数
# add_task.py(求和任务)
from .celery import app import time @app.task def add(a,b): time.sleep(3) return a+b
# send_email.py(发送短信任务)
from .celery import app import time @app.task def send_sms(phone,code): time.sleep(2) return ('给%s发送短信成功,验证码为:%s'% (phone,code))
第三步:切到包所在目录,启动worker
celery -A celery_task worker -l info -P eventlet
第四步:其他程序提交任务,被提交到中间件中,被worker执行
from celery_task.send_email import send_sms from celery_task.add_task import add ##同步调用 # 发送短信 res = send_sms('112423425415',8888) print(res) # 给112423425415发送短信成功,验证码为:8888 # 求和 res = add(3,5) print(res) # 8 ## 异步调用 # 发送短信 res=send_sms.delay('12143214124',8888) print(res) # bc762686-b363-4d7c-a39f-6d356dded995 # 求和 res = add.delay(3,5) print(res) # b867d1b9-a1c2-4608-ab53-5af167cfbf02
第五步,查看结果
# get_result.py from celery_task.celery import app from celery.result import AsyncResult id ='任务id' if __name__ == '__main__': a = AsyncResult(id=id,app=app) if a.successful(): result = a.get() print(result) elif a.failed(): print('任务失败') elif a.status == 'PENDING': print('任务等待中') elif a.status == 'RETRY': print('任务异常后正在重试') elif a.status == 'STARTED': print('任务已经开始被执行')
# 需要传入时间对象 from datetime import datetime,timedelta eta=datetime.utcnow() + timedelta(seconds=10) # 立即执行这个任务 # res = send_sms.delay('12143214124',8888) # 延时10秒执行这个任务 res=send_sms.apply_async(args=['12143214124',8888],eta=eta) print(res) # 9b27f3cd-3041-4edd-ab23-a134b03c6b97
- 需要同时启动beat和worker
- beat 定时提交任务的进程---》配置在app.conf.beat_schedule的任务
- worker 执行任务的
第一步:celery.py加上beat_schedule配置
from celery import Celery broker = 'redis://127.0.0.1:6379/1' backend = 'redis://127.0.0.1:6379/2' app = Celery('test',broker=broker,backend=backend,include=['celery_task.add_task','celery_task.send_email']) # 设置时区 app.conf.timezone = 'Asia/Shanghai' # 是否使用UTC app.conf.enable_utc = False from datetime import timedelta from celery.schedules import crontab # 任务的定时配置 app.conf.beat_schedule = { 'send_sms': { 'task': 'celery_task.user_task.send_sms', # 'schedule': timedelta(seconds=3), # 时间对象 # 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点 'schedule': crontab(hour=17, minute=00), # 每天17:00 'args': ('18888888', '6666'), }, }
第二步:启动beat与worker,正常执行程序即可
# 启动一个beat celery -A celery_task beat -l info # 启动work执行 celery -A celery_task worker -l info -P eventlet
注意事项:
1. 如果是包结构,启动命令的执行位置一定在包这一层
2. include=['celery_task.order_task'],路径从包名下开始导入,因为我们在包这层执行的命令
补充:
如果在公司中,只是做定时任务,还有一个更简单的框架——APSchedule
参考博客:https://blog.csdn.net/qq_41341757/article/details/118759836
luffy_api -celery_task #celery的包路径 -luffy_api #源代码路径
2 在使用提交异步任务的位置,导入使用即可
# 视图函数中使用,导入任务 # 任务.delay() # 提交任务
3 启动worker,如果有定时任务,启动beat
# 启动一个beat celery -A celery_task beat -l info # 启动work执行 celery -A celery_task worker -l info -P eventlet
4. 等待任务被worker执行
5. 在视图函数中,查询任务执行的结果
注意:
启动命令的执行位置,如果是包结构,一定在包这一层加上一句:
import os os.environ.setdefault('DJANGO_SETTINGS_MODULE','luffy_api.settings.dev')
# 秒杀任务 import random import time from .celery import app @app.task def sckill_task(good_id): # 生成订单,减库存,都要在一个事务中 print('商品%s:秒杀开始'% good_id) # 随机等待时间 time.sleep(random.choice([6,7,8])) print('商品%s秒杀结束'% good_id) return random.choice([True,False])
views.py
# 秒杀逻辑,CBV from rest_framework.viewsets import ViewSet from celery_task.order_task import sckill_task from celery_task.celery import app from celery.result import AsyncResult class SckillView(ViewSet): @action(methods=['GET'],detail=False) def sckill(self,request): a = request.query_params.get('id') res= sckill_task.delay(a) return APIResponse(task_id=res.id) @action(methods=['GET'],detail=False) def get_result(self,request): task_id = request.query_params.get('id') a = AsyncResult(id=task_id,app=app) if a.successful(): result = a.get() if result: return APIResponse(msg='秒杀成功') else: return APIResponse(code=101,msg='秒杀失败') elif a.status == 'PENDING': print('任务等待中被执行') return APIResponse(code=666,msg='还在秒杀中') else: return APIResponse()
urls.py
from rest_framework.routers import SimpleRouter from . import views router = SimpleRouter() router.register('goods',views.SckillView,'goods') urlpatterns = [ ] urlpatterns += router.urls
前端Sckill.vue
<template> <div> <button @click="handleSckill">秒杀</button> </div> </template> <script> export default { name: "Sckill", data(){ return{task_id:''} }, methods:{ handleSckill(){ this.$axios.get(this.$settings.BASE_URL+'/user/goods/sckill/?id=1').then(res=>{ this.task_id=res.data.task_id this.t = setInterval(()=>{ this.$axios.get(this.$settings.BASE_URL+'/user/goods/sckill/?task_id'+this.task_id).then(res=>{ if(res.data.code==666){ //如果秒杀任务还没执行,定时任务继续 console.log(res.data.msg) }else { //秒杀结束,无论秒杀是否成功,定时任务结束 clearInterval(this.t) this.t=null this.$message(res.data.msg) } }) },2000) } ).catch(res=>{ } ) } } } </script> <style scoped> </style>
前端路由
{ path: '/sckill/', name: 'sckill', component: Sckill },