Jade Dungeon

Redis缓存策略

一致性级别

  • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么, 读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值, 也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别 (比如秒级别)后,数据能够达到一致状态
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内, 能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来, 是因为它是弱一致性中非常推崇的一种一致性模型, 也是业界在大型分布式系统的数据一致性上比较推崇的模型

三个经典的缓存模式

缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。 一般我们是如何使用缓存呢?有三种经典的缓存模式:

  • Cache-Aside Pattern
  • Read-Through/Write through
  • Write behind

旁路缓存模式(Cache-Aside Pattern)

它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。

  • 读的时候
    • 先读缓存,缓存命中的话,直接返回数据
    • 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
  • 更新的时候
    • 先更新数据库,然后再删除缓存。

旁路缓存模式在写入请求的时候,为什么是删除缓存而不是更新缓存呢?

  • 旁路缓存模式在并发写入请求的时候,删除缓存取代更新缓存不会出现这个脏数据问题。
  • 如果你写入的缓存值,是经过复杂计算才得到的话。更新缓存频率高的话,就浪费性能。
  • 在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了, 这也浪费了性能呢(实际上,写多的场景,用缓存也不是很划算了)。

读写穿透(Read-Through/Write-Through)

读写穿透模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互, 都是通过抽象缓存层完成的。

  • 从缓存读取数据
    • 读到直接返回
    • 如果读取不到的话,从数据库加载,写入缓存后,再返回响应。

原理与「旁路缓存」一样,只是多了缓存:

图例

  • 当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新:

图例

异步缓存写入(Write behind)

「异步缓存写入」跟「读写穿透」有相似的地方, 都是由Cache Provider来负责缓存和数据库的读写。 它两又有个很大的不同:

  • 「读写穿透」是同步更新缓存和数据的,
  • 「异步缓存写入」则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。 但是它适合频繁写的场景,MySQL的InnoDB Buffer Pool机制就使用到这种模式。

图例

实践经验

先数据库后缓存

在写入请求的时候,为什么是先操作数据库呢?为什么不先操作缓存呢? 假设有A、B两个请求,请求A做更新操作,请求B做查询读取操作。 缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。

缓存延时双删

有些小伙伴可能会说,不一定要先操作数据库呀,采用缓存延时双删策略就好啦? 什么是延时双删呢?

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?都是1秒? 这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存, 如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~ 删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

读取biglog异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实, 还可以通过数据库的binlog来异步淘汰key。

图例

以mysql为例 可以使用阿里的canal将binlog日志采集发送到MQ队列里面, 然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

防护策略

  • 缓存击穿(失效):指的是数据库有数据,缓存本应该也有数据, 但是缓存过期了,Redis 这层流量防护屏障被击穿了,请求直奔数据库。
  • 缓存穿透:指的是数据库本就没有这个数据,每次请求直奔数据库,缓存系统形同虚设。
  • 缓存雪崩:指的是大量的热点数据无法在 Redis 缓存中处理(大面积热点数据缓存失效 、Redis 宕机),流量全部打到数据库,导致数据库极大压力。

缓存击穿(失效)

高并发流量,访问的这个数据是热点数据,请求的数据在 DB 中存在, 但是 Redis 存的那一份已经过期,后端需要从 DB 从加载数据并写到 Redis。

解决方案

过期时间 + 随机值

对于热点数据,我们不设置过期时间,这样就可以把请求都放在缓存中处理, 充分把 Redis 高吞吐量性能利用起来。

或者过期时间再加一个随机值。

设计缓存的过期时间时,使用公式:过期时间 = baes 时间 + 随机时间。

即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间, 让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对 DB 造成过大压力

预热

预先把热门数据提前存入 Redis 中,并设热门数据的过期时间超大值。

使用锁

当发现缓存失效的时候,不是立即从数据库加载数据。

而是先获取分布式锁,获取锁成功才执行数据库查询和写数据到缓存的操作,获取锁失败 ,则说明当前有线程在执行数据库查询操作,当前线程睡眠一段时间在重试。

这样只让一个请求去数据库读取数据。

缓存穿透

缓存穿透:意味着有特殊请求在查询一个不存在的数据,即不数据存在 Redis 也不存在于数据库。

导致每次请求都会穿透到数据库,缓存成了摆设,对数据库产生很大压力从而影响正常服务。

解决方案

  • 缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值 (比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
  • 布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中, 当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存, 就不要去数据库查询了。

BloomFilter 要缓存全量的 key,这就要求全量的 key 数量不大, 10 亿 条数据以内最佳,因为 10 亿 条数据大概要占用 1.2GB 的内存。

说下布隆过滤器的原理吧

BloomFilter 的算法是,首先分配一块内存空间做 bit 数组, 数组的 bit 位初始值全部设为 0。

加入元素时,采用 k 个相互独立的 Hash 函数计算, 然后将元素 Hash 映射的 K 个位置全部设置为 1。

检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1, 则表明 key 存在,否则不存在。

哈希函数会出现碰撞,所以布隆过滤器会存在误判。

这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率, 因为它存的是 key 的 Hash 值,而非 key 的值。

所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。

对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法, 如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0。 布隆过滤器判断存在不一定真的存在。

缓存雪崩

缓存雪崩指的是大量的请求无法在 Redis 缓存系统中处理,请求全部打到数据库, 导致数据库压力激增,甚至宕机。

出现该原因主要有两种:

  • 大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
  • Redis 故障宕机,缓存系统异常。

大量数据同时过期引起雪崩

数据保存在缓存系统并设置了过期时间,但是由于在同时一刻,大量数据同时过期。

系统就把请求全部打到数据库获取数据,并发量大的话就会导致数据库压力激增。

缓存雪崩是发生在大量数据同时失效的场景, 而缓存击穿(失效)是在某个热点数据失效的场景,这是他们最大的区别。

解决方案

过期时间添加随机值

要避免给大量的数据设置一样的过期时间, 过期时间 = baes 时间 + 随机时间(较小的随机数,比如随机增加 1~5 分钟)。

这样一来,就不会导致同一时刻热点数据全部失效,同时过期时间差别也不会太大, 既保证了相近时间失效,又能满足业务需求。

接口限流

当访问的不是核心数据的时候,在查询的方法上加上接口限流保护。 比如设置 10000 req/s。

如果访问的是核心数据接口,缓存不存在允许从数据库中查询并设置到缓存中。

这样的话,只有部分请求会发送到数据库,减少了压力。

限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数, 避免过多的请求被发送到数据库。

Redis宕机引起雪崩

一个 Redis 实例能支撑 10 万的 QPS,而一个数据库实例只有 1000 QPS。

一旦 Redis 宕机,会导致大量请求打到数据库,从而发生缓存雪崩。

解决方案

对于缓存系统故障导致的缓存雪崩的解决方案有两种:

服务熔断和接口限流; 构建高可用缓存集群系统。

服务熔断和限流

在业务系统中,针对高并发的使用服务熔断来有损提供服务从而保证系统的可用性。

服务熔断就是当从缓存获取数据发现异常,则直接返回错误数据给前端,防止所有流量打到数据库导致宕机。

服务熔断和限流属于在发生了缓存雪崩,如何降低雪崩对数据库造成的影响的方案。

构建高可用的缓存集群

所以,缓存系统一定要构建一套 Redis 高可用集群,比如 《Redis 哨兵集群》或者 《Redis Cluster 集群》,如果 Redis 的主节点故障宕机了, 从节点还可以切换成为主节点,继续提供缓存服务, 避免了由于缓存实例宕机而导致的缓存雪崩问题。