缓存概述
在高并发的业务场景下,系统的设计重心有很大一部分落在性能上。性能的考虑因素简单来说,就是空间和时间:空间指系统的存储成本(字节数),包括持久化存储(数据库)、缓存(Cache)等,时间是指系统的吞吐能力(TPS、QPS、seconds/request等)。
所有的性能方案归根结底都是二者之间的博弈和权衡,最常见的就是牺牲空间换时间,通过增加缓存(Cache)的方式来提高吞吐能力。
从另一个角度说,缓存是系统设计中必不可少的一部分。
缓存的分层
缓存这个概念在技术栈的不同层级,不同粒度,可以层层深入展开剖析。例如,从网络来说,可以分层为:客户端缓存、网络缓存(CDN)、服务端缓存。而服务端缓存又可以再细化拆分为:网关缓存、应用缓存、数据库缓存等层。
应用缓存模式
本文主要讨论的是服务端侧的应用缓存,其他层级的缓存讨论不包含在本文内容。
缓存设计有一个前提,就是数据的非强一致性——缓存内容是无法做到实时同步的,毕竟持久化存储和缓存是两个独立系统,就像两个人之间的信息交流,无论如何,一个人脑中的信息是无法实时复制到另一个人的脑中,因为信息的流转需要一定的时间。
基于这个前提,下面对比下缓存设计的2个常用模式。
注意:以下的“Cache”就是指缓存。
Cache Aside 更新模式
- 失效:应用程序先从Cache中取数据,如果没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从Cache中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,让Cache失效。
请注意这里的更新把数据写到数据库后,仅仅是让Cache失效,而不是把数据直接写到Cache中。这个操作细节是为了防止两个并发的更新操作,导致Cache出现脏数据。Quora上有个相关的问答 www.quora.com/Why-does-Fa… ,可以参考阅读下。
Cache Aside模式会不会有并发问题呢?会。想想这种场景:一个线程A执行读操作,但是缓存未命中,就会读取数据库中的数据,还来不及写到Cache中,而此时来了一个线程B写操作,写完数据库后,让Cache失效。然后之前的读线程A又获取到CPU,将之前从数据库读到的数据,写到Cache中,这时,Cache中的数据就和数据库中的数据不一致了。
上述的场景理论上会出现,但是实际上出现的概率极低,因为数据库的写操作会比读操作慢很多,而且还要锁数据,读操作必须在在操作前进入数据库操作,又要比写操作更晚更新Cache,要出现这样的场景概率是极低的。
如果真的担心这种低概率的事件发生,可以为Cache的数据增加有效期约束,在一定时间后,自动将缓存清除,使其在读操作时,重新从数据库中读取最新的数据。
Cache Aside 更新模式,在读多写少的场景下,是一种比较合理的设计。
Write Behind Caching 更新模式
Write Behind 又叫 Write Back,它的设计有以下特点:
- 失效:应用程序先从Cache中取数据,如果没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从Cache中取数据,取到后返回。
- 更新:只同步更新Cache中的数据,不更新数据库。异步批量更新Cache中的变更数据到数据库中。
在Write Behind 更新模式中,Cache会异步批量更新数据库。这样会让数据的I/O操作飞快无比(因为同步更新只操作Cache)。并且因为是异步更新数据库,可以把同一数据的多次操作做合并,这样能够极大地提高写操作的性能。
但是这种模式有一个问题,数据不是强一致的,Cache中的数据和数据库的数据可能在一定时间内是不一致的,并且在一定概率上会丢失数据变更(比如Cache崩溃,数据来不及更新到数据库,这个和Linux系统非正常关机会导致数据丢失,是很类似的)。
但是要注意的是,没有绝对的银弹,任何设计方案都是权衡的结果,有时候,为了提高操作性能,放弃数据强一致性,是可接受的。比如博客的点赞、收藏计数的场景,即使丢失了一些少部分计数,也是可以接受的,那使用Write Behind 更新模式是完全没有问题的。
Write Behind 更新模式,在写较多并且允许数据部分丢失的场景下,是一种比较合理的设计。
小结
本文简单地介绍了缓存设计中常用的两种更新模式:Cache Aside 和 Write Behind,在实际的系统设计中注意区分业务场景,选择合理的更新模式。