缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回,是一种常见的空间换时间的性能优化手段。
缓存不仅是指内存,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。
计算机系统中常会借助一个叫做 TLB(Translation Lookaside Buffer)的组件来缓存最近转换过的虚拟地址和物理地址的映射,从而加快地址转换的速度。
TLB 就是一种缓存组件,缓存复杂运算的结果。
视频平台上的短视频实际上是使用内置的网络播放器来完成的。网络播放器接收的是数据流,将数据下载下来之后经过分离音视频流,解码等流程后输出到外设设备上播放。
播放器中通常会设计一些缓存的组件,在未打开视频时缓存一部分视频数据,比如打开视频时,服务端可能一次会返回三个视频信息,我们在播放第一个视频的时候,播放器已经帮我们缓存了第二、三个视频的部分数据,这样在看第二个视频的时候就可以给用户“秒开”的感觉。
任何能够加速读请求的组件和设计方案都是缓存思想的体现。而这种加速通常是通过两种方式来实现:
日常开发中,常见的缓存主要就是 静态缓存、分布式缓存和 热点本地缓存这三种。
静态缓存处在负载均衡层,分布式缓存处在应用层和数据库层之间,本地缓存处在应用层。我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差。
缓存命中率是我们对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高。
静态缓存在 Web 1.0 时期是非常著名的,它一般通过生成 Velocity 模板或者静态 HTML 文件来实现静态缓存,在 Nginx 上部署静态缓存可以减少对于后台应用服务器的压力。
这种缓存仅能针对静态数据,面对动态数据无能为力。
Memcached、Redis 是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色。
热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。当遇到极端的热点数据查询的时候,可以在代码中使用一些本地缓存方案,如 HashMap,Guava Cache 或者是 Ehcache 等。
由于本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。
更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
当写入比较频繁时,缓存的数据会被频繁的清理,从而影响缓存命中率。解决方案有如下两种:
核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。
先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。
一般来说,可以选择两种“Write Miss”方式:
在此策略中一般选用后者,原因是无论采用哪种方式都需要同步到数据库,“No-write allocate”方式相比“Write Allocate”还减少了一次缓存的写入,能够提升写入的性能。
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
在使用本地缓存的时候可以考虑使用这种策略,比如本地缓存 Guava Cache 中的 Loading Cache 就有 Read Through 策略的影子。
Write Through 策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相比于写缓存,同步写数据库的延迟就要高很多了。
这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。
在“Write Miss”的情况下,采用的是“Write Allocate”的方式,即在写入后端存储的同时要写入缓存,这样在之后的写请求中都只需要更新缓存即可,而无需更新后端存储了。
操作系统层面的 Page Cache、日志的异步刷盘、消息队列中消息的异步写入磁时大多采用了这种策略,避免了直接写磁盘造成的随机写问题。故在向低速设备写入数据的时候,可以在内存里先暂存一段时间的数据,甚至做一些统计汇总,然后定时地刷新到低速设备上。
缺点是一旦缓存机器掉电,就会造成原本缓存中的脏块儿数据丢失。
Tip: 本文是极客时间高并发系统设计40问学习笔记。