赞
踩
首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作
例如首页,由于首页被访问的频率最高,如果每一次访问首页都需要去数据库查询,那么数据库的压力会很大。可以将数据写入缓存,下一次直接去缓存中取,这样就会减轻数据库的压力。
这里使用装饰器的方式实现查看缓存是否有值
def cache_decorator(data):
def outer(func):
def inner(*args, **kwargs):
from django.core.cache import cache
from rest_framework.response import Response
common_list = cache.get(data)
if common_list:
return Response(data=common_list)
else:
res = func(*args, **kwargs)
cache.set('banner_list_cache', res.data)
return res
return inner
return outer
但是使用缓存的方式会出现一个问题,当数据库更新的时候,缓存不会随之更新。这时候就需要考虑双写一致性
先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。 在这里,我们讨论三种更新策略
我这里采用定时缓存,使用 Django 作为案例演示
1. 将 celery_task 包放到项目根路径下
2. 在包中的 celery.py 文件编写以下内容
import os
# Luffy.settings.dev 是自己配置文件的路径
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Luffy.settings.dev")
import django
django.setup()
3. 然后可以编写任务,任务.delay()、任务.apply_async() 等方式提交任务,这里使用定时自动提交
4. 最后启动 worker,启动 beat
启动 worker : 'celery -A celery_task worker -l info -P eventlet'
启动 beat: 'celery -A celery_task beat -l info'
from .celery import app from home.models import Banner from django.conf import settings from home.serializers import BannerSerializer from django.core.cache import cache @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) for item in ser.data: item['image'] = settings.HOST_URL + item['image'] # 放到redis中 cache.set('banner_list_cache', ser.data) return True
这套方案,大家是普遍反对的。为什么呢?有如下两点原因。
原因一(线程安全角度) 同时有请求A和请求B进行更新操作,那么会出现
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
原因二(业务场景角度) 有如下两点:
接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
那么,如何解决呢?采用延时双删策略 伪代码如下
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
转化为中文描述就是
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果你用了mysql的读写分离架构怎么办?
ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
采用这种同步淘汰策略,吞吐量降低怎么办?
ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
第二次删除,如果删除失败怎么办?
这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。