celery

1. celery介绍

 1. celery是什么?

   分布式异步任务框架:第三方框架。

   项目中使用异步任务的场景,可以使用它

   之前做异步,如何做? 

    异步发送短信 ----》 开启多线程 ----》 不便于管理

 2.celery 有什么作用?

  - 执行异步任务

  - 执行延迟任务

  - 执行定时任务

 3. celery原理

  - 1. 可以不依赖任何服务器,通过自身命令,启动服务

  -2. celery服务为其他项目提供异步解决任务需求的

  注:会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求

比如:

  人是一个独立运行的服务 |  医院也是一个独立运行的服务

    正常情况下,人可以完成所有的健康情况的动作,不需要医院的参与;但当人生病的时候,就会被医院接收,解决人生病问题。

    人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求

django如果不用异步,正常运行即可,如果想做异步,就借助于celery来完成

 4. celery架构

  - broker:消息中间件,任务中间件(消息队列:redis,rabbitmq)

    django要做异步,提交任务到 任务中间件中(redis) ,存储起来

    celery本身不提供消息服务,但是可以方便的和第三方提供的消息中间件集成,包括,RabbitMQ,Redis等等

  - worker:任务执行者,任务执行单元

    不停的从任务中间件中取任务,执行

    worker 是celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中

  - banckend:结果存储,任务结果存储

    把任务执行结果(函数返回值),存放到结果存储中(redis)

    用来存储worker执行的任务的结果,celery 支持以不同的方式存储任务的结果,包括AMQP,redis等

    

 

2. celery的快速使用

1. celery是开源的,小组织,不支持win,win上使用:需要借助于第三方

2. 安装:pip install celery

  - 安装的是最新版本:5.3.4

    

 3. 写代码 main.py

  1. 先导入celery,然后实例化得到对象

  2. 编写任务,使用app.task装饰---》变成celery的任务

import time

from celery import Celery

# 1. 实例化得到对象
broker = 'redis://127.0.0.1:6379/1' # 消息中间件 :redis,1:表示redis的第一个库
backend = 'redis://127.0.0.1:6379/2' # 结果存储,用redis
app=Celery('app',broker=broker,backend=backend)

# 2. 编写任务,必须用app.task装饰,才变成了celery的任务
@app.task
def send_sms():
    time.sleep(1)
    print('短信发送成功')
    return '手机号短信发送成功'

  # 3. 提交任务,使用别的线程

# 提交任务,使用别的进程
from main import send_sms

# 1.同步执行
# res = send_sms()
# print(res)

# 2. 异步发送短信
# 返回结果不是send_sms的返回值,是一个任务id号
# 这个任务还没有被执行,只是提交到任务中间件中了(redis)
res = send_sms.delay()
print(res) # 346b7b90-31b1-4daa-8792-3112ea028df7

# 任务要执行,要启动worker --》 使用命令启动 ---》启动celery服务
# 在win上:要先安装: pip install eventlet

  结果:redis的db1中,任务提交成功

    

  4. 启动worker,可以在3之前

    Windows:

      - 1. 先安装:pip install eventlet

        

      - 2. 执行命令,启动celery服务:

      celery  -A main worker -l info -P eventlet (路径要对,先切换到main.py所在路径下)                    

# 任务要执行,要启动worker --》 使用命令启动 ---》启动celery服务
# 执行命令:celery -A tasks worker --loglevel=INFO
# 在win上:要先安装: pip install eventlet
# celery -A main worker -l info -P eventlet  # 启动worker
# mac,Linux:
# celery -A main worker -l info

  结果:

  

  #  5 worker就会执行任务,把执行的结果,放到结果存储中

  # 6. 获取结果

from celery.result import AsyncResult
from main import app

id = '346b7b90-31b1-4daa-8792-3112ea028df7'
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('任务已经开始被执行')

 

     worker执行完任务之后:

      

     当再次提交一个任务且 worker还没启动:

      

    获取结果:

      

3. celery包结构

project
├── celery_task # celery包
│ ├── __init__.py # 包文件
│ ├── celery.py # celery连接和配置相关文件,且名字必须叫celery.py
│ └── tasks.py # 所有任务函数
├── add_task.py # 添加任务
└── get_result.py # 获取结果

如图:

  

 1. 创建 celery_task 包:

  包内部有celery.py 和一堆task ---> ['celery_task.home_task','celery_task.user_task']

 2. celery.py

from celery import Celery
broker = 'redis://127.0.0.1:6379/1'  # 消息中间件 redis
backend = 'redis://127.0.0.1:6379/2'  # 结果存,用redis
app = Celery('app', broker=broker, backend=backend,include=['celery_task.home_task','celery_task.user_task'])
# 每个task,写自己相关的任务

如图:

 3. 每个task,写自己相关的任务

 4. 启动worker

 celery -A celery_task worker -l info -P eventlet

 5. 提交任务

from celery_task.home_task import add
res=add.delay(3,4)
print(res)

 6. 查看结果

from celery_task.celery import app
from celery.result import AsyncResult

id = 'e31441d9-e9a6-4d70-9a66-a9227a6bc273' # 任务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('任务已经开始被执行')

4. celery延迟任务和定时任务

4.1 异步任务

 1. 编写任务:

  

 2.需要先启动worker,之后在添加任务

  

 3. 在提交任务,得到任务id号:

# 提交任务,使用delay提交即可
from celery_task.home_task import add
res= add.delay(3,4)
print(res)

如图:

  

  4. 查看结果:

  

4.2 延迟任务

  1. 编写任务:

  

  2. 先启动worker:celery -A celery_task worker -l info -P eventlet

  3. 使用 apply_async,添加延迟任务:

from celery_task.user_task import send_sms # 导入任务
## 提交延迟任务   apply_async
# 添加延迟任务
from datetime import datetime, timedelta
print(datetime.utcnow())  # utc 时间,跟咱们差8个小时
# eta 就是 10s 后的实际
eta = datetime.utcnow() + timedelta(seconds=30)
res = send_sms.apply_async(args=(18953675221,), eta=eta)
print(res)

结果:

   

4.3 定时任务

 1. 在celery.py 中,配置定时任务

# 时区
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': 'celery_task.home_task.add', # home_task 中的add任务
        'schedule': timedelta(seconds=3),
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        'args': (5, 6),
    },
    'send_sms': {
        'task': 'celery_task.user_task.send_sms', # user_task中的send_sms任务
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        'schedule': crontab(hour=9, minute=39),  # 每天9点39,执行
        'args': (18923748221,),
    },

}

 2. 启动worker

celery -A celery_task worker -l info -P eventlet

如图:

  

 3. 启动beat(它来定时提交任务)

celery -A celery_task beat -l info

结果:

  

  

 

5. django中使用celery

通用方案

  1. 将写的celery_task包,放到项目的路径下

  

  2. 如果要使用django中的东西(配置文件,缓存,orm。。。),都需要在celery.py 中配置:

  

  3. 提交异步或延迟任务,导入直接提交即可

  

   4. 只要启动worker,这些任务就会被执行

  5. 使用django内置的东西的任务:

  

 6. 接口缓存

1. 首页获取轮播图接口 --》之前写的是去数据库查询 ---》每次来到首页,都要查询一次 ---》轮播图变得很慢

2. 我们把轮播图数据,放到redis中,做缓存

  - 以后:只要缓存中有,就从缓存(redis)中拿

  - 如果缓存中没有,从数据库查,查完在放到缓存中

3. 查询所有的接口,都可以加缓存

 6.1轮播图接口加缓存

 

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from .models import Banner
from .serializer import BannerSerializer
from django.conf import settings
from django.core.cache import cache
from utils.common_logger import logger
from utils.common_response import APIResponse

def cache_set(cache_key):
    def outer(func):
        def inner(*args, **kwargs):
            # 先看缓存中是否有数据
            data_list = cache.get(cache_key)
            if data_list:
                logger.info('走了缓存')
                return APIResponse(result=data_list)
            else:
                logger.info('走了数据库')
                res = func(*args, **kwargs)
                # 没有就设置缓存
                cache.set(cache_key,res.data)
                return res
        return inner

    return outer
# 加缓存的轮播图,使用装饰器
class BannerView(GenericViewSet, ListModelMixin):
    # BANNER_COUNT:轮播图显示的数量
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    serializer_class = BannerSerializer

    # 重写list方法
    # def list(self, request, *args, **kwargs):
    #     # 先去缓存中查,没有,在去数据库中查
    #     banner_list = cache.get('banner_list')
    #     if not banner_list:
    #         # 缓存中没有,从数据库中查询,并加进缓存中
    #         logger.info('走了数据库')
    #         res = super().list(request, *args, **kwargs)
    #         cache.set(banner_list, res.data)
    #         return APIResponse(result=res.data)
    #     logger.info('走了缓存')
    #     return APIResponse(result=banner_list)

    # 使用装饰器
    @cache_set(cache_key='banner_list')
    def list(self,request,*args,**kwargs):
        res=super().list(self,request,*args,**kwargs)
        return APIResponse(res.data)

结果:

  

 方式三:

视图类:

# 升级---》缓存封装
class BannerView(GenericViewSet, CacheListModelMixin):  # 自动生成路由
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    serializer_class = BannerSerializer
    cache_key = 'banner_list'

CacheListMixin:

class CacheListModelMixin(ListModelMixin):
    # 接口加缓存,重写list
    def list(self, request, *args, **kwargs):
        # 先去缓存中查,如果有,直接返回
        data_list = cache.get(self.cache_key)
        if not data_list:
            # 如果没有,在查数据库,放到缓存,再返回
            logger.info('走了数据库')
            res = super().list(request, *args, **kwargs)
            cache.set(self.cache_key, res.data, None)
            return APIResponse(result=res.data)
        logger.info('走了缓存')
        return APIResponse(result=data_list)

7. 双写一致性

1. 接口加缓存----》mysql数据改了---》缓存数据没动---》数据不一致了

   - 有的数据,必须一致(缓存删除和修改,要在修改数据之后)

  - 修改,插入数据(mysql),删除缓存

  - 修改,插入数据(mysql),修改缓存

  ------不合理的方案-------

  删除缓存,再改数据

  - 有的数据,可以之后在更新缓存 ---》实时性要求没那么高

  - 定时更新 ----》每隔5分支更新一次

2. 高级名:双写一致性

  当修改了数据库中的数据同时也需要修改缓存中的数据,如何保证数据库中和缓存中的数据一致,这就是双写 一致性。

解决方案:

  - 改数据,删除缓存,或者 改完数据之后,修改缓存

  - 定时更新

7.1 django缓存过期时间

cache.set(self.cache_key, res.data, None)   # 永不过期
cache.set(self.cache_key, res.data)   #  不写5分钟过期

7.2 通过定时更新缓存,实现双写一致性

1. 编写任务:在home_task中

from home.models import Banner
from django.conf import settings
from home.serializer import BannerSerializer
from django.core.cache import cache
from .celery import app
@app.task
def update_banner():
    # 1 查出所有轮播图
    banner_list = Banner.objects.filter(is_delete=False,is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    ser= BannerSerializer(instance=banner_list,many=True)
    # 会出现一个小问题,就是图片地址的http://127.0.0.1:8000没有了
    for item in ser.data:
        item['image']=settings.BACKEND_URL+item['image']
    # 2 把轮播图放到cache中
    cache.set('banner_list', ser.data)
    return '更新成功'

2. celery.py中配置:

app.conf.beat_schedule = {
    'update_banner': {
        'task': 'celery_task.home_task.update_banner',
        'schedule': timedelta(seconds=2), # 两秒更新一次
        'args': (),
    },
}

如图:

  

3. 启动worker

celery -A celery_task worker -l info -P eventlet

   

4. 启动beat

celery -A celery_task beat -l info

  

8. 异步秒杀逻辑

1. 分析流程 ---》 提高了并发量

 同步流程:

  用户在前端,点击秒杀按钮 ----》 提交请求到后端 ----》 [ 扣减库存,生成订单]假设耗时  ----》同步操作 ----》 10s 处理完成,秒杀成功 ----》返回给前端 ----》 如果秒杀人数过多,同步操作,不能承载更多人同时秒杀。

 异步流程:

  用户在前端,点击秒杀按钮 ----》 提交请求到后端 ----》 提交一个任务 [ 扣减库存,生成订单] 假设耗时 ----》异步操作 ----》10s 处理完成,秒杀成功 ----》 前端再发请求查询 ----》 如果秒杀人数过多,异步操作,10s内能承载非常多用户操作

8.1 前端

vue的中添加路由

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/seckill',
    name: 'seckill',
    component: SeckillView
  },
]

如图:

  

SeckillView.Vue:秒杀页面

<template>
  <div class="home">
    <Header></Header>
    <div style="padding: 50px;margin-left: 100px">

      <h1>Go语言课程</h1>
      <img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png"
           height="300px"
           width="300px">
      <br>
      <el-button type="danger" @click="handleSeckill">秒杀课程</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>
import Header from "@/components/Header.vue";
import Footer from "@/components/Footer.vue";

export default {
  name: "SeckillView",
  data() {
    return {
      seckill_id: '',
      t: ''
    }
  },
  methods: {
    handleSeckill() {
      this.$axios.post(this.$settings.BASE_URL + 'home/seckill/seckill/', {
        course_id: 'go语言'
      }).then(res => {
        console.log(res.data)
        if (res.data.code == 100) {
          this.$message({
            message: res.data.msg,
            type: 'warning',
            duration: 1500
          });

          // 起一个定时任务,每隔2s向后端查询一次,看是否秒杀成功
          this.seckill_id = res.data.seckill_id
          this.t = setInterval(() => {
            this.$axios.get(this.$settings.BASE_URL + 'home/seckill/get_seckill_result/?seckill_id='+this.seckill_id).then(res => {
              if (res.data.code == 100 || res.data.code == 101) {
                alert(res.data.msg)
                clearInterval(this.t)
                this.t = null

              } else if (res.data.code == 102) { //秒杀逻辑还没开始执行
                this.$message('等待开始秒杀');
              } else if (res.data.code == 103) {
                this.$message('正在秒杀途中');
              }
            })

          }, 2000)

        } else {
          this.$message({
            message: '服务端异常,请联系系统管理员',
            type: 'warning',
            duration: 1500
          });
        }
      })
    }
  },
  components: {
    Header, Footer
  }
}
</script>
<style scoped>

</style>

 8.2 后端

视图类中:home

from celery_task.home_task import seckill_course
from rest_framework.decorators import action

from celery_task.celery import app
from celery.result import AsyncResult

class SeckillView(GenericViewSet):
    @action(methods=['POST'], detail=False)
    def seckill(self, request, *args, **kwargs):
        # 1 取出课程id resquest.data.get('course_id'),取出当前用户 request.user.pk
        course_id = request.data.get('course_id')
        # 2 扣减库存---》数据库 --》课程id的课程 数量减一
        # 3 订单表,生成一条记录
        res = seckill_course.delay(course_id)
        # 4 返回给前端,秒杀成功
        return APIResponse(seckill_id=str(res), msg='秒杀任务已经提交')

    @action(methods=['GET'], detail=False)
    def get_seckill_result(self, request, *args, **kwargs):
        seckill_id = request.query_params.get('seckill_id')
        a = AsyncResult(id=seckill_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=102, msg='暂未轮到您')
        elif a.status == 'STARTED':
            print('任务已经开始被执行')
            return APIResponse(code=103, msg='正在秒杀,请稍后')
        else:
            return APIResponse(code=104, msg='服务端错误,秒杀失败')

路由:

  

任务:

@app.task
def seckill_course(course_id):
    # 2 扣减库存---》数据库 --》课程id的课程 数量减一
    # 3 订单表,生成一条记录
    # 逻辑是:开启事务---》扣减库存---》生成订单
    import time
    import random
    time.sleep(6)
    res = random.choice([100, 102])
    if res == 100:
        print('%s被秒杀成功了' % course_id)
        return True
    else:
        print('%s被秒杀失败了' % course_id)
        return False
posted @ 2023-10-18 20:58  Maverick-Lucky  阅读(26)  评论(0)    收藏  举报