celery 任务队列、双写一致性、异步秒杀

一、介绍

中文网:Celery 初次使用 - Celery 中文手册 (celerycn.io)

Celery 官网:http://www.celeryproject.org/

Celery 官方文档英文版:http://docs.celeryproject.org/en/latest/index.html

1、celery基于python开发的分布式异步消息任务队列

# celery是什么?
	分布式异步任务框架:第三方框架,celery翻译过来是芹菜,吉祥物就是芹菜
    项目中使用异步任务的场景,可以使用它
    之前做异步,如何做? 异步发送短信---》开启多线程---》不便于管理

# celery有什么作用?
    -执行异步任务
    -执行延迟任务
    -执行定时任务

# celery原理
1)可以不依赖任何服务器,通过自身命令,启动服务
2)celery服务为为其他项目服务提供异步解决任务需求的
注:会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求

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

2、celery架构

Celery的架构由三部分组成,消息中间件(message broker),任务执行单元(worker)和任务执行结果存储(task result store)组成。

 

消息中间件

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

任务执行单元

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

任务结果存储

Task result store用来存储Worker执行的任务的结果,Celery支持以不同方式存储任务的结果,包括AMQP, redis等

Celery 组件

Celery 扮演生产者和消费者的角色

    • Producer :
      任务生产者. 调用 Celery API , 函数或者装饰器, 而产生任务并交给任务队列处理的都是任务生产者。

    • Celery Beat :
      任务调度器. Beat 进程会读取配置文件的内容, 周期性的将配置中到期需要执行的任务发送给任务队列。

    • Broker :
      消息代理, 队列本身. 也称为消息中间件。接受任务生产者发送过来的任务消息, 存进队列再按序分发给任务消费方(通常是消息队列或者数据库)。

    • Celery Worker :
      执行任务的消费者, 通常会在多台服务器运行多个消费者, 提高运行效率。

    • Result Backend :
      任务处理完成之后保存状态信息和结果, 以供查询。

二、简单使用

1、安装

pip install celery  # 最新 5.3.4

2、写一个main.py

指定了中间件、后端存储都有redis(1,2为库),任务

import time
from celery import Celery
# 1 实例化得到对象
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)

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

3、提交任务,使用别的进程

add_task.py

from main import send_sms
res=send_sms.delay() 
print(res)

4、在终端中启动worker

注意:需要切换路径到main文件所在路径

windows:

pip3 install eventlet
celery -A main worker -l info -P eventlet

mac linux

celery -A main worker -l info

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

get_result.py

from celery.result import AsyncResult
from main import app
id = '92987636-ae9e-4be9-828b-8c2d10fe066a'
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('任务已经开始被执行')

三、延迟任务和定时任务

1、异步任务

提交任务,使用delay提交即可

from celery_task.user_task import send_tx_duanxin

class MobileView(ViewSet):
    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        try:
            mobile = request.query_params['mobile']
            user = User.objects.get(mobile=mobile)
        except Exception as e:
            raise Exception('未知错误,联系管理员')
        return APIResponse(msg='手机号存在')

    # 发送短信
    @action(methods=['GET'], detail=False)
    def sms_send(self, request, *args, **kwargs):
        mobile = request.query_params.get('mobile')
        code = get_code()
        cache.set('cache_mobile_%s' % mobile, code)
        if mobile:
            # t = Thread(target=send_sms, args=[code, mobile])
            # t.start()
            # res = send_sms.delay(code, mobile)
            res = send_tx_duanxin.delay(code, mobile)
            return APIResponse(msg='短信已经发送')
        raise ValidationError('手机号没有携带')

2、延迟任务

## 提交延迟任务   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)

当延迟时间设置为1分钟,可以看到,提交任务之后,received了,过了一分钟才会执行

3、定时任务

运行逻辑:在celery.py中设置好定时任务,启动worker,再启动beat来定时提交任务

在celery.py中导入user_taskuser_task.py的任务

from celery import Celery

import os

# CELERY_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'AiOps.settings.dev')

broker = 'redis://127.0.0.1:6379/4'  # 消息中间件 redis
backend = 'redis://127.0.0.1:6379/5'  # 结果存,用redis
app = Celery('app', broker=broker, backend=backend, include=['celery_task.user_task'])

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

# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab

app.conf.beat_schedule = {
    'update_duanxin': {
        # 'task': 'celery_task.user_task.send_tx_duanxin',
        'task': 'celery_task.user_task.send_tx_duanxin',
        'schedule': timedelta(minutes=2),
        'args': (),
    },
}

启动

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

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

celery -A celery_task beat -l info

beat不停止会不停的执行提交任务

四、django中使用celery

1、调整包结构

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

###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' 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、 通用方案(第三方方案比较麻烦)

# 1 把celery_task包,放到项目路径下

# 2 提交异步或延迟任务,导入直接提交即可
from celery_task.user_task import cache_demo

res = cache_demo.delay('phone','1872324242')

# 3 只要启动worker,这些任务就会被执行

# 4 如果要使用django中的东西(配置文件,缓存,orm。。。),都需要在celery.py中写
    import os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

#5 使用django内置东西的任务
@app.task
def cache_demo(key, value):
    cache.set(key, value)
    return '缓存成功'

3、案例一:异步发短信

user_task 中指定发短信的任务

import time
from .celery import app
from django.core.cache import cache


@app.task
def send_sms(phone):
    time.sleep(2)
    return f'{phone}发送短信成功'


@app.task
def cache_demo(key, value):
    cache.set(key, value)  # 没有写过期时间,有默认过期时间 4分钟过期
    return '缓存成功'


from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile


@app.task
def send_tx_duanxin(code, ti):
    try:
        cred = credential.Credential("AKIDhFxxFF6rRr8Qd", "QsAGxxx7YvCKLG3kHcjnfbQ")
        httpProfile = HttpProfile()
        httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
        httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
        httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)
        clientProfile = ClientProfile()
        clientProfile.signMethod = "TC3-HMAC-SHA256"  # 指定签名算法
        clientProfile.language = "en-US"
        clientProfile.httpProfile = httpProfile
        client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
        req = models.SendSmsRequest()
        req.SmsSdkAppId = "1400861075"
        req.SignName = "爱瞌睡的老头公众号"
        req.TemplateId = "1957159"
        req.TemplateParamSet = [code, ti]
        req.PhoneNumberSet = ["+86193263"]
        req.SessionContext = ""
        req.ExtendCode = ""
        req.SenderId = ""
        resp = client.SendSms(req)
        print(resp.to_json_string(indent=2))
    except TencentCloudSDKException as err:
        print(err) 

在视图函数中:

导入任务,另外配置好路由

from celery_task.user_task import send_tx_duanxin

def send_tx_duanxing(requset, *args, **kwargs):
    code = get_code()
    res = send_tx_duanxin.delay(code, '15')
    return HttpResponse(res)

五、轮播图接口缓存、缓存封装

1、轮播图缓存

class BannerView(GenericViewSet, ListModelMixin):  # 自动生成路由
    queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    # 注意orders是数据库表中的优先级字段
    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)

2、缓存封装

common_mixin.py(封装成类)

from django.core.cache import cache
from rest_framework.mixins import ListModelMixin
from utils.common_response import APIResponse
from utils.common_logger import logger


class CommonListModelMixin(ListModelMixin):
    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        return APIResponse(result=res.data)


class CacheListModelMixin(ListModelMixin):
    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)
            return APIResponse(result=res.data)
        logger.info('走了缓存')
        return APIResponse(result=data_list)

视图类

注意:cache_key 字段的设置,查询所有都可以加缓存,不同的缓存cache_key不一样

class BannerView(GenericViewSet, CacheListModelMixin):  # 自动生成路由
    queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    # 注意orders是数据库表中的优先级字段
    serializer_class = BannerSerializer
    cache_key = 'banner_list'

封装成装饰器

def cache_sss(key):
    def wrapper(func):
        def inner(*args, **kwargs):
            data_list = cache.get(key)
            if data_list:
                print('走了缓存')
                return APIResponse(result=data_list)
            else:
                res = func(*args, **kwargs)
                cache.set(key, res.data.get('result'))
                print('走了数据库')
                return res
        return inner
    return wrapper

视图类

class BannerView(GenericViewSet, ListModelMixin):  # 自动生成路由
    queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    # 注意orders是数据库表中的优先级字段
    serializer_class = BannerSerializer
    @cache_sss(key='banner_list')
    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        return APIResponse(result=res.data)

六、双写一致性

1、背景

# 1 接口加缓存----》mysql数据改了---》缓存数据没动---》数据不一致了
	- 有的数据,必须一致(缓存删除和修改,要在修改数据后)
    	- 修改,插入数据(mysql),删除缓存
        - 修改,插入数据(mysql),修改缓存
        ----不合理的方案----
        删除缓存,再改数据
        
        - 有的数据,可以容忍--->实时性要求没有那么高
    	- 定时更新  ---》每隔5分钟更新一次
        
# 2 高级名:  双写一致性
当修改了数据库中的数据同时也需要修改缓存中的数据,如何保证数据库中和缓存中的数据一致,这就是双写一致性
解决方案:
	- 改数据,删除缓存,改缓存
    - 定时更新
        
# celery: 发送短信,改成异步
# celery:定时更新轮播图缓存

2、django缓存过期时间

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

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

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

启动worker,beat来定时提交任务(任务:从数据库中查出所有的轮播图,并缓存到redis中)

#####1 任务
@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)
    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(minutes=2),
        'args': (),
    },

}

### 3 启动worker
celery -A celery_task worker -l info -P eventlet

##  4 启动beat
celery -A celery_task beat -l info

七、 异步秒杀逻辑

1、分析流程

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

2、前端

秒杀页面组件

<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>

3、后端逻辑

视图类  SeckillView

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='服务端错误,秒杀失败')

home_task任务:

@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

启动worker

celery -A celery_task  worker  -l info 

  

 

 

 

 

 

 

  

posted @ 2023-10-18 19:31  凡人半睁眼  阅读(146)  评论(0编辑  收藏  举报