硬核!我花 5 小时肝出这篇 Redis 缓存解决方案,带你起飞!
写在前面
对于缓存穿透,雪崩相信很多小伙伴都有听过,不管是工作中还是面试都热点问题,本文重点带大家分析这些问题,给位看官请往下看!
一、缓存穿透
1. 什么是缓存穿透?
为了缓解持久层数据库的压力,在服务器和存储层之间添加了一层缓存;
一个简单的正常请求: 当客户端发起请求时,服务器响应处理,会先从 redis 缓存层查询客户端需要的请求数据,如果缓存层有缓存的数据,会将数据返回给服务器,服务器再返回给客户端;如果缓存层中没有客户端需要的数据,则会去底层存储层查找,再返回给服务器;
image
缓存穿透就是: 当客户端想要查询一个数据,发现 redis 缓存层中没有(即缓存没有命中),于是向持久层数据库查询,发现也没有,于是本次查询失败;当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,此时会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
image
2. 解决办法
在缓存层加布隆过滤器,通俗简述一下其作用:将数据库中的 id ,通过某方式映射到布隆过滤器,当处理不存在的 id 时,布隆过滤器会将该请求过直接过滤出去,不会到数据库做操作。
image
3. 布隆过滤器
1)概述: 布隆过滤器是一种数据结构,比较巧妙的概率型数据结构,实际上是一个很长的二进制向量和一系列随机映射函数,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
2)返回结果的不确切性: 布隆过滤器是一个 bit 向量或者说 bit 数组:假设有 8 位
image
映射数据 1: 使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置为 1,比如三次 hash 完后,data1 将 1、3、6 位,置为 1;
image
映射数据 2: data2 将 2、3、6 位,置为 1,此时由于 hash 为随机性,所以 6 位和 data1 有重复的,便会覆盖 data1 的第 6 位的 1;
image
问题来了!!
6 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了,当我们如果想查询 data3 这个值是否存在,假设哈希函数返回了 1、5、6 三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 data3 这个值不存在。而当我们需要查询 data1 这个值是否存在的话,那么哈希函数必然会返回 1、3、6,然后我们检查发现这三个 bit 位上的值均为 1,那么我们是否可以说 data1 存在了么?答案是不可以,只能是 data1 这个值可能存在!因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 data4 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他位置位了 1 ,那么程序还是会判断 data4 这个值存在。
所以: 布隆过滤器的长度会直接影响误报率,布隆过滤器越长且误报率越小。
3)简单剖析布隆过滤器源码
导入 guava 的包:
源码: BloomFilter 一共四个 create 方法,最终都是走向第四个方法;
参数类型: funnel:数据类型;expectedInsertions:期望插入的值的个数;fpp:错误率(默认值为 0.03);strategy:哈希算法。
总结: 错误率越大,所需空间和时间越小;反之错误率越小,所需空间和时间越大!
二、缓存击穿
1、什么是缓存击穿?
在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿
2、问题排查
Redis 中某个 key 过期,该 key 访问量巨大
多个数据请求从服务器直接压到 Redis 后,均未命中
Redis 在短时间内发起了大量对数据库中同一数据的访问
3、如何解决
1. 使用互斥锁(mutex key)
这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。如果是单机,可以用 synchronized 或者 lock 来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用 memcache 的 add, redis 的 setnx, zookeeper 的添加节点操作)。
[图片上传失败...(image-6d9ac9-1611901471864)]
2. "提前"使用互斥锁(mutex key)
在 value 内部设置 1 个超时值(timeout1), timeout1 比实际的 redis timeout(timeout2)小。当从 cache 读取到 timeout1 发现它已经过期时候,马上延长 timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中
3. "永远不过期"
从 redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
image.png
4. 缓存屏障
4.总结
缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可。
三、缓存雪崩
1. 什么是缓存雪崩?
缓存雪崩是指: 某一时间段,缓存集中过期失效,即缓存层出现了错误,不能正常工作了;于是所有的请求都会达到存储层,存储层的调用量会暴增,造成 “雪崩”;
比如:双十二临近 12 点,抢购商品,此时会设置商品在缓存区,设置过期时间为 1 小时,当到了 1 点时,缓存过期,所有的请求会落到存储层,此时数据库可能扛不住压力,自然 “挂掉”。
2. 解决办法
redis 高可用
这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据预热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
四、缓存预热
1.什么是缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。如图所示:
image.png
如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
2.问题排查
请求数量较高
主从之间数据吞吐量较大,数据同步操作频度较高
3.有什么解决方案?
前置准备工作:
日常例行统计数据访问记录,统计访问频度较高的热点数据
利用 LRU 数据删除策略,构建数据留存队列
准备工作:
将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据
利用分布式多服务器同时进行数据读取,提速数据加载过程
热点数据主从同时预热
实施:
使用脚本程序固定触发数据预热过程
如果条件允许,使用了 CDN(内容分发网络),效果会更好
4.总结
缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
五、缓存降级
降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。我们直接访问内存部分数据缓存或者直接返回默认数据。
举例来说:
对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。如下图所示:
image.png
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
作者:码农清风
链接:https://juejin.cn/post/6922736577026195464
评论 (1 条评论)