【笔记】Cache Pattern

引言

缓存可以说是用的十分普遍了,比如浏览器缓存,服务器本地内存缓存,分布式缓存,CPU缓存,磁盘缓冲区等等。

网站根据80%的请求落在20%的数据上的原则,在DAO层顶部加上缓存,来避免请求直接打到数据库上,给数据库带来太大压力。使用缓存是网站优化的一种很常见的策略,思路就是把数据放到离计算最近的地方。如果数据访问的十分均匀,缓存就无法起到明显的作用。

在工程上,更新缓存有一些Best Practice,即最佳实践,今天来盘点一下常见的几种缓存更新的策略。

先说一种错误的做法,先删除缓存,然后再更新数据库,而后用新数据更新缓存。这种方法存在着明显的问题:

先看一个读写并发的case:

1
2
3
4
5
6
case 1
(读)
读取缓存未命中---------->查询数据库取到旧值------------------------------->将旧值写入缓存
(写)
删除缓存 --------------------------------------------> 更新数据库 ----> 将新值写入缓存

此种情况下,查询操作会用旧值覆盖掉写操作的新值,即使写操作更新完数据库后直接返回,结果也是一样的。这就导致了缓存和数据库数据不一致的问题,更糟的是,后续的查询操作全部取到旧值。

Cache Aside Pattern

这大概是我们日常用的最多的一种缓存更新的模式了,具体是这样的

  • 查询操作;命中直接返回。否则查询数据库后将值放入缓存后返回。

  • 修改操作:更新数据库,失效缓存,返回。

这种模式能够避免case1那种情况发生,在写操作更新期间,其他查询操作继续读取缓存里面的旧值,待写操作完成之后,失效掉旧的缓存,后续的第一个查询会到数据库中获取最新值放到缓存中。

但是如此就万无一失了吗?

再来看一个case

1
2
3
4
5
6
case 2 缓存失效或不存在的情况下读写并发
(读)
读取缓存未命中---------->查询数据库取到旧值------------------------------->将旧值写入缓存
(写)
更新数据库 ------> 失效缓存

以上的case还是会造成缓存中存在脏数据,但是仔细分析一下这种情况发生的条件:

  • 读写并发
  • 查询操作未命中
  • 查询操作查询旧值的时间要早于写操作,将旧值放入缓存要晚于写操作失效缓存
  • 实践中,写操作时间远大于读操作

综上可以推断,此case出现的概率极小。

为确保数据一致性,一般采用

  • 降低case2出现的概率,Facebook的玩法
  • 给缓存设置一个合理的过期时间,数据短暂延迟
  • 采用2PC和Paxos等一致性算法,有效率损失

Read/Write Through Pattern

这种模式是将缓存层抽象成服务,负责代理对底层数据的访问,调用方只和缓存层进行交互。应用认为存储是单一的。

Read/Write Through Pattern

Write Behind Caching Pattern

Write Behind 又叫 Write Back,借鉴了Linux系统中Page Cache的思想。在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。

流程图:

Write Behind

有两个明显的优点:

  • 直接操作内存,大大提高I/O性能
  • 因为异步,还可以合并对同一个数据的多次操作(比如合并多次写操作),所以性能的提高是相当可观的。

但是同样也带来了数据丢失的问题,在极端情况下,比如宕机,会造成数据丢失。这种方案也增大了实现逻辑的复杂度,因为需要跟踪哪些数据被更新了,需要刷入持久层等等。

软件设计中,我们永远也不能找到完美的方案,就像算法时间空间复杂度的取舍,就像分布式中对于CP和AP的权衡,我们永远都在Trade-off。


2019.4.29 14:37