缓存是提高网络应用性能的最常见方案之一,这篇文章简单而全面的介绍了缓存的基本概念和实现方式。原文:Caching — System Design Concept[1]
本文将介绍系统设计中的一个重要的基本概念——缓存。你是否有过这样的经历:第一次打开一个网站时,花了很长的时间,但再次打开同一个网站,打开速度就快了很多。你知道为什么会这样吗?让我们一起来看看!
什么是缓存?
缓存是将请求的结果存储在与原始存储位置或临时存储位置不同的位置的过程,这样我们就可以避免重复执行相同的操作。基本上,缓存是文件和数据的临时存储,从这个新位置访问数据会更快。
例子
- Web 浏览器缓存 HTML、CSS、JS 和图像,以便在再次请求时更快的访问网站。
- CDN 存储静态文件,有助于减少延迟。
- DNS 用于获取查询的 IP 地址,查询结果可以存储在缓存中,因此当我们多次请求 IP 地址时,不必再次执行 DNS 查询,从而可以更快的访问网页。
真实世界的模拟
让我们以一个图书管理员为例来理解隐藏在系统背后的基本思想。想象有一个拥有上千本书的图书馆,一位图书管理员坐在桌子后面,他的职责是帮你从图书馆的库存中拿到你想要的书。首先,让我们从一个没有缓存的图书管理员开始。
一天开始了,第一个顾客来了。他想要一本书(假设是书 A),图书管理员去储藏室取了书,回到办公桌,把书递给顾客。几天后,顾客把书还了回来,图书管理员把书放回原处,然后回到办公桌等待另一位顾客。现在,下一位顾客来了,他要了同样的书 A,图书管理员又要去同样的地方取书并把它交给顾客。在这个系统中,图书管理员必须在顾客每次到达时都去库房取书——即使这本书经常有人借阅。
现在,我们在图书管理员的桌子上放一个可以放 15 本书的书包(就像一个 15 本书的缓存)。在这个书包里,图书管理员可以保存顾客最近还回来的书。现在,第一个顾客来了,并要求借阅 A 书,图书管理员必须去储藏室把它取给客户。后来,顾客还了书,而图书管理员没有立即去储藏室还书,而是把书放在了她的包里。另一个客户来了,这次图书管理员检查了一下她书包里是否有那本书,她找到了!这一次,图书管理员不需要去仓库取书,顾客得到了更高效的服务。
使用 NodeJS 实现一个简单的缓存
让我们实现一个简单的缓存系统:
首先,我们创建一个简单的服务器和一个数据库,通过数据库获取 HTML 页面,并使用服务器在本地托管页面。我们将创建两个访问端点,一个使用缓存,另一个不使用。
Caching.js
使用 NodeJS 的简单缓存
如果我们在服务器上的浏览器中查看没有缓存的访问端点,它将花费 3 秒加载(因为我们使用 setTimeout 在 3 秒内加载没有缓存的页面)。如果再次刷新页面,将再次花费 3 秒加载页面,因为每次刷新页面时,都会转到数据库获取。
在使用缓存的访问端点中,我们第一次访问时,需要 3 秒来加载页面,因为缓存是空的,必须到数据库获取数据。但是,当我们刷新页面时,它会立即加载。当我们第一次加载页面时,系统缓存了结果以备以后的请求。
回收机制(Eviction policy)
当缓存用完时,需要删除旧缓存项,从而可以缓存新内容。事实上,删除最近最少使用的对象是最流行的方法之一,这个解决方案可以优化缓存中命中请求资源的概率。
- 随机替换(RR,Random Replacement):正如其字面意思,我们可以随机删除一个条目。
- 最少使用次数(LFU,Least frequently used):记录一个条目被请求的频率,并删除最不频繁使用的条目。
- 最近最少使用(LRU,Least Recently Used):在 LRU 中,删除最近使用次数最少的条目。
- 先入先出(FIFO,First In First Out):FIFO 算法保存对象加载到缓存中的顺序。如果缓存没有命中,从头部取出一个或多个对象,并将一个新的缓存对象插入到队尾。如果缓存命中,保持缓存不变。
不同的缓存方法
应用服务器缓存(Application server cache)
我们可以直接在应用层缓存数据。每次向服务发出请求时,如果存在缓存的本地数据,可以快速返回。如果不在缓存中,将从数据库查询数据。
全局缓存(Global caches)
在全局缓存中,所有节点使用相同的单一缓存空间,每个应用节点以与本地节点相同的方式查询缓存。
分布式缓存(Distributed cache)
通常使用一致性哈希算法分割缓存数据,每个节点都拥有部分缓存数据。如果请求节点正在搜索某一段数据,那么可以很容易的使用哈希函数从分布式缓存中定位信息,以确定数据是否可用。
内容分发网络(CDN,Content Distribution Network)
内容分发网络
如果我们正在开发的框架还没有大到足以拥有自己的 CDN,而我们的页面又需要大量静态媒体,那么租用 CDN 是最好的选择。使用像 apache 这样的轻量级 HTTP 服务器,可以为不同的子域(如“blog.enjoyalgorithms.com”)提供静态媒体服务,并将 DNS 从我们的服务器切到 CDN 层。
客户端缓存(Client-Side Caches)
客户端缓存直接在浏览器或其他客户端(例如中间网络缓存)中缓存以前请求的文件数据。
ISP 层缓存(ISP layer cache)
ISP 缓存的工作方式与浏览器缓存基本相同。一旦你访问了一个网站,你的 ISP 可能会缓存这些页面,这样当你下次访问它们时,加载速度会更快。这样做的主要问题是,与浏览器缓存不同,你不能删除这些临时文件,必须等待 ISP 的缓存过期才能请求文件的新副本。
缓存失效(Cache Invalidation)
如果数据库中的数据被更改,它在缓存中应该是无效的,否则可能会触发不一致的应用程序操作。当前主要有三种缓存系统设计方案:
- Write through cache:通过缓存执行写操作,只有当写 DB 和缓存都成功时,写操作才被验证为成功。在缓存和存储之间,将拥有完全的数据一致性。在崩溃、电源故障或其他系统干扰的情况下,任何东西都不会丢失。然而,在这种情况下,因为要写入两个不同的系统,写入延迟会更高。
- Write around cache:写操作绕过缓存,直接写入 DB。在缓存读取过程中,由于数据不在缓存中,cache miss 会增加,缓存设备需要从数据库中读取信息。因此,在快速写入和重新读取数据的应用程序中,这可能会导致更高的读延迟。
- Write back cache:写操作直接发送给缓存层,一旦缓存的写操作完成,就认为写操作成功。然后缓存异步的将写操作同步到 DB。对于写密集型应用,这可以降低写延迟以及提高写吞吐量。但是,因为缓存是写入数据的唯一副本,如果缓存层被关闭,有可能丢失数据。通过在缓存中部署多个可以识别写操作的副本,我们可以尽量减少数据丢失的风险。
缓存的好处
缓存的好处
- 提高应用程序性能—缓存可以用来提高系统性能和降低 API 延迟。
- 降低数据库成本——缓存会占用缓存服务器的额外流量,从而减少数据库流量,最终降低数据库成本。
- 减轻后端负载——将相同的请求流量从主服务器卸载到缓存服务器将减轻后端负载。
- 增加读吞吐量(IOPS)——缓存服务器对缓存数据的响应比主服务器快得多,这增加了读吞吐量。
References:[1] https://medium.com/enjoy-algorithm/caching-system-design-concept-500134cff300