缓存一致性设计思路

这篇具有很好参考价值的文章主要介绍了缓存一致性设计思路。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

  1. Spring注解使用,控制Redis缓存更新
  2. 缓存一致性问题是如何产生的?
  3. 双更新模式:操作不合理,导致数据一致性问题
  4. “后删缓存”,能解决多数不一致
  5. 大厂高并发,“后删缓存”依旧不一致
  6. 如何解决高并发的不一致问题?延迟双删与闪电缓存
  7. 如何解决缓存击穿?读操作互斥与集中更新

Redis 是现在互联网中使用最广泛的分布式缓存系统,几乎每家公司都在用。它的 qps 可以达到10万每秒,吞吐量还是非常可观的,对于一般体量的互联网公司,一台机器就够了。但不论是什么业务,都不得不面对一个棘手的问题:那就是Redis和源数据的一致性问题

一、Spring注解使用,控制Redis缓存更新

使用 SpringBoot 可以很容易地对 Redis 进行操作。Java 的 Redis 的客户端常用的有三个:jedis、redisson、lettuce。其中,Spring 默认使用的是 lettuce。
很多人喜欢使用 Spring 抽象的缓存包 spring-cache,它可以使用注解,非常方便。它的注解采用 AOP 的方式,对 Cache 层进行了抽象,可以在各种堆内缓存框架和分布式框架之间进行切换。

我们来看一下它的 maven 坐标:

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-cache</artifactId> 
</dependency>

使用spring-cache有三个步骤:

  1. 在启动类上加上@EnableCaching 注解;
  2. 使用 CacheManager 初始化要使用的缓存框架,使用 @CacheConfig 注解注入要使用的资源;
  3. 使用@Cacheable等注解对资源进行缓存。而针对缓存操作的注解有三个:

@Cacheable 表示如果缓存系统里没有这个数值,就将方法的返回值缓存起来;
@CachePut 表示每次执行该方法,都把返回值缓存起来;
@CacheEvict 表示执行方法的时候,清除某些缓存值。

二、缓存一致性问题是如何产生的?

在说缓存一致性问题如何产生的前,我们先看下缓存的API操作,缓存操作和数据库的CRUD结合起来,大致可以抽象成以下几个方法:

  • getFromDB(key)
  • getFromRedis(key)
  • putToDB(key,value)
  • putToRedis(key,value)
  • deleteFromDB(key)
  • deleteFromRedis(key)

把Redis当缓存使用,就说明Redis是不合适作为落地存储的。
一般我们是把最终的数据存放在数据库中的,,一般情况下,Redis 的操作速度比数据库的操作速度快得多。毕竟是 10wQPS 和上千 QPS 的对比。

上面这些 API 很简单,但把它们的顺序调整一下,一致性就会出现问题。一致性,简单说就是“数据库里的数据”与“Redis 中的数据”不一样了。
对于读的过程,一般是没什么异议的。

  • 首先,读缓存;
  • 如果缓存没有值,那就读取数据库的值;
  • 同时把这个值写进缓存中;

我们下面主要看一下写模式。

三、双更新模式:操作不合理,导致数据一致性问题

我们来看下常见的一个错误编码方式,这些是代码 review 时要着重看的点,也是常出问题的地方。

public void putValue(key,value){
    putToRedis(key,value);
    putToDB(key,value);//操作失败了
}

比如我们需要更新一个值,首先刷了缓存,然后把数据库也更新了。但更新数据库过程中出现了异常,发生了回滚。所以,最后“缓存里的数据”和“数据库的数据”就不一样了,也就是出现了数据不一致的问题。

那如果先更新数据库,再更新缓存呢?如代码:

public void putValue(key,value){
    putToDB(key,value);
    putToRedis(key,value);
}

这依然会有问题。

考虑到下面的场景:操作 A 更新 a 的值为 1,操作 B 更新 a 的值为 2。由于数据库和 Redis 的操作,并不是原子的,它们的执行时长也不是可控制的。当两个请求的时序发生了错乱,就会发生缓存不一致的情况。

放到实操中来说:A 操作在更新数据库成功后,再更新 Redis;但在更新 Redis 之前,另外一个更新操作 B 执行完毕。那么操作 A 的这个 Redis 更新动作,就和数据库里面的值不一样了。
其实双更新模式的问题,主要不是体现在并发的一致性上,而是业务操作的合理性上。

我们大多数业务代码并没有经过良好的设计。一个缓存的值,可能是多条数据库记录拼凑或计算得出来的。比如一个余额操作,可能是“钱包里的值”加上“基金里的值”计算得出来的。

要是采用“更新”的方式,那这个计算代码就分散在项目的多个地方,这就不合理了。

那么怎么办呢?其实,我们把“缓存更新”改成“删除”就好了。

四、“后删缓存”,能解决多数不一致

因为每次读取时,如果判断 Redis 里没有值,就会重新读取数据库,这个逻辑是没问题的。唯一的问题是:我们是先删除缓存?还是后删除缓存?

答案是后者!

1、如果先删缓存

我们来看一下先删除缓存会有什么问题:

public void putValue(key,value){
    deleteFromRedis(key);
    putToDB(key,value);
}

操作 B 删除了某个 key 的值,这时候有另外一个请求 A 到来,那么它就会击穿到数据库,读取到旧的值。无论操作 B 更新数据库的操作持续多长时间,都会产生不一致的情况。

2、如果后删缓存

而把删除的动作放在后面,就能够保证每次读到的值都是新鲜的,从数据库里面拿到最新的。

public void putValue(key,value){
    putToDB(key,value);
    deleteFromRedis(key);
}

这就是我们通常说的Cache-Aside Pattern,也是我们平常使用最多的模式。我们看一下它的具体方式。
先看一下数据的读取过程,规则是“先读 cache,再读 db”,详细步骤如下:

  1. 每次读取数据,都从 cache 里读;
  2. 如果读到了,则直接返回,称作 cache hit;
  3. 如果读不到 cache 的数据,则从 db 里面捞一份,称作 cache miss;
  4. 将读取到的数据塞入到缓存中,下次读取时,就可以直接命中。

再来看一下写请求,规则是“先更新 db,再删除缓存”,详细步骤如下:

  1. 将变更写入到数据库中;
  2. 删除缓存里对应的数据。

为什么说最常用呢?因为 Spring cache 就是默认实现了这个模式。

五、大厂高并发,“后删缓存”依旧不一致

所以在高并发情况下,Cache Aside Pattern会不够用。下面就描述一个“先更新再删除”这种场景下,依然会产生不一致的情况。场景很好理解、很极端,但在高并发多实例的情况下很常见。

有一系列的高并发操作,一直执行着更新、删除的动作。某个时刻,它更新数据库的值为 1,然后删除了缓存。

public void proccess(key,value){
    N:putToDB(key,1);
    N:deleteFromRedis(key);

    A:getFromRedis(key);
    A:getFromDB(key)=1;
    B:putToDB(key,2);
    B:deleteFromRedis(key);
    A:putToRedis(key,1);

    //DB=2,Redis=1
}

正在这时,有两个请求发生了:

  • 一个是读操作,读到的当然是数据库的旧值 1,我们记作操作 A;

  • 同时,另外一个请求发起了更新操作,把数据库记录更新为 2,我们记作操作 B。

一般情况下,读取操作都是比写入操作快的,但我们要考虑两种极端情况:

  • 一种是这个读取操作 A,发生在更新操作 B 的尾部;

  • 一种是操作 A 的这个 Redis 的操作时长,耗费了非常多的时间。比如,这个节点正好发生了 STW。
    那么很容易地,读操作 A 的结束时间就超过了操作 B 删除的动作。就像上图虚线部分画的一样,这个时候,数据也是不一致的。

实际上,你也无法控制它们的执行顺序。只要发生这种情况,大概率数据库和 Redis 的值会不一致。

但为什么一般公司不去处理这种情况呢?你仔细看这张图,它发生的条件是非常苛刻的。它要求在一系列“并发写”的同时,还有“并发读”的参与。而一般业务是达不到这个量级的,所以一般公司不去处理这种情况,但高并发业务就非常常见了。

六、如何解决高并发的不一致问题?

大家看上面这种不一致情况发生的场景,归根结底还是“删除操作”发生在“更新操作”之前了。

1、延时双删

而假如有一种机制,能够确保删除动作一定被执行,那就可以解决问题,起码能缩小数据不一致的时间窗口。常用的方法就是延时双删,依然是先更新再删除,唯一不同的是:我们把这个删除动作,在不久之后再执行一次,比如 5 秒之后。

public void putValue(key,value){
    putToDB(key,value);
    deleteFromRedis(key);

    ...deleteFromRedis(key,after5sec);
}

而删除动作也有多种选择:

  • 如果放在 DelayQueue 中,会有随着 JVM 进程的死亡,丢失更新的风险;

  • 如果放在 MQ 中,会增加编码的复杂性。
    所以到了这个时候,并没有一个能够行走天下的解决方案。我们得综合评价很多因素去做设计,比如团队的水平、工期、不一致的忍受程度等。

2、闪电缓存

还有一种不太常用的,那就是采用闪电缓存。就是把缓存的失效时间设置非常短,比如 3~4 秒。一旦失效,就会再次去数据库读取最新数据到缓存。但这种方式,在非常高的并发下,同一时间对某个 key 的请求击穿到 DB,会锁死数据库,所以很少用。

对于一般并发场景,上面的各种修修补补,已经把不一致问题降低到很小的概率了。但是它仍然是有问题的,因为它引入了一个高可用问题:缓存击穿。

七、如何解决缓存击穿?

缓存击穿,指的是缓存中没有数据但数据库中有,由于同一时刻请求量特别大,但是没有读到缓存数据,就会一股脑涌入到数据库中读取,造成数据库假死。

任何删除缓存的动作都会造成缓存击穿。
所以我们上面一直说的是要删除缓存,但在极高并发下,你还不能乱删。
你反过头去看一下,好像我们一开始双更的方案比 Cache-Aside Pattern 还要靠谱一些,起码能用。怎么回事?代码还能不能写了?这就是业务开发中的特事特办,要专门针对这种功能进行编码。场景特殊时,代码也就不要追求极端优雅性了,毕竟也没有万能的解决方案。

这时,盘点一下我们手头上的工具,可以看到有两种不同的解决方式:

  • 读操作互斥,使用锁或者分布式锁来控制;

  • 更新集中,采用定时或者 binlog 的方式同步更新。

1、读操作互斥

先来看一下锁操作。我们依然采用 Cache-Aside Pattern,只不过在读的时候进行一下处理。来看一下伪代码,从 Redis 读取不到值的时候,我们要上锁去从数据库中读这个值。我们这里默认这个值是有的,否则就得处理缓存穿透的问题。

get(key){
    res = getFromRedis(key);
    //读取缓存为null
    if(null == res){
        lock.lock(...);
        //再次读取缓存为null
        res = getFromRedis(key);
        if(res == null){
            res = getFromDB(key);
            if(null != res){
                //读取设值
                putToRedis(key,res);
            }
        }
        lock.unlock();
    }
    return res;
}
getFromDB(key){
    ...
}

使用分布式锁和非分布式锁的主要区别,还是在于数据一致性窗口上:

  • 对于多线程锁来说,可能某些节点执行得非常慢,更新了旧的值到 Redis;

  • 对于分布式锁来说,肯定又是一个效率上的话题。

2、集中更新

我们再来看一下集中更新。这个很美好,但大多数业务很复杂,这对业务架构的前期设计要求非常高。比如通过 Binlog 方式,典型的如 Canal。我们不会在代码里做任何 Redis 更新的操作,而是会设计一个服务,订阅最新的 binlog 更新信息,然后解析它们,主动去更新缓存。这个一般在大并发大厂才会采用。

还有一种就是弱化数据库。所有的数据首先在 Redis 落地,也就是把 Redis 作为数据库使用,把数据库作为备份库使用。有定时任务,定期把 Redis 中的数据,保存到数据库或其他地方。

一般,重要业务还要配备一个对账系统,定时去扫描,以便快速发现不一致的情况。文章来源地址https://www.toymoban.com/news/detail-416682.html

到了这里,关于缓存一致性设计思路的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • CPU缓存一致性原理

    在本站的文章 CPU缓存那些事儿 中, 介绍了cpu的多级缓存的架构和cpu缓存行cache line的结构。CPU对于缓存的操作包含读和写,读操作在cache line中有所涉及,在本文中,将重点讨论CPU对于缓存进行写时的行为。 CPU对于高速缓存的读操作的过程在之前的文章中有提到过,这里梳理

    2024年02月12日
    浏览(32)
  • Redis之缓存一致性

    按照缓存更新的方式大致分为: 内存淘汰、过期删除、主动更新。 利用 Redis 的内存淘汰策略,当内存不足时自动进行淘汰部分数据,下次查询时更新缓存,一致性差,无维护成本。 因为 Redis 是基于内存的,如果内存超过限定值( Redis 配置文件的 maxmemory 参数决定 Redis 最大内

    2024年02月07日
    浏览(36)
  • 【Redis】之数说缓存一致性

    对于使用 Redis 作为缓存来说,如何保证数据库和缓存数据一致性是个麻烦的问题。对于缓存和数据库的操作,主要有以下两种方式: 先删缓存,再更新数据库; 先更新数据库,再删除缓存; 这两种方式都存在缓存一致性问题,下面我们就分析一下如何解决这两种方式的缓存

    2024年02月13日
    浏览(28)
  • 缓存和数据库一致性

    项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库,后更新缓存还是先更新数据库,然后删除缓存,在并发场景之下,仍然会存在数据不一致的情况(也存在删除失败的情况,删除失败可以使用异步重试解决)。有一种解决方法是延迟双删的策略,先删

    2024年01月17日
    浏览(32)
  • Redis缓存双写一致性

    如果redis中有数据:需要和数据库中的值相同 如果redis中无数据:数据库中的值要是最新值,且准备回写redis 缓存按照操作来分,可细分为两种: 只读缓存和读写缓存 只读缓存很简单:就是Redis只做查询,有就是有,没有就是没有,不会再进一步访问MySQL,不再需要会写机制

    2023年04月17日
    浏览(28)
  • Redis---缓存双写一致性

    目录 一、什么是缓存双写一致性呢?  1.1 双检加锁机制  二、数据库和缓存一致性的更新策略 2.1、先更新数据库,后更新缓存  2.2 、先更新缓存,后更新数据库  2.3、先删除缓存,在更新数据库 延时双删的策略:  2.4.先更新数据库,在删除缓存(常用) 2.5、实际中是不可

    2024年02月15日
    浏览(31)
  • Redis缓存(双写一致性问题)

    前言 : 什么是缓存? 缓存就像自行车,越野车的避震器 举个例子:越野车,山地自行车,都拥有\\\"避震器\\\", 防止 车体加速后因惯性,在酷似\\\"U\\\"字母的地形上飞跃,硬着陆导致的 损害 ,像个弹簧一样; 同样,实际开发中,系统也需要\\\"避震器\\\",防止过高的数据访问猛冲系统,导致其操作线程无法

    2024年02月02日
    浏览(42)
  • 缓存和数据库一致性问题分析

    目录 1、数据不一致的原因 1.1 并发操作 1.2 非原子操作 1.3 数据库主从同步延迟 2、数据不一致的解决方案 2.1 并发操作 2.2 非原子操作 2.3 主从同步延迟 2.4 最终方案 3、不同场景下的特殊考虑 3.1 读多写少的场景 3.2 读少写多的场景 导致缓存和数据库数据不一致的原因有三个

    2024年02月14日
    浏览(30)
  • 28.Netty源码之缓存一致性协议

    Mpsc 的全称是 Multi Producer Single Consumer,多生产者单消费者。 Mpsc Queue 可以保证多个生产者同时访问队列是线程安全的,而且同一时刻只允许一个消费者从队列中读取数据。 Netty Reactor 线程中任务队列 taskQueue 必须满足多个生产者可以同时提交任务,所以 JCTools 提供的 Mpsc Queu

    2024年02月13日
    浏览(29)
  • 浅谈缓存最终一致性的解决方案

    作者:clareguo,腾讯 CSIG 后台开发工程师 来源:腾讯技术工程open in new window 到底是更新缓存还是删除缓存? 到底是先更新数据库,再删除缓存,还是先删除缓存,再更新数据库? 对于互联网业务来说,传统的直接访问数据库方式,主要通过数据分片、一主多从等方式来扛住读

    2024年01月16日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包