Code前端首页关于Code前端联系我们

民工兄的Redis教程(十七):缓存问题(一致性、失败、渗透、雪崩、污染)

terry 2年前 (2023-09-26) 阅读数 97 #后端开发

缓存含义

缓存一些数据(最近访问过的),当客户端需要访问数据库中的数据时,可以访问首先缓存。如果里面存在相关数据,就无法访​​问数据库,从而减轻数据库的压力。

那么客户端对数据库的操作包括增删改查,但是只有在查询数据库中的信息时才会访问缓存。那么缓存数据是如何更新的呢?他会不会出现数据不及时更新的问题?

如何保证缓存数据与数据库的一致性

将数据放入缓存的时机

对于客户端来说,查询数据的步骤如下:

  • 1.首先,查找缓存中的数据。如果数据存在,则直接获取数据,返回
  • 2。如果缓存不存在,则需要查找数据库,从数据库中取出数据放入缓存中,返回数据
  • 3。当第二次查询数据且缓存中的数据未过期时,查询操作可以要求缓存获取对应的数据

缓存更新数据(3个选项)

客户端执行更改操作在数据库上:

1。先清除缓存,再更新数据库

更新数据库数据时,先清除缓存,再更新数据库。当后续请求重新加载数据时,将从数据库检索数据并将其更新到缓存中。

有一个问题:如果在清除缓存之后、更新数据库之前这段时间里出现了新的请求,就会从数据库中读取旧的数据,写入到缓存中,这样会再次造成数据不一致,导致后续的数据不一致。读操作会不一致。都是旧数据了

2。先更新数据库,再清除缓存

执行更新操作,先更新数据库。成功时清除缓存,并通过后续请求将新数据写回到缓存中。

有一个问题:在更新MySQL之间和清除缓存之前,请求获取缓存中的旧数据。但是,一旦数据库更新完成,一致性就会恢复。

3。异步更新缓存

数据库更新操作完成后,不直接控制缓存。操作命令封装在消息中,放入消息队列中。然后Redis 会自行更新数据。消息队列保证了数据流量的数据一致性。确保缓存数据正常。

更多关于Redis学习的文章可以在:NoSQL数据库系列-Redis中找到。该系列不断更新。

缓存问题

缓存溢出

大量请求在数据库中找不到匹配的数据

概念

缓存查询意味着用户想要在Redis中查找并找到数据,那就意味着它是缓存中没有命中,就像持久化数据库发起查询,发现数据库没有数据,所以查询失败。当用户请求较多时,缓存不会命中,数据库就没有数据。所有用户直接访问数据库,对数据库造成重大损害。压力,那就是缓冲区泄漏。 民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

解决方案
  • 第一个解决方案:使用布隆过滤器

查看相关数据是否在这个数据库中。使用布隆过滤器。如果返回全1,则可能存在;如果返回的结果有不为1的,那么数据库中肯定没有。这样就可以拒绝访问数据库的请求,从而大大减轻了数据库的压力。

布隆过滤器实现基于非常大的位字段和多个哈希函数。假设位域的长度为m,哈希函数的数量为k。 民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染) 以上图为例。具体操作过程:假设集合{x,y,z}中有3个元素,哈希函数的数量为3。首先,初始化位数组,将其中的每一位设置为0。对于集合中的每个元素,元素按顺序通过三个哈希函数进行映射。每个映射都会生成一个与位域上的点对应的哈希值,然后将相应的位域位置标记为 1 。当询问集合中是否存在元素 W 时,相同的方法使用散列将 W 映射到位数组中的三个点。如果这三个点中有一个不为1,则可以断定该元素一定不存在于集合中。反之,如果三个点都为1,则该元素可以存在于集合中。注意:这里无法判断某个元素是否一定存在于集合中,可能会出现某种程度的误判。如图所示:假设一个元素通过映射对应了下标为4、5、6的三个点。虽然这三个点都是1,但是很明显这三个点是对不同元素进行哈希处理得到的位置。因此,这种情况表明即使元素不在集合中,但它们也可以都等于1。这是判断错误的衡量标准。存在的理由。

使用布隆过滤器后,将保存的数据插入到布隆过滤器中。每个数据查询首先询问布隆过滤器。当判断过滤器中存在时,就去数据库缓存中执行查询。如果没有输入任何数据,Query if the filter does notise会直接返回并通知用户找不到数据,这样可以大大减轻数据库查询的压力。 民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

  • 第二个选项:缓存空对象

当数据库数据不存在时,及时返回的空对象也会被缓存,并设置过期时间。然后在访问时从缓存中检索数据,从而保护数据库。

现有问题:

  • 1。如果将过期时间设置为空,则在更新数据库数据和使缓存数据失效之间经过一段时间。如果缓存中存储的数据出现问题,就会影响业务,必须保证数据的一致性
  • 2、需要更多的空间来存储更多的控件,从而导致大量的内存中存在空值的key

缓存分裂

请求量过大,缓存突然过期

缓存分裂适用于该key是热点key,不断携带大数据并发量。当缓存的key过期后,持续的大并发会破坏缓存,直接请求数据库。它会立即导致数据库压力过大。

解决方案

  • 第一个解决方案:热点数据永不过期

从缓存角度来说,如果不设置过期时间,那么缓存过期后就不会出现问题。

  • 第二个方案:添加互斥锁

使用分布式锁,保证每个key一次只有一个线程可以访问后端服务,其他没有锁权限的线程可以等待。 。

缓存雪崩

在某个时间点,缓存会过期或者Redis会崩溃。

对于数据库来说,所有的请求压力都集中到了数据库上,导致数据库调用突然增加,也会导致数据库崩溃。 民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

解决方案

  • 第一种方案:Redis采用高可用

这个方案的思路是,数据存储在Redis的服务器上。即使一台服务器挂掉,其他服务器也可以继续工作。

  • 方案二:限流降级

这个思路是在缓冲区过期后通过加锁或者排队的方式来控制读数据库的线程数量,从而可以让线程排队来控制整体的请求速率。

  • 第三种选择:数据预热

数据预热是在业务部署之前对数据的访问。大部分数据可以加载到缓存中,其他数据在高并发发生之前就已经加载完毕。关键,设置不同的过期时间,让缓存过期时间更加均匀。更多学习Redis的文章请参见:NoSQL数据库系列-Redis。该系列不断更新。

双写一致性

含义

双写一致性的含义是保证缓存中的数据与数据库中的数据一致。

单线程方案

单线程实际上是指并发量不是很大,或者对缓存和DB数据一致性要求不是太高的情况。

这个问题很经典:缓存+数据库读写模式就是Cache Aside Pattern

解决思路

- 查询时先检查缓存。如果缓存中有数据,则直接返回;缓存中没有数据。 ,查询数据库然后更新缓存。- 更新数据库后清除缓存。

分析:

(1)。为什么更新数据库后缓存不是更新而是清除?

举个例子,如果数据库更新非常频繁,比如1分钟更新100次,那么缓存更新的话,缓存也会更新100次,但是直到这一分钟根本不会被调用。也就是说,缓存每10分钟只能查询一次,所以频繁的缓存更新会造成很多不必要的开销。

所以我们的想法是:仅在缓存使用时才对缓存进行计数。

(2)。这个方案适用于高并发场景吗?

不适用

例如更新数据库后,还没来得及清除缓存,其他请求就已经从缓存中加载了数据。此时加载的数据与数据库中的实际数据不一致。

高并发解决方案

使用内存队列来解决,将读写请求排队并按顺序执行(即串行化解决方案)。 (需要定义多行,不同的产品放在不同的行中,也就是说同一行中只有一种产品)

分析:

这种方案也有缺点。当并发量较高时,队列很容易被阻塞,这个队列的位置就成为了整个系统的瓶颈,所以没有100%完美的解决方案。只有最合适的方案,没有最完美的方案。 民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

并发争用
含义

多个微服务系统必须同时控制同一个redis key。例如,正确的顺序是A→B→C。当A执行时,网络突然抖动,导致B、C先执行,导致整个过程出现业务错误。

解决方案

引入分布式锁(zookeeper或者redis本身)

每个系统在启动之前必须先通过Zookeeper获取分布式锁,保证系统同时只能控制一个实例这个key否,不允许其他系统读取或写入。

热点缓存键重构优化

后台

开发者采用“缓存+过期时间”的策略,不仅可以加快数据读写速度,还能保证数据定期更新。这个型号基本上可以满足大部分需求。但是,有两个问题如果同时出现,可能会对应用程序造成致命的损害:

  • 当前的按键是热键(例如热门娱乐新闻)并且并发量非常大。
  • 重建缓存不可能在短时间内完成,可能是复杂的计算,比如复杂的SQL、多次IO、多重依赖等。

缓存失效的那一刻,有大线程去重建缓存,增加了后端的负载,甚至可能导致应用程序崩溃。

解决方案

解决这个问题的主要方法是避免大量线程同时刷新缓存。

我们可以使用互斥锁来解决这个问题。该方法只允许一个线程刷新缓存。其他线程等待刷新缓存的线程完成执行并再次从缓存中检索数据。

代码思路分享:

String get(String key) {
 // 从Redis中获取数据
 String value = redis.get(key);
 // 如果value为空, 则开始重构缓存
 if (value == null) {
  // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
  String mutexKey = "mutext:key:" + key;
  if (redis.set(mutexKey, "1", "ex 180", "nx")) {
    // 从数据源获取数据
    value = db.get(key);
    // 回写Redis, 并设置过期时间
    redis.setex(key, timeout, value);
    // 删除key\_mutex
    redis.delete(mutexKey);
  }
  else {
  //其它线程休息50ms,重写递归获取
  Thread.sleep(50);
  get(key);
  }
}
  return value;  
}

更多学习Redis的文章请参见:NoSQL数据库系列中的Redis。该系列不断更新。

缓存脏(或已满)

缓存脏问题涉及缓存中的某些数据,这些数据只会被访问一次或几次。一旦被访问,就不再被访问,但这部分数据仍然保留在缓存中,占用缓存空间。

缓存污染会随着数据的不断增长而逐渐显现出来。随着服务继续运行,缓存中将会有大量数据永远不会被再次访问。缓冲区空间有限。如果缓冲区空间已满,向缓冲区写入数据时会产生额外的开销,从而影响Redis的性能。这部分额外开销主要与考虑写淘汰策略,根据淘汰策略选择要删除的数据,然后执行删除操作有关。

最大缓存设置是多少?一般来说,考虑到访问性能和内存空间开销,我建议将缓存容量设置为总数据量的15%到30%。

对于 Redis,一旦确定了最大缓冲区容量,例如 4GB,就可以使用以下命令来设置缓冲区大小:

CONFIG SET maxmemory 4gb

但是缓冲区必然会被填满,因此需要采取数据淘汰策略。

缓存淘汰策略

Redis 总共支持 8 种缓存淘汰策略,分别是 noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random、allkeys-lfu 策略。

你怎么理解?主要分为三类:

  • noeviction
    • noeviction(4.0版本后默认)
  • 设置过期时间的数据淘汰
    • Random:易失性随机
      • lru : volatile -lru
      • lfu: volatile-lfu
    • 所有数据被删除
      • random: allkeys-random
      • lru: allkeys-lru
      • lfu: allkeys 和 BigKey 优化
        什么是 BigKey 在Redis中,一个字符串最大可达512MB,一个二级数​​据结构(如hash、list、set、zset)可以存储大约40亿(2^32-1)个元素,但在实际中,如果出现以下两种情况,我会认为是一个很大的关键。

        字符串类型:它的大小体现在单个值的大上。如果超过 10 KB,通常被认为是大密钥。

        非字符串类型:哈希、列表、集合、有序集合,它们的大小体现在元素的数量上。

        一般情况下,string类型控制在10KB,hash、list、set、zset元素个数不要超过5000个。反例:包含200万个元素的list。对于大的未链接的钥匙,不要使用 del 来删除它们。使用hscan、sscan、zscan方法一一删除。同时要注意防止big key过期时间自动清除(比如200万个zset设置1小时过期,会触发del操作,造成Blocking)

        BigKey的危害
        • 导致redis阻塞
        • 网络拥塞

        Bigkey表示每次产生的网络流量较大。假设大密钥是1MB,那么每秒的客户端访问量是1000,那么每秒就会产生1000MB的流量,这对于普通千兆网卡(128MB/s以字节为单位)的服务器和一般的服务器来说简直就是一场灾难将使用具有多个实例的单机进行部署。这意味着大钥匙

        还会影响其他案件,后果将是灾难性的。

        • 过期清除

        有一个大键保留自己(只执行简单的命令,如 hget、lpop、zscore 等),但设置过期时间。过期后将被删除。如果不是使用Redis 4.0,异步删除(只有lazy-lazy-expire yes)会阻塞Redis。

        BigKey的出现

        一般来说,bigkey的出现是由于程序设计不正确或者对数据大小预测不明确造成的。我们来看几个例子:

        社交网络类别:粉丝列表,一些名人或大V如果不精心设计,绝对会成为大钥匙。

        统计:例如,按天保存网站用户的某个功能或集合。如果很少人使用它,它一定是bigkey。

        缓存类:从数据库加载数据并序列化到Redis。这种方法很常见,但是有两点需要注意:第一,所有字段都需要缓存;类型? ,有相关数据吗?有的同学为了绘图方便,把所有相关数据都存储在一个按键下,导致按键很大。

        优化BigKey

        -拆分

        big list:list1,list2,...listN

        big hash:数据可以分段存储,比如big key,存储的用户数据如何拆分到 200 个 key,每个 key 下存储 5000 个用户数据

        • 明智地掌握数据结构

        如果需要 bigkey,你还应该考虑每次删除所有元素(例如,有时你需要 hmget 而不是 hgetall ) ,删除也是一样,尝试用优雅的方式来处理。

        反例:

        set user:1:name tom
        set user:1:age 19
        set user:1:favor football
        

        推荐的哈希存储对象:

        hmset user:1 name tom age 19 favor football
        
        • 控制key生命周期,redis不是垃圾桶。

        建议使用expire来设置过期时间(如果条件允许,可以将过期时间分散,避免集中过期)。 参考文章:https://blog.csdn.net/xkyjwcc/article/details/121704554 https://www.cnblogs.com/shoshana-kong/p/246html

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门