到P1结束,redis都已经是一个不错的服务了,具体体现在缓存应用程序需要的数据,甚至在内存爆满的条件下还可以提供服务,似乎目的已经达成。但是实际上可能会遇到一些极端的情况,比如宕机。如果redis宕机了怎么办?目前所有的数据都存储在内存当中,宕机意为着失去所有缓存的数据。前面说过我理解的真正需要用到redis的应用程序,它们一定有大量的查询请求,可能造成后面躲着的数据库服务扛不住。
其实写到P1结束的时候,我突然意识到一个问题:在平时写的系统中,redis服务挂了,那些请求redis的API会抛出异常,这将导致明明还有数据库可以承担业务,但是用户的请求会全部异常,为什么没人捕获这个异常,继续向数据库请求呢?
@Service
class XxxService {
/* dependencies */
// Service Code
public Xxx xxxMethod () {
Xxx xxx = null;
if (/*redis opertions*/) {
/* xxx = ...*/
}
if (xxx == null) {
/* database opertions */
}
return xxx;
}
}
基于这个问题,去检索了一下,结果在知乎上还真的有讨论帖,里面的回答都很有道理:分布式系统中Redis等缓存系统宕机应不应该影响正常应用系统运行? - 知乎 (zhihu.com)
已知如果连接失败,则如果全局捕获了异常,则不会使系统宕机。
这个标题就让我更迷惑了:为什么是该不该影响?难道还有第二个选项:redis宕机了数据库能用也不用?这似乎不合常理,但仔细一想,也不是没有道理的,下面就来探讨一下这个问题[1]。不过前提,实际业务场景仍是要具体问题具体分析,这是永恒不变的,这里讨论的也只是某些情况。
回到为什么要加缓存这个问题,性能瓶颈是一方面,我认为保护系统(高可用)是另外一个重要的原因:假如请求的数量数据库可以支撑,那么就用数据库。但是如果大量的请求会击垮数据库呢?这就不止一个两个接口会无法使用,可能整个系统都无法使用!绝不可以一拍脑袋就将压力降下去,这是危险的。这个时候可以采取的方案有:
- 可以访问数据库,但开启熔断,使请求的数量维持在数据库可以接收的范围内;
- 放弃提供服务,抓紧抢修redis服务,使redis服务重新上线;
- 使用redis集群,在redis宕机时,让备用节点顶替,自动恢复,不过这个是需要更多的成本(得加钱)
如果redis服务在系统当中很重要(架构师在技术选型时已经确认很重要),那么对它的使用就要做好备用方案。
基于上面的问题,redis和数据库之间有三个常见的概念,涉及到请求的数据redis没有缓存:
缓存穿透
数据库里面没有这个数据(假如说是10),自然缓存也不存在,那么每次访问10的情况就都是:访问10->缓存没有,去数据库拿->数据库没有(没有也没办法写到缓存)。每次访问10都会造成资源浪费。
利用这,攻击者就可以制造大量的请求去攻击应用程序背后的db,比如攻击者可以获取user_id的格式,构造大量的根本不存在在数据库的请求(这一步甚至可以构造合法的请求),那么保护在数据库前的缓存的作用就消失了,拖慢系统的性能甚至让数据库宕机。这就是缓存穿透攻击。
解决方案
-
对请求的参数进行校验,不合法的参数直接忽略。不过上面也提到,这个是可以被绕过的,不过只要数据的格式隐藏的够深让人不好猜到,也能起到奇效。比如参数是123456789,但是如果12和89是加的前缀和后缀,那么只要攻击者不知道这个规则,就可以防御。
-
缓存空值,如果请求在数据库不存在,可以在缓存中存入一个特殊值代表没有,比如说“null”。这样也有坏处,如果攻击者构建了大量的不存在的键,则会给缓存带来很大压力,可能会导致新的缓存不能存入(noeviction内存淘汰策略),或频繁的进行内存淘汰、删除过期键时间长等等。
-
密码学防御,读者可以参考缓存穿透和密码学防御思路 - FreeBuf网络安全行业门户。
-
布隆过滤器(着重介绍)
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。布隆过滤器可以告诉我们 “某样东西一定不存在或者可能存在”,也就是说布隆过滤器说这个数不存在则一定不存,布隆过滤器说这个数存在可能不存在(即误判)。
利用这个判断是否存在的特点可以做很多有趣的事情。
- 解决Redis缓存穿透问题(面试重点)
- 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
- 对爬虫网址进行过滤,爬过的不再爬
- 解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
- HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求
这张图可以解释布隆过滤器的原理,二进制向量和散列函数。图中是一个两个字节大小的向量,有两个元素,三个散列函数,根据散列函数计算出来的值将对应的位置1。将来想要判断值是否存在的时候,需要判断这三个散列函数计算的值的在向量中的位是否都是1,如果都是,则证明这个值可能存在,否则一定不存在。
为什么是可能存在,其实也好理解,如图:
现在很凑巧,有一个请求查询的键为k3,根据散列函数计算结果如图所示,但是它在数据库中实际是不存在的!(即符合缓存穿透,缓存和数据库中都没有这个键)理想情况下,我们希望布隆过滤器可以识别这是个不存在的键,可惜这一次拦截失败了,布隆过滤器误判了,认为这个键存在,也即出现了一次“缓存穿透”。另外,当布隆过滤器中存的值越来越多,即置1的位越来越多,也会带来误判几率的增加。这从直觉上就可以解释。
但是即便如此,布隆过滤器的性能依旧是很优秀的,可以用于亿级数据的过滤,其性能可见一斑。此外,我们还可以通过数学计算及合理的配置来降低误判的几率。如何做到?Java对布隆过滤器的依赖包里有一个fpp(误判率)的参数,通过设置可以让误判率尽可能接近这个值。实际上,底层也是通过增加位数组的大小和使用更多的哈希函数来降低误判率的,不过这也会带来时空复杂度的消耗。增加位数组的长度自然可以让设置的位更加分散,增加哈希函数可以让键的匹配更精准,但是过多的哈希函数也会让一个键置1的位变多,可能也会增加误判率。实际开发中可以选择使用提供的布隆过滤器的API来控制误判率,如果想要自己实现,就要合理地平衡哈希函数的个数、数组长度和数据量之间的选值。至于个数应该怎么选,我没有很好的头绪,可能数学推导和统计实践可以计算出一个好的参数组合,如同JDK中HashMap的平衡因子那样,从实践中得到一个性能较好的数值。
原文链接:https://blog.csdn.net/qq_41125219/article/details/119982158
缓存击穿
缓存击穿,从名字上就感觉比缓存穿透要严重得多。这个名词针对的是热点key的场景。众所周知,redis的key可以设置过期时间,但是在大多数项目中,都会存在“热点数据”,即并发访问量很大的数据。缓存击穿中,我们指的都是单个热点key突然失效,导致大量的请求一瞬间全部击打在数据库上,导致数据库瞬间访问量激增,瞬时压力暴增甚至崩溃。
解决方案
根据业务的场景可以选择合适的解决方案,如热点数据永不过期、控制访问量,如熔断、互斥访问数据库(不好,低性能)。
缓存雪崩
缓存雪崩的名字又比缓存击穿恐怖一些,这次指的是大量的热点key几乎同一时间过期,依然是数据库瞬时压力激增崩溃,甚至重启以后还可能会被打到崩溃的严重现象,这叫缓存雪崩。还蛮好理解记忆的。
解决方案
解决缓存雪崩,主要的矛盾是不能让大量的热点数据在同一时间过期。那么,可以给每一个热点数据的过期时间上加上一些随机的值,让过期时间分散开。同时,熔断策略依然是有效的。还有为了防止因为redis服务宕机导致瞬时压力增加,还需要为redis提高容灾能力,如构建redis集群。当然,为数据库提高容灾能力是治标不治本的策略。
数据库中的缓存
详解MySQL中的Buffer Pool,深入底层带你搞懂它!-腾讯云开发者社区-腾讯云 (tencent.com)
这涉及到一个大家在准备后端的面试前一般会涉及到的概念——缓存击穿。这个概念一般与另外两个概念——缓存穿透、缓存雪崩有关联,为了不影响行文的连续性,我将会在P2的后面再来着重介绍这三个概念,现在扯回来 ↩︎