前言

一般在项目中,最消耗性能的地方就是后端服务的数据库了。而数据库的读写频率常常都是不均匀分布的,大多情况是读多写少,并且读操作(select)还会有一些复杂的判断条件,比如 like、group、join 等等,所有会出现很多的慢查询,因此数据库很容易在读操作的环节遇到瓶颈。

缓存的引入并不是只有好处,同时会带入许多问题,例如:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级。

本次解析缓存更新的几种策略。

缓存更新策略

缓存更新大致有以下三种策略:

  • Cache Aside
  • Read/Write Through
  • Write Behind

Cache Aside

Cache Aside分为读写两部分:

  • 读数据

    1. 从缓存中读数据,缓存中存在则返回;
    2. 缓存中不存在,则查询数据库;
    3. 将从数据库中查询出的数据更新到缓存中。
  • 写数据

    1. 数据先更新到数据库中;
    2. 使缓存中的数据失效;

为什么是使缓存失效,而不是更新缓存?

因为当存在并发写的情况时,会并发更新数据可能会导致缓存中脏数据的出现;同时并发会导致缓存更新频繁。

但是即便是缓存删除也会产生脏数据的可能:

  1. A读取缓存发现缓存中不存在,查询数据库;
  2. 此时B更新数据,并使缓存失效;
  3. 此时A接收到了数据库获取了B更新之前的数据;
  4. A将数据更新到缓存中,此时出现了脏数据.

这种情况网上普遍的解释是:

这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。

在对一致性要求比较高的场合,通常需要引入更加复杂的机制确保更新缓存的正确性。

Read/Write Through

在上面的Cache Aside中应用程序需要维护两个数据存储,即数据库和缓存;Read/Write Through的思路便是将更新数据库的操作由缓存去代理了。

同样的分为读和写两种情况。

Read Through

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

Write Through

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

Write Behind

Write Behind 又叫 Write Back。

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

但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性能是有冲突的。软件设计从来都是取舍Trade-Off。

另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

参考

缓存的更新套路