参考
Redis - 常见缓存问题 - 知乎
Redis的缓存更新策略 - Sherlock先生 - 博客园
三种缓存策略:Cache Aside 策略、Read/Write Through 策略、Write Back 策略-CSDN博客
1.缓存问题
1.1.缓存穿透
大量请求未命中缓存,直接访问数据库。
解决办法:
1)对请求进行校验,不合理的直接返回
2)将查询不到的数据也存入缓存,但是过期时间设置短一点
3)设置布隆过滤器
1.1.1.布隆过滤器
数据结构
位数组(Bit Array):初始全为0的二进制位序列。
哈希函数集合:多个独立哈希函数(如 hash1, hash2, hash3)。
操作流程
添加元素:
对元素应用所有哈希函数,得到多个索引位置。
将位数组中对应索引位置设为1。
查询元素:
对元素应用所有哈希函数,得到多个索引位置。
若任一位置为0:元素一定不存在。
若所有位置为1:元素可能存在(存在误判概率)。
操作命令
SETBIT key offset value
GETBIT key offset
1.2.缓存雪崩
大量缓存同时过期,导致请求直击数据库。
解决办法:
1)给所有的缓存设置不一样的过期时间
2)构建多级缓存。如:本地cache、redis、数据库
1.3.缓存击穿
某个热点缓存过期或者被移出,导致大量请求直击数据库。
解决办法:
1)对热点数据加分布式锁
2)后台异步续期
3)多缓存策,使用备份缓存,当主缓存失效,则查询备份缓存,若备份缓存中存在,再更新到主缓存。
1.4.缓存污染
大量不经常被访问的数据把缓存占满了。
1.4.1.淘汰策略
noeviction
该策略是Redis的默认策略。在这种策略下,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。
allkeys-lru
从所有键中使用LRU(Least Recently Used,最近最少使用)算法淘汰键。
适用场景:缓存场景中,保留频繁访问的键,逐出很少被访问的键。
volatile-lru
从设置了过期时间的键中使用LRU算法进行淘汰。
适用场景:缓存一些有过期时间的数据,并根据访问频率进行内存管理。
allkeys-random
从所有键中随机选择并删除某个键。
适用场景:缓存数据访问频率没有明显差异的情况。
volatile-random
从设置了过期时间的键中随机选择并删除某个键。
适用场景:缓存带有过期时间的数据,且删除哪个数据不重要的场景。
volatile-ttl
在设置了过期时间的键中,根据过期时间的先后进行删除,越早过期的越先被删除。
适用场景:希望优先清理即将过期的数据的场景。
allkeys-lfu(Redis 4.0+)
从所有键中使用LFU(Least Frequently Used,最不常用)算法淘汰键。
适用场景:需要根据使用频率进行淘汰的场景,适用于访问频率有明显差异的数据集。
volatile-lfu(Redis 4.0+)
从设置了过期时间的键中使用LFU算法进行淘汰。
适用场景:与volatile-lru类似,但更关注使用频率。
1.4.2.过期键处理方式
惰性处理:当访问到了这个key再去删除
定时处理:定时遍历所有的key,找到过期的key并删除。
定时依次遍历所有的 DB,默认每 100ms 执行一次。
从 DB 的过期列表中随机取20个 Key ,判断是否过期,如果过期,则清理。
如果有5个以上的 Key 过期,则重复步骤2,否则继续处理下一个 DB 。
在清理过程中,如果达到 CPU 的 25% 时间,退出清理过程。
2.内存和数据库一致性问题
2.1.延迟双删-读多写少
1)删除缓存
2)更新数据库
3)过一段时间后再一次删除缓存
2.2.异步写回-写多读少
讲多次写入归并到缓存队列中,定时的将缓存中的数据计算之后写入数据库。
2.3.写操作时,为什么是删除缓存,而不是更新缓存?
1多线程并发修改的问题,例:
线程A更新数据库(值V1→V2)。
线程B更新数据库(值V2→V3)。
线程B更新缓存(值V3)。
线程A更新缓存(值V2)。
2 更新缓存会面临多次无效写操作(多次对该数据进行写时,缓存中的值会多次update,实际有意义的值只有最后一次写时更新的值),而删除逻辑就更加简单。
2.4.先改数据库还是先删缓存?
2.4.1.如果先删缓存
失败,则直接抛出异常,不存在不一致问题
成功,但是数据库修改失败,则还需更新缓存,可能导致不一致。
成功,数据库也成功,仍然存在不一致问题,例:
A线程删除缓存
B线程查询发现内存不存在,则查询数据库,并将数据库的值存入缓存
A线程更新数据库
在先删缓存,再改数据库的情况下,即使都成功了,仍旧可能存在不一致问题,如何解决?
延迟双删
2.4.2.如果先改数据库
失败,则直接抛出异常,不存在不一致问题
成功,但是缓存删除失败,存在不一致问题
成功,缓存删除也成功,不存在不一致问题
数据库修改成功,但是缓存删除失败,如何处理?
失败重试:失败后,通过消息队列重新删除。
异步更新:监听日志,监听到修改操作,则执行删除任务,如果删除失败,再发送消息队列。
2.5.缓存更新策略
2.5.1.Cache Aside(旁路缓存)-读多写少
应用程序同时和数据库/缓存交互。
读:
如果命中缓存,则直接返回。
如果没命中,则查询数据库,回写缓存。
写:
先更新数据库,再删除缓存。
2.5.2.Read/Write Through(读写穿透)
应用程序只和缓存交互。
读:
如果命中缓存,则直接返回。
写:
如果缓存中存在,则直接更新缓存,再通过缓存组件,同步更新到数据库。
如果缓存没命中,则直接更新数据库,然后返回。
2.5.3.Write Back(写回)-写多读少
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
3.Redis发布订阅模式
3.1.基于channel的发布订阅
订阅者可以订阅多个channel,一个channel也可以有多个订阅者。
发布者向channel发布消息后,channel的所有订阅者都能收到消息。
订阅者只能收到订阅之后的消息,订阅前发布的消息接收不到。
发布:publish
订阅:subscribe
实现原理:
字典+链表
每个channel作为一个字典项。字典项后面跟着一个链表,链表上的每个节点就是一个订阅者。
3.2.基于pattern的发布订阅
通过通配符订阅,多个channel
发布:publish
订阅:psubscribe
Subscribe:订阅单个channel
Psubscribe:通过通配符,订阅多个channel
实现原理:
基于链表实现,每个订阅者都是链表上的一个节点,节点描述了订阅的pattern。