第三十一章:celery

一:celery

1.celery介绍

1.1 官网

# 1 celery官网:http://www.celeryproject.org/

# 2 介绍:Celery is a project with minimal funding, so we don’t support Microsoft Windows. Please don’t open any issues related to that platform

# 3 celery 是独立的服务

1.2 架构

# Celery 是什么:分布式的异步任务   框架
	-翻译过来是 芹菜 的意思,跟芹菜没有关系
    -框架:服务,python 的框架,跟 django 无关
    -能用来做什么
    	-1 异步任务:可以提高项目的并发量,之前开启线程做,现在使用 celery 做
        -2 定时任务
        -3 延迟任务

# 理解 celery 的运行原理
1)可以不依赖任何服务器,通过自身命令,启动服务
2)celery 服务为为其他项目服务提供异步解决任务需求的
注:会有两个服务同时运行,一个是项目服务,一个是 celery 服务,项目服务将需要异步处理的任务交给 celery 服务,celery 就会在需要时异步完成项目的需求
 
# 架构
    -任务中间件: Broker(中间件)
        -其他服务提交的异步任务,放在里面排队
    	- celery 本身不提供消息中间件
        -需要借助于第三方:redis,rabbitmq
    
    -任务执行单元:worker,真正执行任务的地方,一个个进程,执行函数
    
    -结果存储:backend
    	-函数 return 的结果存储在这里
    	- celery 本身不提供结果存储,借助于第三方库:redis,mysql

# 使用场景
	异步执行:解决耗时任务
    延迟执行:解决延迟任务
    定时执行:解决周期(周期)任务

举例描述:

人是一个独立运行的服务(django) | 医院也是一个独立运行的服务(celery)
	正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
	人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求

消息中间件(message broker)(邮箱, 邮局): 本身不提供消息服务,可以和第三方消息中间件集成,常用的有 redis mongodb rabbitMQ

任务执行单元(worker)(寄件人): 是Celery提供的任务执行单元, worker并发的运行在分布式的系统节点中

任务执行结果存储(task result store)(收件人):用来存储Worker执行的任务的结果,Celery支持以不同方式存储任务的结果,包括Redis,MongoDB,Django ORM,AMQP等

2.快速使用

2.1 使用步骤

# 1.安装
	pip install celery
    win:pip install eventlet
	
# 2.写一个 main.py: 实例化得到 app 对象,写函数(任务),注册成 celery 的任务
    
# 3.再别的程序中:提交任务 ---> 提交到 broker 中去了
    add.delay(3,4)
# 4.启动 worker,从 broker 中取任务执行,执行完放到 backend 中
    win:
        celery worker -A main -l info -P eventlet  # 4.x
        celery -A main worker -l info -P eventlet  # 5.x 及之后用这个
    
    lin、mac:
        celery worker -A main -l info
        celery -A main worker -l info
# 5.查看结果
	-再 backend 中查看任务执行的结果
    -直接看:命令行、可视化工具
    -通过代码查看
        from main import app
        from celery.result import AsyncResult
        id = '7bef14a0-f83e-4901-b4d8-e47aaac09b39'
        if __name__ == '__main__':
            res = AsyncResult(id=id, app=app)
            if res.successful():
                result = res.get()  #7
                print(result)
            elif res.failed():
                print('任务失败')
            elif res.status == 'PENDING':
                print('任务等待中被执行')
            elif res.status == 'RETRY':
                print('任务异常后正在重试')
            elif res.status == 'STARTED':
                print('任务已经开始被执行')

2.2 代码示例

# main.py  注册 test 任务
import time
from celery import Celery

backend = 'redis://127.0.0.1:6379/1'
broker = 'redis://127.0.0.1:6379/0'
app = Celery('test', backend=backend, broker=broker)


@app.task
def test(a, b):
    time.sleep(2)
    add = a + b
    print(a + b)
    return add
# work.py  提交任务
from main import test

print('hello world')

# delay 异步
res = test.delay(50, 66)
print('res', type(res))	# <class 'celery.result.AsyncResult'>
print("res", res) # res 8ffd680b-cd37-4f4b-ab01-9428ade4606b
# look.py  查看任务
from main import app
from celery.result import AsyncResult


# 拿到任务 id 查看执行结果
id = '8ffd680b-cd37-4f4b-ab01-9428ade4606b'
if __name__ == '__main__':
    res = AsyncResult(id=id, app=app)
    if res.successful():
        result = res.get()
        print(result)
    elif res.failed():
        print('任务失败')
    elif res.status == 'PENDING':
        print('任务等待被执行')
    elif res.status == 'RETRY':
        print('任务异常后正在重试')
    elif res.status == 'STARTED':
        print('任务已经开始被执行')

二:使用指南

# 注意:
1 启动命令的执行位置,如果是包结构,一定在包这一层
2 include=['celery_task.order_task'],路径从包名下开始导入,因为我们在包这层执行的命令

1.celery包结构

project
    ├── celery_task  	# celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery 连接和配置相关文件,且名字必须交 celery.py
    │   └── tasks.py    # 所有任务函数
    ├── add_task.py  	# 添加任务
    └── get_result.py   # 获取结果
    
    
############# 第一步:新建包 celery_task #############
# 在包下新建[必须叫celery]的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('test', broker=broker, backend=backend, include=['celery_task.order_task', 'celery_task.user_task'])


##### 第二步:在包内部,写task,任务异步任务####
# order_task
from .celery import app
import time
@app.task
def add(a, b):
    print('-----', a + b)
    time.sleep(2)
    return a + b

# user_task
from .celery import app
import time
@app.task
def send_sms(phone, code):
    print("给%s发送短信成功,验证码为:%s" % (phone, code))
    time.sleep(2)
    return True

####第三步:启动worker ,包所在目录下
	celery  -A celery_task  worker -l info -P eventlet
    
    
###第四步:其他程序 提交任务,被提交到中间件中,等待worker执行,因为worker启动了,就会被worker执行
from celery_task import send_sms
res=send_sms.delay('1999999', 8888)
print(res)  # 7d39033c-4cc7-4af2-8d78-e62c277db183


### 第五步:worker执行完,结果存到backend中

### 第六步:我们查看结构
from celery_task import app
from celery.result import AsyncResult
id = '7d39033c-4cc7-4af2-8d78-e62c277db183'
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('任务已经开始被执行')

2.celery任务类型

# 异步任务   
	任务.delay(参数) 
# 延迟任务
	任务.apply_async(args=[参数], eta=时间对象) # 如果没有修改时区,需要使用 utc 事件
# 定时任务
	-需要启动 beat 和启动 worker
    	-beat    定时提交任务的进程 ---》配置在 app.conf.beat_schedule 的任务
        -worker  执行任务的
        
    ### 使用步骤
    	# 第一步:在 celery 的 py 文件中写入
        app.conf.timezone = 'Asia/Shanghai'
        # 是否使用 UTC
        app.conf.enable_utc = False
        # celery的配置文件#####
        # 任务的定时配置
        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=9, minute=43),  # 每天9点43
                'args': ('18888888', '6666'),
            },
        }
        
        ## 第二步:启动beat
        celery -A celery_task beat -l info
        
        ## 第三步:启动worker
        celery -A celery_task worker -l info -P eventlet

# 注意:
	1 启动命令的执行位置,如果是包结构,一定在包这一层
    2 include = ['celery_task.order_task'], 路径从包名下开始导入,因为我们在包这层执行的命令

2.1 延时任务

# 延迟任务
	任务.apply_async(args=[参数], eta=时间对象) # 如果没有修改时区,需要使用 utc 事件
# 包结构
project
    ├── celery_task  	# celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery 连接和配置相关文件,且名字必须交 celery.py
    │   └── test_task.py    # 所有任务函数
    ├── run.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('test', broker=broker, backend=backend, include=['celery_task.test_task'])

# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# test_task.py

from celery_task.celery import app
import time


@app.task
def send_sms(phone, code):
    time.sleep(3)
    print("给%s发送短信成功,验证码为:%s" % (phone, code))
    return True
# run.py
from celery_task.test_task import send_sms
from datetime import datetime, timedelta

time_w = datetime.utcnow() + timedelta(seconds=20)
print(time_w)
# res = send_sms.delay('15612341234', '658923')
res2 = send_sms.apply_async(args=['1551111111', '123123', ], eta=time_w)

# print(res)
print(res2)
# 启动

# celery -A celery_task worker -l info -P eventlet

2.2 定时任务

# 包结构
project
    ├── celery_task  	# celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery 连接和配置相关文件,且名字必须交 celery.py
    │   └── test_task.py    # 所有任务函数
    ├── run.py  	# 添加任务
# celery.py
from celery import Celery
from datetime import datetime, timedelta

# from celery.schedules import crontab

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

# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用 UTC
app.conf.enable_utc = False
app.conf.beat_schedule = {
    'send_sms': {
        'task': 'celery_task.test_task.send_sms',
        'schedule': timedelta(seconds=5),
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        # 'schedule': crontab(hour=9, minute=43),  # 每周一早八点
        'args': ('15066662222', '8965')
    },
    # 配置多个定时任务
}

# test_task.py
from celery_task.celery import app
import time


@app.task
def send_sms(phone, code):
    time.sleep(3)
    print("给%s发送短信成功,验证码为:%s" % (phone, code))
    return True
# 启动

# celery -A celery_task beat -l info
# celery -A celery_task worker -l info -P eventlet

3.秒杀任务

# urls.py
router.register('seckill', SeckillView, 'seckill')

# 完整接口路由
# http://127.0.0.1:8000/api/v1/course/seckill/seckill/
# http://127.0.0.1:8000/api/v1/course/seckill/get_result/?id=
# views.py
from rest_framework.viewsets import ViewSet
from celery_task.test_task import shoping
from utils.response import APIResponse
from celery.result import AsyncResult
from celery_task.test_task import app


class SeckillView(ViewSet):
    @action(methods=['GET'], detail=False)
    def seckill(self, request):
        shop_id = request.query_params.get('id')
        res = shoping.delay(shop_id)
        return APIResponse(msg='提交秒杀', id=res.id)

    @action(methods=['GET'], detail=False)
    def get_result(self, request):
        id = request.query_params.get('id')
        res = AsyncResult(id=id, app=app)
        if res.successful():
            result = res.get()
            if result:
                return APIResponse(msg=f'秒杀成功 {result}')
            else:
                return APIResponse(msg='秒杀失败')
        elif res.status == 'PENDING':
            return APIResponse(code= 101, msg='还在秒杀中')

# test_task.py  使用 celery_task 包
from celery_task.celery import app
import time
import random


@app.task
def shoping(id):
    # 秒杀任务前
    print('开始秒杀商品 %s' % id)
    time.sleep(10)
    print('结束秒杀商品 %s' % id)
    # 秒杀任务后
    return random.choice([True, False])

# Seckill.vue 前端
<template>
  <div>
    <img src="https://img2.baidu.com/it/u=1215965897,3514702558&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1678640400&t=c4c880d6ad208e24f69c0a731a3fd055" alt="" height="300px" width="300px">
    <br>
    <el-button type="danger" plain @click.once="handleClick">秒杀</el-button>
  </div>
</template>

<script>
export default {
  name: "Seckill",
  methods: {
    handleClick() {
      this.$axios.get(this.$settings.BASE_URL + 'api/v1/course/seckill/seckill/').then(res => {
        if (res.data.code == 0) {
          let task_id = res.data.id
          console.log(res.data.msg)
          this.$message({
            message: res.data.msg,
            type: 'error'
          });
          // 起个定时任务,每隔5s向后端查询一下是否秒杀成功
          let t = setInterval(() => {
            this.$axios.get(this.$settings.BASE_URL + 'api/v1/course/seckill/get_result/?id=' + task_id).then(
                res => {
                  if (res.data.code == 101) {  //秒杀结束了,要么成功,要么失败了
                    console.log(res.data.msg)
                    // 销毁掉定时任务
                  } else{
                    //什么事都不干
                    alert(res.data.msg)
                    clearInterval(t)
                  }
                }
            )
          }, 5000)
        }
      })
    }
  }
}
</script>

4.在 Django 中使用 celery

# 使用步骤:
	-1 把咱们写的包,复制到项目目录下
    	-luffy_api
        	-celery_task # celery的包路径
				celery.py  # 此文件中 一定不要忘了一句话
                    import os
                    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
            -luffy_api  #源代码路径

    -2 在使用提交异步任务的位置,导入使用即可
    	-视图函数中使用,导入任务
        -任务.delay()  # 提交异步任务
        -任务.apply_async()  # 提交延时任务
        -定时任务配置
        	# 时区
            app.conf.timezone = 'Asia/Shanghai'
            # 是否使用 UTC
            app.conf.enable_utc = False
            app.conf.beat_schedule = {
                'send_sms': {
                    'task': 'celery_task.test_task.send_sms',
                    'schedule': timedelta(seconds=5),
                    # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
                    # 'schedule': crontab(hour=9, minute=43),  # 每周一早八点
                    'args': ('15066662222', '8965')
                },
                # 配置多个定时任务
            }

    -3 启动worker,如果有定时任务,启动beat

    -4 等待任务被worker执行

    -5 在视图函数中,查询任务执行的结果


# 重点:celery 中使用 djagno,有时候,任务中会使用 django 的 orm,缓存,表模型。。。。一定要加
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

三:实际应用

1.轮播图加入缓存

from rest_framework.mixins import ListModelMixin


class BannerView(GenericViewSet, ListModelMixin):
    queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[
               :common_settings.BANNER_COUNT]
    serializer_class = BannerModelSerializer

    def list(self, request, *args, **kwargs):
        redis_banner = cache.get('banner_list')
        if not redis_banner:
            print('走了数据库')
            res = super().list(self, request, *args, **kwargs)
            cache.set('banner_list', res.data)
            return APIResponse(data=res.data)
        print('走了缓存')
        return APIResponse(data=redis_banner)

2.双写一致性

# 加入缓存后,缓存中有数据,先去缓存拿,但是如果mysql中数据变了,缓存不会自动变化,出现数据不一致的问题
	mysql 和 缓存数据库 数据不一致
    
# 双写一致性
	写入mysql,redis没动,数据不一致存在问题
    
#如何解决
	1 修改数据,删除缓存
    2 修改数据,更新缓存
    3 定时更新缓存  ---》实时性差
    
# 定时任务:celery

轮播图案例

# celery.py

# 第一步:在 celery 配置定时任务
app.conf.beat_schedule = {
    'banner_list': {
        'task': 'celery_task.home_task.banner_list',
        'schedule': timedelta(seconds=5),
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        # 'schedule': crontab(hour=9, minute=43),  # 每周一早八点
        # 'args': ('15066662222', '8965')
    },
}

# 第二步:启动worker,启动beat
# home_task.py

# 第二步:在 celery_task 包内新建 home_task.py 文件创建 banner_list 任务,启动worker,启动beat

from celery_task.celery import app
from home.models import Banner
from home.serializer import BannerModelSerializer
from django.conf import settings
from django.core.cache import cache

@app.task
def banner_list():
    banner_obj = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    ser = BannerModelSerializer(instance=banner_obj, many=True)
    for s in ser.data:
        s['pic_addr'] = settings.BACKEND_URL + s['pic_addr']
        print(s['pic_addr'])
    return True

# 后端地址
# BACKEND_URL = 'http://127.0.0.1:8000'
posted @ 2023-03-12 14:23  亦双弓  阅读(30)  评论(0)    收藏  举报