Redis 缓存机制
Redis,一个轻量级的开源内存数据结构存储系统,以其卓越的性能和灵活性,成为实现高效缓存策略的不二选择。
1. 缓存三兄弟
1.1 缓存击穿
一个并发访问量比较大的 key 在某个时间过期,导致所有的请求直接打到 DB 上。
解决方案
- 加锁更新:加互斥锁更新,强一致,性能差。比如请求查询 A,发现没有命中缓存,则对 A 这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,释放锁。
- 逻辑过期:不设置过期时间,过期时间存放在缓存中。通过异步的方式不断地刷新过期时间,防止缓存击穿现象出现。
1.2 缓存穿透
查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透可能会使后端存储负载加大,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。
可能发生的原因:
- 自身业务代码问题
- 恶意攻击、爬虫导致空命中
解决方案
- 缓存空值/默认值:在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。
- 布隆过滤器:存储与缓存之间加一层布隆过滤器,判断数据是否存在,如数据不存在,则不访问存储
缓存空值存在问题:
- 缓存中存在更多的无效键值对,需要更多的内存空间。可以针对此类数据设置一个较短的过期时间,让其自动失效剔除
- 缓存层和存储层的数据会有一段时间窗口的不一致(空值过期前,存储层更新了该值),可能会对业务造成影响。可以利用消息队列或其他异步方式清理缓存中的空对象。
1.3 缓存雪崩
某一时刻,发生大规模的缓存失效情况,例如缓存服务宕机、大量 key 在同一时间过期,导致大量请求打在 DB 上,可能导致整个系统的崩溃,称为缓存雪崩。
解决方案:
- 提高缓存可用性
- 集群部署:通过集群中提升缓存的可用性,可以利用 Redis Cluster 或者第三方的集群方案等
- 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存失效的时间都不相同
- 过期时间:
- 均匀过期,随机过期时间,避免过期时间太过集中
- 热点数据永不过期
- 熔断降级:
- 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统
- 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求而直接返回一个提前准备好的fallback错误处理信息
2. 布隆过滤器
布隆过滤器,一个连续的数据结构,每个存储位置都是一个 bit,来标识数据是否存在
存储数据的时候,使用 K 个不同的哈希函数将这个变量映射为 bit 列表的 K 个点,把他们置为 1
判断缓存 key 是否存在,同样使用 K 个不同的哈希函数,映射到 bit 列表上,判断是不是 1
- 如果不全是 1,那么 key 不存在
- 如果全是 1,也只是表示 key 可能存在
缺点:
- 它在判断元素是否在集合中时,存在一定的错误几率,因为哈希算法有一定的碰撞概率
- 不支持删除元素
3. 缓存和数据库数据一致性
根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。
3.1 缓存更新策略
-
删除缓存而不是更新缓存
- 当一个线程对缓存的key进行写操作的时候,如果其它线程进来读数据库的时候,读到的就是脏数据,产生了数据不一致问题。相比较而言,删除缓存的速度比更新缓存的速度快很多,所用时间相对也少很多,读脏数据的概率也小很多
-
先更数据,后删缓存
- 更新数据,耗时可能在删除缓存的百倍以上。在缓存中不存在对应的 key,数据库又没有完成更新的时候,如果有线程进来读取数据,并写入到缓存,那么在更新成功之后,这个 key就是一个脏数据
- 毫无疑问,先删缓存,再更数据库,缓存中key不存在的时间的时间更长,有更大的概率会产生脏数据
3.2 缓存不一致处理
原因:
- 缓存 key 删除失败
- 并发导致写入了脏数据
解决方案:
1、消息队列保证 key 被有效删除
思路:可以引入消息队列,把要删除的 key 或者删除失败的 key 加入消息队列中,利用消息队列的重试机制,重新删除对应的 key
缺点:对业务代码有一定的侵入性
2、数据库订阅 + 消息队列保证
思路:可以用一个服务 (比如阿里的 canal) 去监听数据库的 binlog,获取需要操作的数据。然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。
3、延时双删防止脏数据
这种情况主要来自“先删缓存,再更数据”,并发情况下产生脏数据
解决方案是 延时双删,在第一次删除缓存之后,过一段时间,再次删除缓存
4、设置缓存过期时间兜底
设置缓存过期时间,即使发生不一致问题,总有过期的时候
4. 热点 key
4.1 热点 key 处理
在 Redis 集群部署中,热 key 可能会造成整体流量不均衡,个别节点出现 OPS 过大甚至超载的情况
在客户端、代理端和服务端监控,识别 热点 key
客户端:设置全局字典(key 和调用次数),每次调用 Redis 命令时,更新字典
Redis 服务端:使用 monitor 命令可以监控到 Redis 执行的所有命令,统计热点 key
处理方法:
- 热 key 打散到不同的服务器,降低压力
- 加入二级缓存,提前加载热 key 数据到内存中,如果 Redis 宕机,走内存查询
4.2 热点 key 重建
开发的时候一般使用 “缓存 + 过期时间” 的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求
但是有两个问题如果同时出现,可能就会出现比较大的问题:
- 当前key是一个热点key (例如一个热门的娱乐新闻),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
问题要点:
- 减少重建缓存次数
- 数据尽可能的一致
- 较少的潜在风险
解决方案:
- 互斥锁:这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永不过期:缓存不设置过期时间,也即物理不过期;为每个 value 设置一个 逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存
5. 缓存预热
缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法
- 直接写个缓存刷新页面或者接口,上线时手动操作
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时任务刷新缓存
参考二哥的面渣逆袭 & 《Redis设计与实现》,加油!