什么是缓存击穿?
缓存击穿是单个热点数据在缓存中过期时,大量的访问请求会穿越缓存直接请求数据库并尝试重建缓存,导致数据库瞬间承受巨大压力。
解决方案
解决缓存击穿有两种常用策略:加互斥锁和永不过期策略。
加互斥锁(Redis分布式锁)可以保证同一时刻只有一个进程能够访问数据库并更新缓存,其他进程等待或重试,进程获取到锁后,双重检查缓存,看看其他进程是否已经重建缓存了。
永不过期策略物理上不设置过期时间,但是会设置一个逻辑过期时间,如果数据逻辑上过期,也会返回旧数据,同时创建一个线程异步查询数据库并更新缓存,防止数据库中更改了,但是用户永远看到的是旧数据。
什么是缓存穿透?
缓存击穿是特指数据虽然在缓存中过期了,但是在数据库中一定能查到,大量请求落库。
但是缓存穿透是指数据压根就不存在,永远也查不到结果,仍然会有大量请求落库,也无法重建缓存。
常见的解决方案也有两种:布隆过滤器和缓存空值。
布隆过滤器
布隆过滤器用于查询一个元素是否在集合中,可以把所有可能存在的数据全部放到布隆过滤器中,每次查询数据的时候先通过布隆过滤器判断数据是否可能存在,不存在则直接返回查询失败,可能存在再去查询缓存,这样可以避免无效的查询。
布隆过滤器可能会误判,因为其原理是将一个数据通过不同的哈希算法映射到不同的二进制位上设置为1,下次查询的时候使用相同的一套哈希算法将数据再次哈希,如果哈希到的位置全是1,则有可能存在,但凡有一个位是0,表明数据一定不存在。可能出现的情况是,即使是没有加入布隆过滤器的数据,经过哈希算法后也有可能映射到1位。但是不会漏判
缓存空值
缓存空值是指,对于数据库中查不到的数据,我们也会建立缓存,只不过key不为null值为null。但要设置一个合理的过期时间,否则就算数据库中不再为null,也永远查不到。
在实际的项目中,在业务执行之前,还需要在接口层面做一些过滤,比如参数校验,拦截不合理请求,对可疑IP进行封禁。
什么是缓存雪崩?
大范围的缓存失效或者缓存服务器宕机,导致大量请求落入数据库
成因和解决策略
大范围的缓存同时过期,解决方法是设置随机过期时间。
缓存服务器崩溃,解决方式是使用高可用的Redis集群,比如Redis Cluster,数据在多个节点上有备份,节点分散在不同的物理机器,并且支持故障转移。或者对于一些高频的数据,设置本地缓存,当Redis出现问题时切换到本地缓存,这叫“缓存降级”。
说说布隆过滤器
布隆过滤器是一种空间效率极高的概率性数据结构,用于快速判断一个元素是否在一个集合中。它的特点是能够以极小的内存消耗,判断一个元素“一定不在集合中”或“可能在集合中”,常用来解决 Redis 缓存穿透的问题。
布隆过滤器存在误判吗?
误判产生的原因是因为哈希冲突。在布隆过滤器中,多个不同的元素可能映射到相同的位置。随着向布隆过滤器中添加的元素越来越多,位数组中的 1 也越来越多,发生哈希冲突的概率随之增加,误判率也就随之上升。
误判率取决于:位数组的大小m、哈希函数的数量k、存入的元素的数量n。
布隆过滤器支持删除吗?
不支持,这是它的一个重要限制,因为随着数据的删除,其对应的二进制位如果也要被设0,可能会错误地影响到其他元素的判断结果。
如果想实现删除操作,可以使用计数布隆过滤器,通过减少计数器的值来删除元素。但是由于不再是二进制位,空间开销会增大 。
为什么不能使用哈希表代替布隆过滤器?
布隆过滤器最突出的优势是内存效率。
假如我们要判断 10 亿个用户 ID 是否曾经访问过特定页面,使用哈希表至少需要 10G 内存(每个 ID 至少需要8字节),而使用布隆过滤器只需要 1.2G 内存。
如何保证缓存和数据库的一致性?
读取时先查 Redis,未命中再查 MySQL,同时为缓存设置一个合理的过期时间;更新时先更新 MySQL,再删除 Redis。这种方式简单有效,适用于读多写少的场景。TTL 过期时间也能够保证即使更新操作失败,未能及时删除缓存,过期时间也能确保数据最终一致。
为什么不是更新缓存而不是删除缓存?
因为多个并发的更新操作可能会导致缓存和数据库数据不一致。如果更新时直接删除缓存,无论 A 和 B 谁先删除缓存,后续的读取操作都会从 MySQL 获取最新值。其次,删除操作的效率高于更新操作。
为什么要先更新数据库再删除缓存?
- 如果先删除缓存,这时有另一个查询操作,发现缓存中没有数据,就从数据库中查,这时如果数据还没更新完成,仍然读取到的是旧值并且用旧值重建缓存,导致缓存中数据不是最新的。
而采用先更新数据库再删缓存的策略,即使出现类似的并发情况,最坏的情况也只是短暂地从缓存中读取到了旧值,但缓存删除后的请求会直接从数据库中获取最新值。
- 另外,如果先删缓存再更新数据库,当数据库更新失败时,缓存已经被删除了。这会导致短期内所有读请求都会穿透到数据库,对数据库造成额外的压力。
而先更新数据库再删缓存,如果数据库更新失败,缓存保持原状,系统仍然能继续正常提供服务。
如果对缓存数据库的一致性要求很高要怎么办?
第一种,引入消息队列来保证缓存最终被删除,比如说在数据库更新的事务中插入一条本地消息记录,事务提交后异步发送给 MQ 进行缓存删除。 即使缓存删除失败,消息队列的重试机制也能保证最终一致性。
第二种,使用 Canal 监听 MySQL 的 binlog,在数据更新时,将数据变更记录到消息队列中,消费者消息监听到变更后去删除缓存。这种方案的优势是完全解耦了业务代码和缓存维护逻辑。
如果说业务比较简单,不需要上消息队列,可以通过延迟双删策略降低缓存和数据库不一致的时间窗口,在第一次删除缓存之后,过一段时间之后,再次尝试删除缓存。主要防止脏数据
无论采用哪种策略,最好为缓存设置一个合理的过期时间作为最后的保障。即使所有的主动删除机制都失败了,TTL 也能确保数据最终达到一致。
如何保证本地缓存和分布式缓存的一致性?
通过Redis的sub/pub机制向所有应用实例发送缓存更新通知或通过版本号机制。
为了保证 Caffeine 和 Redis 缓存的一致性,我采用的策略是当数据更新时,通过 Redis 的 pub/sub 机制向所有应用实例发送缓存更新通知,收到通知后的实例立即更新或者删除本地缓存。
还可以每次从 Redis 获取数据的同时也获取该数据的版本号。从本地缓存获取数据前,先检查自己的版本号是否是最新的,如果发现版本落后,就主动从 Redis 中获取最新数据。
如果项目的多个地方都要使用二级缓存,如何设计?
将二级缓存抽象成一个统一的组件,然后用CacheManager封装LocalCache,RedisCache和Database,提供get、set、evict等基本操作,执行先查本地缓存,再查分布式缓存,最后查数据库的完整流程。
本地缓存和Redis缓存的区别了解吗?
Redis 可以部署在多个节点上,支持数据分片、主从复制和集群。而本地缓存只能在单个服务器上使用。
对于读取频率极高、数据相对稳定、允许短暂不一致的数据,我优先选择本地缓存。比如系统配置信息、用户权限数据、商品分类信息等。
而对于需要实时同步、数据变化频繁、多个服务需要共享的数据,我会选择 Redis。比如用户会话信息、购物车数据、实时统计信息等。
什么是热key?
短时间内被频繁访问的key,比如618期间被频繁访问的爆款商品的详情信息
由于Redis是单线程模型(Redis6.0之后的网络IO是多线程),大量请求集中到同一个键会导致对应Redis节点的CPU使用率飙升,响应时间变长,而其他节点相对空闲。
更严重的情况是,当热Key过期或者被误删,会导致严重的缓存击穿。
如何监控热key?
临时的方案可以使用 redis-cli --hotkeys
命令来监控 Redis 中的热 Key。
或者在访问缓存时,本地维护一个计数器,记录每个数据的访问次数,如果某个数据在一分钟内的访问次数超过阈值,就将其标记为热key。
怎么处理热key?
最有效的解决办法是增加本地缓存,将热key缓存到本地,这样请求就不用访问redis了
对于一些特别热的key,可以拆分为多个子key,然后随机分布到不同的 Redis 节点上。比如将 hot_product:12345
拆分成 hot_product:12345:1
、hot_product:12345:2
等多个副本,读取时随机选择其中一个。
怎么处理大key呢?
大key是指键值对占用内存很大的数据,常见的大key有包含大量元素的List、Set、Hash类型,存储大文件的String类型,以及包含复杂嵌套对象的JSON字符串作为String存储。
大key会导致Redis空间不足,也会导致主从同步复制延迟,甚至引发网络拥塞。
可以通过 redis-cli --bigkeys
命令来监控 Redis 中的大 Key。
最根本的解决办法是拆分大key为多个小key存储,比如将一个包含大量用户信息的 Hash 拆分成多个小 Hash。
另外,对于 JSON 数据,可以进行 Gzip 压缩后再存储,虽然会增加一些 CPU 开销,但在内存敏感的场景在是值得的。
缓存预热怎么做?
缓存预热是指在系统启动或者特定时间点,提前将热点数据加载到缓存中,避免冷启动时大量请求直接打到数据库。
应用程序启动时就将热点数据加载到Redis,或者定时任务预热,比如每天凌晨全量加载所有热点数据到内存。
无底洞问题听说过吗?怎么解决?
无底洞问题的核心在于,随着缓存节点数量的增加,虽然总的存储容量和理论吞吐量都在增长,但是单个请求的响应时间反而变长了。
这个问题的根本原因是网络通信开销的增加。当节点数量从几十个增长到几千个时,客户端需要与更多的节点进行通信。
其次就是数据分布的碎片化。随着节点增多,数据分散得更加细碎,原本可以在一个节点获取的相关数据,现在可能分散在多个节点上。
解决方案:
- 第一,可以将请求按节点分组,同一节点的多个请求合并成一个批量请求,减少网络往返次数。
- 第二,可以使用一致性哈希算法来优化数据分布,相关的数据尽量放在一个节点,减少数据迁移和重分布的开销。