【每日八股】Redis篇(五):应用(上)
目录
- 缓存雪崩、击穿、穿透和解决办法?
- 缓存雪崩
- 概念
- 解决办法
- 缓存击穿
- 概念
- 解决办法
- 缓存穿透
- 概念
- 解决办法
- 布隆过滤器如何工作?
- 概念
- 核心组成
- 工作流程
- 关键特性
- 如何保证数据库和缓存的一致性?
- Cache-Aside 旁路缓存(通用方案)
- Double Delete 双删策略(高并发优化)
- Read/Write-Through 穿透读写
- Write-Behind 异步写(高性能)
- Binlog 同步(最终一致)
- 版本号控制(防并发覆盖)
- 如何保证删除缓存操作一定能成功?
- 业务一致性要求高怎么办?
- 先更新数据库后更新缓存
- 延迟双删
- 如何避免缓存失效?
- 如何实现延迟队列?
- 如何设计一个缓存策略,可以动态缓存热点数据呢?
- Redis 实现分布式锁?
- 补充:什么是分布式锁?为什么需要分布式锁?分布式锁有哪些应用场景?
- Redis 实现分布式锁
- 如何保证加锁和解锁过程的原子性?
- 使用 Redis 实现分布式锁的优缺点?
- 如何为分布式锁设置合理的超时时间?
缓存雪崩、击穿、穿透和解决办法?
缓存雪崩
概念
当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
解决办法
- 随机过期时间:为缓存设置基础过期时间 + 随机值,分散失效时间;
- 永不过期策略:包括物理不过期和逻辑过期。物理不过期指的是缓存不设置过期时间,通过异步线程定期更新;逻辑过期指的是缓存中设置过期时间字段,由业务逻辑判断是否需要异步更新。
- 熔断降级:使用 Hystrix 等工具对数据库访问限流,避免崩溃。
- 多级缓存:结合本地缓存(如 Caffeine)+ 分布式缓存(Redis),分层缓解压力。
缓存击穿
概念
某个热点 key 突然失效,高并发请求瞬间穿透到数据库。
解决办法
- 互斥锁:通过 Redis 的
SETNX
或分布式锁控制单一线程重建缓存,其它线程等待后重试。 - 逻辑永不过期:缓存不设置过期时间,通过后台异步线程定期更新。
- 热点数据预热:提前加载高频访问数据到缓存,并监控热点 Key,动态调整过期策略。
缓存穿透
概念
查询不存在的数据(如非法 ID),绕过缓存直接访问数据库。
解决办法
- 布隆过滤器(Bloom Filter):在缓存层前加过滤器,快速判断 Key 是否存在,拦截非法请求。缺点是存在误判率,需要定期重建过滤器。
- 空值缓存:对查询结果为 null 的 Key,缓存短时间空值。
- 参数校验:在业务层对请求参数做合法性检验。
- 限流封禁:对频繁访问无效 Key 的 IP 或用户实施限流或封禁。
布隆过滤器如何工作?
概念
布隆过滤器(Bloom Filter)是一种高效的概率型数据结构,用于快速判断一个元素是否可能存在于一个集合中。其核心特点是空间效率极高,但存在一定的误判率(可能误报存在,但绝不会漏报存在)。
核心组成
位数字(Bit Array):一个长度为m
的二进制数组,初始所有位都是0
。
多个哈希函数(Hash Functions):一组k
个独立的哈希函数,每个函数都能将输入元素映射到位数组的某个位置(0
到m-1
)。
工作流程
添加元素
如果要添加元素 x,首先依次通过 k 个哈希函数计算,得到 k 个哈希值。将位数组中这 k 个位置置为 1。
查询元素
对于要查询的元素 y,同样通过计算 k 个哈希函数计算,得到 k 个哈希值。检查位数组中这 k 个位置的值:
- 如果所有位置均为 1,返回可能存在。
- 如果任意位置为 0,返回一定不存在。
关键特性
- 误判率:可通过增大
m
或优化k
来提升。 - 不支持删除操作:替代方案是使用计数布隆过滤器。
如何保证数据库和缓存的一致性?
Cache-Aside 旁路缓存(通用方案)
- 读流程:先读缓存 → \rightarrow →未命中读 DB → \rightarrow →回填缓存;
- 写流程:先更新 DB → \rightarrow →再删除缓存。
- 特点:适合读多写少场景,存在短暂的不一致空窗;
- 不一致场景:并发写时可能读到旧数据。
Double Delete 双删策略(高并发优化)
- 写流程:先删缓存 → \rightarrow →更新DB → \rightarrow →延迟再删缓存(通过消息队列/延迟队列);
- 特点:通过二次删除解决并发期间写不一致;
- 注意:需要合理设置延迟时间(业务 RT + 缓冲)。
Read/Write-Through 穿透读写
Read/Write Through原理是把更新数据库的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
- 写流程(Write-Through):当有数据更新时,如果没有缓存命中,直接更新数据库。如果命中缓存,则先更新缓存,期间同步更新 DB;
- 读流程(Read-Through):在查询操作中更新缓存;
- 特点:数据强一致,但是写入性能较低;
- 适用场景:金融交易等一致性要求强的系统;
Write-Behind 异步写(高性能)
- 写流程:在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢;
- 特点:高性能但存在数据丢失风险;
- 适用场景:秒杀库存、点赞技术等可容忍的丢失场景。
Binlog 同步(最终一致)
通过数据库 binlog → \rightarrow → 解析日志 → \rightarrow → 更新缓存。
- 特点:保证最终一致,延迟约 100ms~1s。
- 优势:业务代码无入侵,适合多级缓存同步。
版本号控制(防并发覆盖)
- 数据版本化:每次更新携带版本号;
- 更新时校验版本,防止旧数据覆盖;
- 适用场景:分布式系统多节点更新;
如何保证删除缓存操作一定能成功?
重试机制
引入消息队列,删除缓存的操作由消费者来做,删除失败的话重新去消息队列拉取相应的操作,超过一定次数没有删除成功就向业务层报错。
订阅 binlog
订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。可以让删除服务模拟自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,主节点收到请求后,就会开始推送 binLog ,删除服务解析 binlog 字节流之后,转换为便于读取的结构化数据,再进行删除。
业务一致性要求高怎么办?
先更新数据库后更新缓存
可以先更新数据库再更新缓存,但是可能会有并发更新的缓存不一致的问题。解决办法是更新缓存前加一个分布式锁,保证同一时间只运行一个请求更新缓存,加锁后对于写入的性能就会带来影响;在更新完缓存时,给缓存加上较短的过期时间,出现缓存不一致的情况缓存的数据也会很快过期。
延迟双删
采用延迟双删,先删除缓存,然后更新数据库,等待一段时间再删除缓存。保证第一个操作再睡眠之后,第二个操作完成更新缓存操作。
如何避免缓存失效?
由后台线程频繁地检测缓存是否有效,检测到缓存失效了马上从数据库读取数据,并更新到缓存。
或者在业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。
在业务刚上线的时候,最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。
如何实现延迟队列?
延迟队列(Delayed Queue)是一种特殊的消息队列,消息在发送后不会立即被消费,而是延迟指定时间后才能被消费者处理。常用于需要定时触发的场景,如订单超时关闭、定时提醒、任务重试等。
可以使用 Redis 当中的 ZSet 来实现延迟队列,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
如何设计一个缓存策略,可以动态缓存热点数据呢?
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。用 zadd 方法和 zrange 方法来完成排序队列和获取前面商品。
Redis 实现分布式锁?
补充:什么是分布式锁?为什么需要分布式锁?分布式锁有哪些应用场景?
什么是分布式锁?
分布式锁是用于协调分布式系统中多个节点对共享资源进行互斥访问的同步机制。在单机环境中可通过本地锁控制并发,但在分布式环境下需跨节点协同,此时分布式锁成为关键技术。
必要性分析
- 资源竞争控制:当多个服务实例同时操作共享资源(如数据库记录、文件)时,需保证原子性操作。例如电商库存扣减场景,未加锁可能导致超卖。
- 数据一致性保障:防止并发写入导致数据错乱,如支付系统的余额变更操作。
- 幂等性支持:在消息队列消费等场景中,确保重复请求不会引发副作用。
- 任务调度防重:分布式定时任务需保证同一时刻仅一个节点执行,避免重复计算。
典型应用场景
- 库存管理:秒杀活动中防止超卖;
- 分布式任务调度:ElasticJob 等框架的任务触发控制;
- 配置中心:灰度发布时配置的原子更新;
- 分布式会话管理:集群环境下 Session 的互斥访问;
- 分布式事务锁:Saga 模式中的资源预留;
Redis 实现分布式锁
使用 SETNX 命令,只有插入的 key 不存在才插入,如果 SETNX 的 key 存在就插入失败,key 插入成功代表加锁成功,否则加锁失败;解锁的过程就是将 key 删除,保证执行操作的客户端就是加锁的客户端,加锁时候要设置 unique_value,解锁的时候,要先判断锁的 unique_value 是否为加锁客户端,是才将 lock_key 键删除。此外要给锁设置一个过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,可以指定 EX/PX 参数设置过期时间。
SET lock_key unique_value NX PX 10000
如何保证加锁和解锁过程的原子性?
使用Lua脚本,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
使用 Redis 实现分布式锁的优缺点?
- 优点:性能高效;实现方便;避免单点故障。
- 缺点:超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源;Redis 主从复制模式中的数据是异步复制的,导致分布式锁的不可靠性;如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
如何为分布式锁设置合理的超时时间?
可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。