缓存更新策略
前言
一般在项目中,最消耗性能的地方就是后端服务的数据库了。而数据库的读写频率常常都是不均匀分布的,大多情况是读多写少,并且读操作(select)还会有一些复杂的判断条件,比如 like、group、join 等等,所有会出现很多的慢查询,因此数据库很容易在读操作的环节遇到瓶颈。
缓存的引入并不是只有好处,同时会带入许多问题,例如:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级。
本次解析缓存更新的几种策略。
缓存更新策略
缓存更新大致有以下三种策略:
- Cache Aside
- Read/Write Through
- Write Behind
Cache Aside
Cache Aside分为读写两部分:
读数据
- 从缓存中读数据,缓存中存在则返回;
- 缓存中不存在,则查询数据库;
- 将从数据库中查询出的数据更新到缓存中。
写数据
- 数据先更新到数据库中;
- 使缓存中的数据失效;
为什么是使缓存失效,而不是更新缓存?
因为当存在并发写的情况时,会并发更新数据可能会导致缓存中脏数据的出现;同时并发会导致缓存更新频繁。
但是即便是缓存删除也会产生脏数据的可能:
- A读取缓存发现缓存中不存在,查询数据库;
- 此时B更新数据,并使缓存失效;
- 此时A接收到了数据库获取了B更新之前的数据;
- 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。