民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

2023年 7月 10日 68.7k 0

缓存存在的意义

将一些数据(最近访问的)放在缓存中,当客户端需要访问数据库中数据时,可以先访问缓存,如果它里面存在这样对应的数据就不会去访问数据库,从而减小数据库的压力。

那么客户端对数据库的操作有 增删改查,但是只有当查数据库里面的信息时才会先访问缓存,那么缓存里的数据时如何更新的?它会不会有数据更新不及时的问题?

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

缓存数据插入的时机

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

  • 1、首先到缓存查询数据,如果数据存在则直接获取数据返回
  • 2、如果缓存不存在,需要查询数据库,从数据库获取数据并插入缓存,将数据返回
  • 3、当第二次查询这个数据时并且这个数据在缓存中尚未过期,查询操作就可以查询缓存拿到对应的数据

缓存更新数据(3种方案)

客户端对数据库进行一个更改操作:

1、先删除缓存在更新数据库

进行更新数据库数据时,先删除缓存,然后更新数据库,后续的请求再次读取数据时,会从数据库中读取数据更新到缓存。

存在问题:删除缓存之后,更新数据库之前,这个时间段内如果有新的请求过来,就会从数据库中读到旧的数据并写入缓存,再次造成数据不一致,并且后续读操作都是旧数据。

2、先更新数据库在删除缓存

进行更新操作,先更新数据库,成功之后,在删除缓存,后续请求将新数据写回缓存。

存在问题:更新MySQL之后和删除缓存之前的这段时间内,请求读取的还是缓存内的旧数据,不过等数据库更新完成后,就会恢复一致。

3、异步更新缓存

数据库的更新操作完成后不直接操作缓存,将操作命令封装成消息放到消息队列里,然后由Redis自己去更新数据,消息队列保证数据操作数据的一致性,保证缓存数据的数据正常。

更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。

缓存问题

缓存穿透

大量请求在数据库查不到相应数据

概念

缓存穿透是指用户想查询一个数据,发现Redis中没有,也就是缓存没有命中,就像持久性数据库发起查询,发现数据库也没有这个数据,于是查询失败了,当用户请求很多的情况下,缓存没有命中,数据库也没有数据,会都直接访问数据库,给数据库造成很大的压力,这就是缓存穿透。民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

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

判断对应的数据是否在这个数据库里,使用布隆过滤器,如果全返回1,则可能存在;如果返回结果存在一个不是1,那就肯定不在这个数据库中,这样就可以拒绝这个请求去访问数据库,大大降低数据库的压力。

布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k。民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。

使用布隆过滤器之后,将存储的数据放入布隆过滤器,每次数据查询首先查询布隆过滤器,当在过滤器中判断存在时在到数据库缓存查询,如果没有进入数据查询,如果在过滤器不存在,则直接返回告诉用户该数据查不到,这样能大大减轻数据库查询压力。民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

  • 第二种方案:缓存空对象

当数据库数据不存在时,及时返回的空对象也缓存起来,同时设置一个过期时间,之后在访问数据将从缓存中获取,保护了数据库。

存在问题:

  • 1、对空值设置过期时间,会存在更新数据库数据到缓存数据失效的一段时间,缓存数据有问题,会对要保证数据一致性的业务造成影响
  • 2、会需要更多的空间来存储更多的控制,造成内存中有大量的空值的键

缓存击穿

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

缓存击穿是指一个key是一个热点key,在不停的扛着大量的并发,当缓存中的key在失效的瞬间,持续的大并发就会穿破缓存,直接请求到数据库。对数据库造成瞬间压力过大。

解决方案

  • 第一种方案:热点数据永不过期

从缓存角度看,没有设置过期时间,就不会存在缓存过期之后产生的问题。

  • 第二种方案:加互斥锁

使用分布式锁,保证对每个key的访问同一时刻只能一个线程去查询后端服务,其他没有获取锁权限的线程则等待即可。

缓存雪崩

在某一个时间段,缓存集中过期失效或者Redis宕机。

对于数据库而言,所有请求压力会全部到达数据库,导致数据库调用量暴增,可能也造成数据库宕机的情况。民工哥死磕Redis教程(十七 ): 缓存问题(一致性、击穿、穿透、雪崩、污染)

解决方案

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

这种方案的思路就是讲数据在Redis中存放在服务器上,即使一个服务器挂掉,其他服务器还可以继续工作。

  • 第二种方案:限流降级

这种思路就是在缓存失效后,通过加锁或者队列来控制读取数据库的线程数量让线程在队列排队,控制整体请求速率。

  • 第三种方案:数据预热

数据预热及时在正是部署服务之前,先访问一遍数据,可以将大部分的数据加载到缓存中,在即将发生大并发之前已经加载不同的key,设置不同的过期时间,让缓存失效的时间更加均匀。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。

双写一致性

含义

双写一致性的含义就是:保证缓存中的数据和DB中数据一致。

单线程下的解决方案

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

该问题就是经典的:缓存+数据库读写的模式,就是 Cache Aside Pattern

解决思路

- 查询的时候,先查缓存,缓存中有数据,直接返回;缓存中没有数据,去查询数据库,然后更新缓存。- 更新DB的后,删除缓存。

剖析:

(1).为什么更新DB后,是删除缓存,而不是更新缓存呢?

举个例子,比如该DB更新的频率很高,比如1min中内更新100次把,如果更新缓存,缓存也对应了更新了100次,但缓存在这一分钟内根本没被调用,或者说该缓存10min才可能会被查询一次,那么频繁更新缓存是不是就产生了很多不必要的开销呢。

所以我们这里的思路是:用到缓存的时候,才去计算缓存。

(2).该方案高并发场景下是否适用?

不适用

比如更新DB后,还有没有来得及删除缓存,别的请求就已经读取到缓存的数据了,此时读取的数据和DB中的实际的数据是不一致的。

高并发下的解决方案

使用内存队列解决,把 读请求 和 写请求 都放到队列中,按顺序执行(即串行化的方式解决)。(要定义多个队列,不同的商品放到不同的队列中,换言之,同一个队列中只有一类商品)

剖析:

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

并发竞争
含义

多个微服务系统要同时操作redis的同一个key,比如正确的顺序是 A→B→C,A执行的时候,突然网络抖动了一下,导致B,C先执行了,从而导致整个流程业务错误。

解决方案

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

每个系统在操作之前,都要先通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作这个个 Key,别系统都不允许读和写。

热点缓存key的重建优化

背景

开发人员使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:

  • 当前key是一个热点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共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。

怎么理解呢?主要看分三类看:

  • 不淘汰
    • noeviction(v4.0后默认的)
  • 对设置了过期时间的数据中进行淘汰
    • 随机:volatile-random
    • ttl:volatile-ttl
    • lru:volatile-lru
    • lfu:volatile-lfu
  • 全部数据进行淘汰
    • 随机:allkeys-random
    • lru:allkeys-lru
    • lfu:allkeys-lfu

BigKey的危害及优化

什么是BigKey

在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey。

字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。

非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。

一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)

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

bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey

可能会对其他实例也造成影响,其后果不堪设想。

  • 过期删除

有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。

BigKey的产生

一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:

社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。

统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。

缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需注意:第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个key下,产生bigkey。

BigKey的优化

- 拆

big list:list1、list2、...listN

big hash:可以将数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据

  • 合理采用数据结构

如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理.

反例:

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

推荐hash存对象:

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/17226404.html

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论