Skip to main content
  1. posts/

缓存一致性问题思考

·2066 words·5 mins

概述

缓存是一件很复杂的事情。我认为比较核心的知识应该是,

  1. 缓存一致性保证
  2. 缓存数据结构设计和缓存内存管理
  3. 缓存中间件的建设(包含通信设计,故障处理,高可用保障等一系列知识) 本文主要从使用的角度去剖析缓存中的一些核心的内容。 在使用上,我把缓存比较核心的东西总结为, 一个业务难题,缓存使用场景 一个核心问题,并发和异常的情况下,如何尽可能的保证数据的一致性。 两个核心操作,查询,更新;

业务难题

之前我们聊过一轮缓存相关的内容,我们之前一直纠结于要不要使用缓存,以及什么情况下该使用缓存。 这个业务难题,你很难去笼统的说,什么情况下该用缓存,什么情况下不该用缓存? 我觉得我们只能根据缓存带来的优点和可能出现的问题然后在结合我们的业务场景去考虑,我们究竟要不要使用缓存。

缓存优缺点

优点:

  1. 快,提高响应速度和系统吞吐量
  2. 减轻数据库压力

缺点:

  1. 数据一致性问题
  2. 增加系统的复杂性和维护成本

建议使用缓存的场景,

  1. 读多写少
  2. 高频访问且耗时服务

不管是哪种场景,一旦我们决定在业务中使用缓存,有一个问题是不可避免的,就是如何保证数据的一致性。 我甚至有一个偏执的想法,就是,如果你能cover住缓存的可能带来的问题。那其实只要能带来性能上的提升,我们可以大胆的使用缓存。

缓存更新探讨

Cache Aside

### 存在问题 脏数据 : 读请求先读数据库,然后写请求更新数据库,然后失效缓存,最后读请求在更新缓存(出现概率很低,但是如果是多个节点,某个节点很慢,确实会出现这个场景,但是,一般来讲,不同节点请求的数据应该是不同的)【加锁;lease;延时双删】 缓存击穿 : 写请求失效缓存后,大量读请求访问数据库。【加锁,获取不到锁的请求尝试等一下在访问数据库;facebook采用lease的方式处理】 异常情况:写数据库成功,删缓存失败;【异步确保;分布式事务保证】
image.png
缓存脏数据图解

带着上面的问题,我们先来看看facebook的lease设计 lease设计主要解决两个问题:

  1. 过期设置(Cache Aside的脏数据问题)
  2. 大量频繁的读写请求,导致读总是查询数据库。

但是它有自己的客户端和服务器。为了方便理解,我这里沿用了上诉Cache Aside图。 这里先说明一下lease生成规则,

  1. 当缓存中的key失效时,生成一个lease; 然后每10s更新lease并清除返回标识。
  2. 对于查询请求,缓存未命中时,将lease返回给客户端,然后清除该key的lease。

于是针对每个过期key,都会对应一个lease:key-value的缓存。

有兴趣的话,可以看看这篇文档 https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf

解决缓存击穿&脏数据

1.数据库查询加锁

当然,还有一些保底手段去处理缓存不命中的问题。 比如,定时更新缓存数据。

解决缓存雪崩

缓存雪崩与缓存击穿的最大的区别,就是 击穿是一个key或者少量key失效时,大量并发请求; 而缓存雪崩则是大量key同时失效。 引发这个问题的根本原因是,

  1. key的设置时间相同;【失效时间设置加一个随机值】
  2. 缓存不可用;【缓存高可用建设,少量机器宕机;大量机器宕机之类的故障处理】

解决缓存穿透

根本原因:请求缓存中不存在的值。然后就请求就必须去数据库。 处理方案:

  1. 接口校验;
  2. 缓存null值或无效值。
  3. 增加布隆过滤器。【就是去缓存拿数据之前,先去布隆过滤器里查询数据是不是存在;不存在就直接返回了】

其他方式

当然,这个Cache Aside还会有很多变种误导大家。有些说法让我一度也觉得没啥问题; 比如。

  1. 先删缓存,再更新数据库【并发查询和更新,可能出现脏数据;缓存击穿;异常】【也有解决方案:延时双删】
  2. 更新数据库,再更新缓存【并发更新脏数据;异常】
  3. 更新缓存,再更新数据库【并发更新脏数据;异常】

这些也不一定是错的。我们可以辩证的去看待,他们都在努力的去解决一个问题,就是数据一致性的问题。 https://juejin.cn/post/6844904137654534158#heading-12这个博客分析的挺好的。 ​

Read/Write Through

存在问题

并发:更新请求处理时,读请求是否要被阻塞。 阻塞的话,更新数据库时,大量读请求阻塞。跟直接访问数据库的区别在哪呢。 不阻塞的话,获取的就不是最新的数据。如果更新数据库失败就会出现幻读。 或者引入版本管理。那开发的代价就很高了。 异常:更新数据库失败,出现幻读。 所以要花很大的代价去维护缓存和数据库的一致性,而且数据库表的数据不尽相同,这一点处理起来应该也很困难。

Write Behind Caching

存在问题

并发:好像没啥问题 异常:更新数据库失败了,重试保证最终一致性。好像也没啥问题。 那这个异步处理的时机应该怎么维护才算合适呢?数据一设置马上就异步线程去同步数据库。 但是还有一个问题,可能会出现刚更新完缓存,服务器宕机了。然后数据库没更新,就会导致数据丢失。这个问题好像没办法解。 ​

总结

  1. 所以,其实综合来看,Cache Aside的方式是代价最小的。这可能也是目前被广泛使用的原因吧。
  2. 如果对缓存带来的问题有相应的解决措施的话,其实,能提高性能的话,不必太过于纠结要不要使用缓存。

参考

https://coolshell.cn/articles/17416.html (缓存更新套路) https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf (facebook缓存建设) https://juejin.cn/post/6844904137654534158#heading-12 (缓存博客) https://draveness.me/papers-segcache/ (如何提高缓存利用率)