[TOC]
参考
1. 概要
缓存服务和数据服务(如数据库)是独立的系统,更新的时候,无法做到原子性地操作两个服务,即有两步操作来更新缓存服务和数据服务的数据。因此在并发读写以及第二步操作异常时,会出现各种问题。
此处说的第二步操作,要么是操作缓存服务,要么是操作数据服务,根据具体的实现方式而定。
2. 缓存使用的误区
服务与服务之间不要通过缓存传递数据
如果缓存挂掉,可能导致雪崩,此时要做高可用缓存,或者水平切分
调用方不宜再单独使用缓存存储服务底层的数据,容易出现数据不一致,以及反向依赖
不同服务,缓存实例要做垂直拆分
3. 常见更新模式
3.1. cache aside
同时更新缓存和数据库
这是最常用的pattern了。其具体逻辑如下:
- 数据查询:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 数据更新:先把数据存到数据库中,成功后,再让缓存失效。
此处,更新的时候是先数据库,后缓存,也有另一种思路,先缓存,后数据库。两种思路详见后文分析。
3.1.1. 先数据库后缓存
这是最常用最常用的pattern。
分析异常情况如下:
-
:
- 一个是查询请求,一个是更新请求的并发。更新请求在写完数据库之后,让缓存失效之前,查询请求到来,此时,缓存依然有效,所以,并发的查询请求拿的是没有更新的数据,但是,随后的更新请求会马上让缓存的失效了,在这之后的查询请求再把数据从数据库中取出来,放入到缓存。
- 同样是一个查询请求,一个更新请求的并发。查询请求 cache miss,读了数据库,在将数据放到缓存之前,更新请求到来,更新了数据库内容且让缓存失效,此时查询请求再将之前读的老数据放入到缓存。这样也会导致缓存和数据不一致。 为了避免这种情况,可以采取延迟失效,同时读请求回填 cache 的时候,如果遇到 key 存在,则不更新,只能等待超时失效之后,读请求回填的 cache 才可以更新;也可以考虑延迟双删,缓存失效后,等待1s 再失效一次缓存。
- :如果是更新请求在写完数据库,让缓存失效之前,更新请求发起方异常(如宕机),此时数据库已经更新了,但是缓存一直没有更新。导致查询请求从缓存获取的数据一直都是老数据。为了规避第二步操作异常,有两种方法:1. 考虑在缓存上加上失效时间,但是这和周期性的更新缓存类似,还是会对系统性能还是有较大的影响;2. 使用可靠的消息队列记录(如 binlog)更改,然后消费消息进行缓存失效
这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。
3.1.2. 先缓存后数据库
分析如下:
:一个是查询请求,一个是更新请求的并发。在更新请求让缓存失效之后,写数据库之前,查询操作到来,此时,会从数据库读到老数据进而写回到缓存,等更新请求写完数据库,
:如果是更新请求将缓存失效后,在写数据库前,更新操作发起方异常(如宕机),此时仅仅是 cache miss,
针对第二种方案,我们可以采取以下两种方案规避并发读写的问题:
- 在使缓存失效的时候,不是立即失效,而是采取延迟失效(比如说设置缓存失效时间为1s),保证在数据库更新之前,读请求依旧能够命中cache,进而不会出现读请求更新 cache 的情况。这个方案应该是最佳方案。
- 延迟双删。第一次缓存失效且数据库操作完成之后,延迟再失效一次缓存。
3.1.3. 更新缓存还是失效缓存
为什么不是更新缓存,而是失效缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是
3.1.3. 结论
个人觉得,先缓存后数据库,配合延迟失效的方案最好。欢迎读者讨论。
3.2. Read/Write Through Pattern
先更新缓存,缓存负责同步更新数据库。
在上面的 Cache Aside 更新模式中,应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。而在Read/Write Through 更新模式中,应用程序只需要维护缓存,数据库的维护工作由缓存代理了。
3.2.1. Read Through
Read Through 模式就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载。
3.2.2. Write Through
Write Through 模式和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库(这是一个同步操作)。
3.3. Write Behind Caching Pattern
先更新缓存,缓存定时异步更新数据库。
Write Behind Caching 更新模式就是在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是直接操作内存速度快。因为异步,Write Behind Caching 更新模式还可以合并对同一个数据的多次操作到数据库,所以性能的提高是相当可观的。
但其带来的问题是,数据不是强一致性的,而且可能会丢失。另外,Write Behind Caching 更新模式实现逻辑比较复杂,因为它需要确认有哪些数据是被更新了的,哪些数据需要刷到持久层上。只有在缓存需要失效的时候,才会把它真正持久起来。
3.4. 总结
三种缓存模式的优缺点:
- Cache Aside 更新模式实现起来比较简单,但是需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。
- Read/Write Through 更新模式只需要维护一个数据存储(缓存),但是实现起来要复杂一些。
- Write Behind Caching 更新模式和Read/Write Through 更新模式类似,区别是Write Behind Caching 更新模式的数据持久化操作是异步的,但是Read/Write Through 更新模式的数据持久化操作是同步的。优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。
缓存是通过牺牲强一致性来提高性能的。所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短太长都不好,太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。