系列文章目录
Spring Cache的使用–快速上手篇
分页查询–Java项目实战篇
全局异常处理–Java实战项目篇
完善登录功能–过滤器的使用
上述只是部分文章,对该系列文章感兴趣的可以查看我的主页哦
文章目录
- 系列文章目录
- 前言
- 一、缓存穿透
- 1.1 问题引入
- 1.2 解决方法
- 1.2.1布隆过滤
- 1.2.2 缓存空对象
- 二、缓存击穿
- 2.1 问题引入
- 2.2 解决方法
- 2.2.1 互斥锁
- 2.2.2 缓存永不失效
- 三、缓存雪崩
- 3.1 问题引入
- 3.2 解决方法
- 3.2.1 保持缓存层的高可用性
- 3.2.2 限流降级组件
- 3.2.3 缓存不过期
- 3.2.4 优化缓存过期时间
- 3.2.5 使用互斥锁重建缓存
- 3.2.6 异步重建缓存
- 总结
前言
大家在学习Redis中不仅要学习到Redis的五大基本类型,会运用get\set进行获取和存储值,更多的是再学习时要知道为什么要使用Redis以及使用Redis的好处。相信大家在看到这篇文章时已经对Redis有了一定的理解和使用。我们都知道Redis缓存是用来减少数据库的压力,提高性能的内存存储的数据结构服务器。
说白了就是当用户在发送请求数据库时,首先会经过Redis缓存,Redis先进行查询用户想要的数据(Redis读写速度比数据库中读写要快很多),如果Redis缓存中有用户想要请求的数据的话就不需要去到数据库中查找数据而提升性能。反之,如果Redis中没有查到这个数据,那么就会经过数据库中查询数据返回给用户。就是在这个过程中就会出现文章标题展示的问题。下面我们来一一查看这些问题的由来和如何避免这些问题的发生。
一、缓存穿透
1.1 问题引入
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透;
用户想要查询一个数据,发现redis缓存中没有这个数据,也就是缓存没有命中,于是向数据库中查询这个数据。发现也没有,于是本次查询失败(没有查询到用户想要的数据)。当用户很多的时候在某一时刻访问的是一个数据库中不存在的数据,缓存都没有命中,于是都去请求了到数据库中。当请求的数量很大时,这会给数据库造成很大的压力(甚至数据库会崩掉),这时候就相当于出现了缓存穿透。
1.2 解决方法
解决方法:
- 布隆过滤
- 缓存空对象
1.2.1布隆过滤
如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。
Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。
- 优点
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
- 缺点
但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,则使用散列表足矣。
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
1.2.2 缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;也就是如果查询的数据库中没有的数据也会在Redis缓存中存入空值,用户在访问该数据时就会直接通过Redis就查询到空值返回结果,不需要再去数据库中查询。
但是这种方法会存在两个问题:
-
如果空值能够被缓存起来,这就意味着Redis缓存中需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
-
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
二、缓存击穿
2.1 问题引入
有时候,我们在访问热点数据时(商品秒杀)。比如:我们在某个商城购买某个热门商品。
为了保证访问速度,通常情况下,商城系统会把商品信息放到缓存中。但如果在10:00时缓存的数据过期,需要通过数据库查询重新加载数据到缓存中(假设10:00过了一秒后重新添加到缓存),那么就在这一秒内,因为是热点商品,大量的访问数据会直接进入数据库中,导致数据库瞬间带来巨大的压力。
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
2.2 解决方法
解决方法:
- 互斥锁
- 缓存永不失效
2.2.1 互斥锁
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行。从而降低本身的效率。
2.2.2 缓存永不失效
既然问题中说到了Redis缓存的热点数据会过期,我们就可以不设置过期时间,让其永久有效。比如参与秒杀活动的热门商品,由于这类商品id并不多,在缓存中我们可以不设置过期时间。
比如在秒杀活动开始前,我们可以提前从数据库中查询出商品的数据,然后同步到缓存中,提前做预热。等秒杀活动结束一段时间之后,我们再手动删除这些无用的缓存即可。
三、缓存雪崩
3.1 问题引入
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力,造成数据库后端故障,从而引起应用服务器雪崩。
- 流量激增:比如异常流量、用户重试导致系统负载升高;
- 缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
- 程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
- 硬件故障:比如宕机,机房断电,光纤被挖断等。
- 数据库严重瓶颈,比如:长事务、sql超时等。
- 线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;
3.2 解决方法
3.2.1 保持缓存层的高可用性
使用Redis 哨兵模式或者Redis 集群部署方式,即便个别Redis 节点下线,整个缓存层依然可以使用。除此之外,还可以在多个机房部署 Redis,这样即便是机房死机,依然可以实现缓存层的高可用。
3.2.2 限流降级组件
无论是缓存层还是存储层都会有出错的概率,可以将它们视为资源。作为并发量较大的分布式系统,假如有一个资源不可用,可能会造成所有线程在获取这个资源时异常,造成整个系统不可用。降级在高并发系统中是非常正常的,比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成整个推荐服务不可用。常见的限流降级组件如 Hystrix、Sentinel 等。
3.2.3 缓存不过期
Redis 中保存的 key 永不失效,这样就不会出现大量缓存同时失效的问题,但是随之而来的就是Redis 需要更多的存储空间。
3.2.4 优化缓存过期时间
设计缓存时,为每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效,造成缓存雪崩。
3.2.5 使用互斥锁重建缓存
在高并发场景下,为了避免大量的请求同时到达存储层查询数据、重建缓存,可以使用互斥锁控制,如根据 key 去缓存层查询数据,当缓存层为命中时,对 key 加锁,然后从存储层查询数据,将数据写入缓存层,最后释放锁。若其他线程发现获取锁失败,则让线程休眠一段时间后重试。对于锁的类型,如果是在单机环境下可以使用 Java 并发包下的 Lock,如果是在分布式环境下,可以使用分布式锁(Redis 中的 SETNX 方法)。
分布式环境下使用Redis 分布式锁实现缓存重建,优点是设计思路简单,对数据一致性有保障;缺点是代码复杂度增加,有可能会造成用户等待。假设在高并发下,缓存重建期间 key 是锁着的,如果当前并发 1000 个请求,其中 999 个都在阻塞,会导致 999 个用户请求阻塞而等待。
3.2.6 异步重建缓存
在这种方案下构建缓存采取异步策略,会从线程池中获取线程来异步构建缓存,从而不会让所有的请求直接到达存储层,该方案中每个Redis key 维护逻辑超时时间,当逻辑超时时间小于当前时间时,则说明当前缓存已经失效,应当进行缓存更新,否则说明当前缓存未失效,直接返回缓存中的 value 值。如在Redis 中将 key 的过期时间设置为 60 min,在对应的 value 中设置逻辑过期时间为 30 min。这样当 key 到了 30 min 的逻辑过期时间,就可以异步更新这个 key 的缓存,但是在更新缓存的这段时间内,旧的缓存依然可用。这种异步重建缓存的方式可以有效避免大量的 key 同时失效。
总结
Redis缓存穿透、击穿、雪崩问题及解决方法是面试中必须掌握的知识点,对于我们而言要自己理解每个问题的场景和理解几个解决方法。知道为什么要这样解决该问题。当然在不同的场景下可以选择不同的解决方法从而达到理想中的最优。