0%

缓存系统常见问题总结

关于缓存的知识太多了,先整理一点,慢慢补充吧。

缓存同步策略

首先需要明确,除了把整个缓存和持久化存储都做串行化,或者操作一个的时候直接锁死另一个,不然必然会有产生数据不一致的可能。选择哪种缓存策略是根据自己的业务场景进行取舍。

经常见到的一种方式,是写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。

但是这种方式也有问题,假如两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

典型的缓存模式,一般有如下几种:

  • Cache Aside
  • Read/Write Through
  • Write Around
  • Write Behind

Cache Aside

经常用到的一种策略模式。这种模式主要流程如下:

  • 失效:应用程序先从 Cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

  • 命中:应用程序从 Cache 中取数据,取到后返回。

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

这种缓存策略会在如下情况出现脏数据:

  • 线程 A 查询一个数据,在缓存中没有找到,于是去数据库查,拿到数据之后还没放到缓存中。
  • 这时候线程 B 过来更新这一条数据,先更新数据库之后准备令缓存失效,由于缓存还没放进来所以不需要失效。
  • 线程 A 把数据放到了缓存中。但是这个数据其实已经是脏数据了,跟数据库中不同步。

这种情况发生概率要稍微低,因为需要在数据库的查询操作和缓存的更新操作之间,插入一个数据库的修改和缓存的修改操作,一般情况下,后者需要的时间是要远高于前者的。当然也不能忽视这种情况确实存在。

Read/Write Through

Read/Write Through 套路是把更新数据库(Repository)的操作由缓存自己代理了,对应用层来说是透明的,应用不再需要关心数据同步问题。

Read Through

Read Through 就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或 LRU 换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的。

Write Through

Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由 Cache 自己更新数据库(这是一个同步操作)。

Write-through_with_no-write-allocation

这种方式的风险也显而易见,如果追求落库之后再返回成功,效率必然降低很多,缓存的意义就不大了。如果追求落到缓存上就算成功,那问题又抛给了缓存的丢失风险上。

Write Around

这种策略下,数据直接写入数据库,只有读取的数据才能进入缓存。Write Around 可以与 Read Through 结合使用,并在数据只写一次、读取次数较少或从不读的情况下提供良好的性能。例如,实时日志或聊天室消息。同样,这个模式也可以与 Cache Aside 组合使用。

Write Behind

Write Behind 又叫 Write Back。就是 Linux 文件系统的 Page Cache 的算法。

Write Back 套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作飞快无比(因为直接操作内存嘛 ),因为异步,Write Back 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

性能很强悍,问题也很明显,数据不是强一致性的,而且可能会丢失。另外 Write Behind 实际上的逻辑还比较复杂,因为需要追踪定位哪些数据需要做持久化。

常见面试题

下面是几个老生常谈面经常见问题了,该背的还是得背啊…

缓存穿透

缓存穿透的意思:请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去。

可能引发的问题:请求全落在数据库上,数据库压力过大,直接崩了。

解决办法:

  • 缓存空值,之所以会发生穿透,就是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。所以我们把空值也给缓存了,下次请求就会走数据库了。要记得设定过期时间。
  • 布隆过滤器,定义不解释了,就是可以用来判断超大数据量中单个数据的存在性问题。但是存在误差,如果布隆过滤器判断不存在,那肯定不存在,但是如果判断存在,也有一定的可能误判。另外基础的布隆过滤器没办法删除元素,需要另外的逻辑辅助处理,或者依靠其他变种。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。

可能引发的问题:跟上面一样,数据库压力过大崩了。

一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值。

缓存击穿

对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,雪崩则是很多 key。

可能引发的问题:单个 key 过期的瞬间大量请求过来,导致数据库压力过大崩了。

解决办法:

  • 加锁,单个请求去更新缓存就行了,别的排队等着。
  • 双层的超时值,保存一个用于更新缓存的超时值,到达这个值之后去延长一下真是的超时时间。

热点 Key 问题

缓存中的某些 Key(可能对应用与某个促销商品)对应的 value 存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈。

解决办法:

  • 客户端缓存(多级缓存),将热点 key 对应 value 并缓存在客户端本地,并且设置一个失效时间。
  • 将单个热点分散为多个子 key,时期请求的时候 hash 到不同的机器上处理。

热点集中失效

不解释了,跟缓存雪崩一个样,就是说热点数据全都到期,然后全落到数据库上。

解决办法是过期时间加随机值,或者加互斥锁排队。

参考资料

缓存更新的套路

缓存穿透,缓存击穿,缓存雪崩解决方案分析