celery介绍和使用、双写一致性问题

291 阅读7分钟

celery介绍

1. celery官网:http://www.celeryproject.org/
2. celery跟django一样,是一个独立的服务,两者互不干涉,互不影响
'''
    1) celery可以不依赖任何服务器,通过自身命令,启动服务
    2) celery服务是为了满足 其他项目中异步任务的需求 而产生的
        注:会有两个服务同时运行,一个是项目服务,一个是celery服务,当项目服务需要处理异步任务时,就向celery提交异步任务请求,由celery来执行任务,返回结果。
'''
eg:
    人是一个独立运行的服务(django) | 医院也是一个独立运行的服务(celery)
    正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
    人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求

# celery有什么用?
    1 完成异步任务:可以提高项目的并发量,之前开启线程做,现在使用celery做
    2 完成延迟任务
    3 完成定时任务
    
# 架构
    -消息中间件:broker 提交的任务(函数)都放在这里,celery本身不提供消息中间件,需要借助于第三方:redis,rabbitmq
    -任务执行单元:worker,真正执行任务的地方,一个个进程,执行函数
    -结果存储:backend,函数return的结果存储在这里,celery本身不提供结果存储,借助于第三方:redis,数据库,rabbitmq

celery快速使用

1. 安装: pip3 install celery
2. 使用:
    -写一个main.py,实例化得到app对象,写函数、任务、注册成celery的任务。
    -在别的程序中提交任务--->提交到broker中了
            add.delay(3,4)  # add是任务的函数名
    -启动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  # main代指app对象
    -在backend中查看任务执行的结果
    -get_result.py:
            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('任务已经开始被执行')

celery包结构

# 写一个celery的包,以后想在任意项目中用,直接把包导入过去使用即可
项目名:
    -celery_task
        -__init__.py
        -celery.py
    -user_task.py
    -home_task.py  # 这两个都是任务文件
    -add_task.py  # 提交任务
    -get_result.py  # 获取任务执行结果

# 使用步骤
    -新建包:celery_task
    -在包先新建一个 celery.py
    -在里面写app的初始化
    -在包里新建user_task.py 编写用户相关任务 
    -在包里新建home_task.py 编写首页相关任务 
    -其它程序,提交任务
    -启动worker ---》他可以在任务提交之前启动,也可以在之后--->一定要cd到包所在的目录下(script文件夹)执行这条命令
    	celery -A celery_task worker -l info -P eventlet
    -查看任务执行的结果

celery_task/celery.py

# app的初始化
from celery import Celery

broker = 'redis://127.0.0.1:6379/1'  # 存储任务
backend = 'redis://127.0.0.1:6379/2'  # 任务执行结果

app = Celery(__name__, broker=broker, backend=backend,include=['celery_task.home_task', 'celery_task.user_task'])  # 注册任务

# 原本我们的任务是要在这下面写的,现在使用包结构可以写在包内的其他文件中,只需要在上面哪行代码中注册一下任务即可

celery_task/home_task.py

# 任务1
import time
from .celery import app
@app.task
def add(a, b):
    time.sleep(3)
    c = a + b
    print(f'计算结果是{c}')
    return c

celery_task/user_task.py

# 任务2
import time
from .celery import app
@app.task
def send_sms(mobile, code):
    time.sleep()
    print(f'短信发送成功,验证码是{code}')
    return True

add_task.py

# 提交异步任务
from celery_task.user_task import send_sms

res = send_sms.delay('15539929350', '6666')  # delay指的是提交异步任务
print(res)
# 这里只是提交了任务,真正执行任务还要启动worker

get_result.py

# 获取任务执行结果
from celery_task.celery import app
from celery.result import AsyncResult
id = '3235f215-5a1d-4fb0-95bd-dde42536b092'  # 任务的id
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('任务已经开始被执行')

celery异步任务、延迟任务、定时任务

# 异步任务
	任务.delay(参数,参数)
# 延迟任务
	任务.apply_async(args=[参数,参数],eta=时间对象(utc时间))
# 定时任务
	-配置文件:(在app的配置文件中配置 )
	app.conf.beat_schedule = {
        'send_sms_task': {
            'task': 'celery_task.user_task.send_sms',
            'schedule': timedelta(seconds=5),
            # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
            'args': ('1897334444', '7777'),
        },
        'add_task': {
            'task': 'celery_task.home_task.add',
            'schedule': crontab(hour=12, minute=10, day_of_week=3),  # 每周一早八点
            'args': (10, 20),
        }
    }
-启动beat(提交任务)
    celery -A celery_task beat -l info
-启动worker(开始干活)
    celery -A celery_task worker -l info -P eventlet  # 两者谁先谁后都可以
	

django中使用celery

# 使用步骤:
1 把写好的包复制到项目路径下
2 在包内的celery.py 的上面加入代码
    import os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
    import django
    django.setup()
3 在django的视图类中,导入,提交任务
4 启动worker,beat

秒杀逻辑

前端:
    点击按钮,发送秒杀请求,后端会返回“正在秒杀中”。发送完立马起了一个定时任务,每隔5s,向后端查看一下是否秒杀成功,如果秒杀没成功,定时任务继续执行,如果秒杀成功了,清空定时任务,弹窗告诉他秒杀结果
handleClick() {
  this.$axios.get(this.$settings.BASE_URL + 'userinfo/seckill/').then(res => {
    if (res.data.code == 100) {
      let task_id = res.data.id
      this.$message({
        message: res.data.msg,
        type: 'error'
      });
      // 起个定时任务,每隔5s向后端查询一下是否秒杀成功
      let t = setInterval(() => {
        this.$axios.get(this.$settings.BASE_URL + 'userinfo/get_result/?id=' + task_id).then(
            res => {
              if (res.data.code == 100 || res.data.code == 101) {  //秒杀结束了,要么成功,要么失败了
                alert(res.data.msg)
                // 销毁掉定时任务
                clearInterval(t)
              } else if (res.data.code == 102) {
                //什么事都不干
              }
            }
        )
      }, 5000)
    }
  })
}

后端:
    提交秒杀任务
    查询任务是否成功
1 秒杀接口
    提交秒杀任务
    def seckill(request):
        # 提交秒杀任务
        res = seckill_task.delay()
        return JsonResponse({'code': 100, 'msg': '正在排队', 'id': str(res)})
2 查询是否秒杀成功的接口
    根据用户传入的id,查询任务是否成功
    def get_result(request):
        task_id = request.GET.get('id')
        res = AsyncResult(id=task_id, app=app)
        if res.successful():
            result = res.get()  # 7
            return JsonResponse({'code': 100, 'msg': str(result)})
        elif res.failed():
            print('任务失败')
            return JsonResponse({'code': 101, 'msg': '秒杀失败'})
        elif res.status == 'PENDING':
            print('任务等待中被执行')
            return JsonResponse({'code': 102, 'msg': '还在排队'})

30.png

给轮播图接口加缓存

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

    def list(self, request, *args, **kwargs):
        result = cache.get('banner_list')
        if result:  # 缓存里有
            print('走了缓存,速度很快')
            return APIResponse(result=result)
        else:
            # 去数据库拿
            print('走了数据库,速度慢')
            res = super().list(request, *args, **kwargs)
            result = res.data.get('result')  # {code:100,msg:成功,result:[{},{}]}
            cache.set('banner_list', result)
            return res

双写一致性问题

加缓存可以提高接口的响应速度,提高并发量。
但是加了缓存之后,如果mysql的数据变了,但是请求的都是缓存里的数据,这样会导致mysql和redis的数据不一致。
# 双写一致性问题解决方案
    -1 修改mysql数据库,删除缓存  
    -2 修改数据库,修改缓存    
    	# 缓存的修改和删除一定要在mysql更新之后
    -3 定时更新缓存   ---》针对于实时性不是很高的接口适合定时更新
   

# 给首页轮播图接口加入了缓存,出现了双写一致性问题,使用定时更新来解决双写一致性的问题【会存在不一致的情况,我们可以忽略】---》定时任务,celery的定时任务

利用celery定时任务实现双写一致性

home_task.py

@app.task
def update_banner():
    # 更新缓存
    # 查询出现在轮播图的数据
    queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    ser = BannerSerializer(instance=queryset, many=True)
    # ser 中得图片,没有前面地址
    for item in ser.data:
        item['image'] = settings.HOST_URL + item['image']
    cache.set('banner_list', ser.data)
    return True

celery.py

app.conf.beat_schedule = {
    'update_banner': {
        'task': 'celery_task.home_task.update_banner',
        'schedule': timedelta(seconds=50),
        'args': (),
    }
}
# 启动django
# 启动worker
# 启动beat

# 第一次访问:查的数据库,放入了缓存
# 以后再访问,走缓存
# 一旦mysql数据改了,缓存可能不一致
# 当时我们定时更新,最终保持了一致