当前位置:   article > 正文

Redis 与 Mysql 双写一致性方案解析_mysql与redis 如何保证双写一致性

mysql与redis 如何保证双写一致性

一、前言

首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作

在这里插入图片描述
例如首页,由于首页被访问的频率最高,如果每一次访问首页都需要去数据库查询,那么数据库的压力会很大。可以将数据写入缓存,下一次直接去缓存中取,这样就会减轻数据库的压力。

二、装饰器

这里使用装饰器的方式实现查看缓存是否有值

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

三、一致性方案

但是使用缓存的方式会出现一个问题,当数据库更新的时候,缓存不会随之更新。这时候就需要考虑双写一致性

先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。 在这里,我们讨论三种更新策略

  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存(使用较多)
  • 定时更新缓存,需要使用 celery 的定时任务

定时更新缓存

我这里采用定时缓存,使用 Django 作为案例演示

  • 首先需要在 celery_task 包中创建 py 文件,里面存放定时任务函数,用于定时将数据库的数据同步到缓存,第一步需要添加配置,由于 celery_task 包是独立的服务,没有环境变量,不能使用Django的东西,所以需要在 Django 中集成 celery
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'
	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 任务函数
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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

先更新数据库,再更新缓存

这套方案,大家是普遍反对的。为什么呢?有如下两点原因。

原因一(线程安全角度) 同时有请求A和请求B进行更新操作,那么会出现

  • 线程A更新了数据库
  • 线程B更新了数据库
  • 线程B更新了缓存
  • 线程A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

原因二(业务场景角度) 有如下两点:

  • 如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
  • 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

先删缓存,再更新数据库

该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库 上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决呢?采用延时双删策略 伪代码如下

public void write(String key,Object data){
        redis.delKey(key);
        db.updateData(data);
        Thread.sleep(1000);
        redis.delKey(key);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

转化为中文描述就是

  • 先淘汰缓存
  • 再写数据库(这两步和原来一样)
  • 休眠1秒,再次淘汰缓存 这么做,可以将1秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?

针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

如果你用了mysql的读写分离架构怎么办?

ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

  • 请求A进行写操作,删除缓存
  • 请求A将数据写入数据库了,
  • 请求B查询缓存发现,缓存没有值
  • 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
  • 请求B将旧值写入缓存
  • 数据库完成主从同步,从库变为新值 上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

采用这种同步淘汰策略,吞吐量降低怎么办?

ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。

第二次删除,如果删除失败怎么办?

这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库
  • 请求A试图去删除请求B写入对缓存值,结果失败了。 ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。 如何解决呢? 具体解决方案,且看博主对第(3)种更新策略的解析。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/165167
推荐阅读
相关标签
  

闽ICP备14008679号