通过前面的改造,你的电商系统在完成了对数据库的主从分离和分库分表之后,已经可以支撑十几万的DAU了,整体系统的架构也变成了下面这样:
从整体看,数据库分了主库和从库,数据也被切分到多个数据节点上。但随着并发的增加,存储数据量的增多,数据库的磁盘IO逐渐成了系统的瓶颈,我们需要一种访问更快的组件来降低请求响应时间,提升整体系统的性能。这时我们会使用缓存。什么是缓存?如何将它的优势最大化?
本节课是缓存篇的总纲,将从缓存定义、缓存分类和缓存优势劣势三个方面带你掌握缓存的设计思想和理念
什么是缓存
缓存,是一种存储结构的组件,它的作用是让对数据库的请求更快的返回。
我们经常会把缓存放在内存中来存储,所以有人就把内存和缓存画上了等号,这时外行的。作为业内人士,要知道在某些场景下我们可能还会使用SSD作为冷数据的缓存。比如360开源的Pika就是使用SSD存储数据解决redis的容量瓶颈的。
实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。下面是常见硬件组件的延时情况。
从这些数据中,可以看到,做一次内存寻址大概需要100ns,而做一次磁盘的查找则需要10ms。可见我们使用内存作为缓存的存储介质相比于以磁盘为主要存储介质的数据库来说,性能上会提高多个数量级,同时也能够支撑更高的并发量。所以内存是最常见的缓存数据的介质。
缓存作为一种常见的空间换时间的性能优化手段,在很多地方都有应用,例子:
1.缓存案例
比如抖音,平台上的短视频实际上是使用内置的网络播放器来完成的。网络播放器接受的是数据流,将数据下载下来之后经过分离音视频流,解码等流程后输出到外设设备上播放。
如果我们在打开一个视频的时候才开始下载数据的话,无疑会增加视频的打开时间(首播时间),并且播放的过程中会有卡顿。所以我们的播放器中通常会设计一些缓存的组件,在未打开视频时缓存一部分视频数据,比如打开抖音,服务端可能一次会返回三个视频信息,我们在播放第一个视频的时候,播放器已经帮我们缓存了第二、3个视频的部分数据,这样在看第二个视频的时候可以给用户"秒开"的感觉。
除此之外,我们熟知的HTTP协议也是有缓存机制的。当我们第一次请求静态的资源时,比如一张图片,服务端除了返回图片信息,在响应头里还有一个“Etag”的字段。浏览器会缓存图片信息自己这个字段的值。当下一次再请求这个图片的时候,浏览器发起的请求头里面会有一个“If-None-Match”的字段,并且把缓存的Etag的值写进去发给服务端。服务端比对图片信息是否有变化,如果没有,则返回浏览器一个304的状态码,浏览器会继续使用缓存的图片信息。通过这种缓存协商的方式,可以减少网路传输的数据大小,从而提升页面展示的性能。
2.缓存与缓冲区
除了缓存,我们在日常开发过程中还会经常听见一个相似的名词——缓冲区,那么,什么是缓冲区呢?
缓存可以提高低速设备的访问速度,或者减少复杂耗时的计算带来的性能问题。理论上来说,我们可以通过缓存解决所有关于“慢”的问题。比如从磁盘随机读取数据慢,从数据库查询数据慢,只是不同场景消耗的存储成本不同。
缓冲区则是一块临时存储数据的区域,这些数据后面会被传输到其他设备上。缓冲区更像“消息队列篇”中即将提到的消息队列,用以弥补高速设备和低速设备通信时的速度差。比如我们将数据写入磁盘时并不是直接刷盘,而是写到一块缓冲区里面,内核会标识这个缓冲区为脏。当经过一定时间或者脏缓冲区比例到一定阈值时,由单独的线程把脏块刷新到硬盘上。这样避免了每次写数据都要刷盘带来的性能问题。
以上就是缓冲区和缓存的区别。
现在你已经了解了缓存的含义,那么我们经常使用的缓存有哪些呢?我们又如何使用缓存,将它的优势最大化呢?
缓存分类
常见的缓存主要是静态缓存、分布式缓存和热点本地缓存三种。
静态花奴村在Web1.0时期是非常著名的,它一般通过生成Velocity模板或者静态HTML文件来实现静态缓存,在Nginx上部署静态缓存可以减少对于后台应用服务器的压力。例如我在做一些内容管理系统的时候,后台会录入很多的文章,前台在网站上展示文章内容,就像新浪网易这种门户网站一样。
当然我们也可以把文章录入到数据库里面,然后前端展示的时候穿透查询数据库来获取数据,但是这样会对数据库造成很大压力。即使我们使用分布式缓存来挡读请求,但是对于像日均PV几十亿的大型门户网站来说,基于成本考虑仍然是不划算的。
所以我们的解决思路是每篇文章在录入的时候渲染成静态页面,放置在所有的前端Nginx等Web服务器上,这样用户在访问的时候会优先访问Web服务器上的静态页面,再对旧的文章执行一定的清理策略后,依然可以保证99%以上的缓存命中率。
这种缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢?这时候就需要分布式缓存了。
我们平时熟悉的Memcached、redis就是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色。
静态的资源缓存可以考虑静态缓存,对于动态的请求可以选择分布式缓存,什么时候需要考虑热点本地缓存呢?
答案就是当我们遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。
比如某个明星结婚了这种无聊的新闻,吃瓜群众回到ta的首页围观,这就会引发这个用户信息的热点查询。这些查询通常会命中某一个缓存节点或者某一个数据库分区,短时间内会形成极高的热点查询。
那么我们会在代码中使用一些本地缓存方案,如HashMap,Guava Cache或者Ehcache等,他们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,可以用来阻挡短时间内的热点查询。例子:
比如说你的电商系统的首页有一些推荐的商品,这些商品信息是由编辑在后台录入和变更。你分析编辑录入新的商品或者变更某个商品的信息后,在页面的展示是允许有一些延迟的的,比如说30s的延迟,并且首页请求量最大,即使使用分布式缓存也很难抗住,所以你决定使用Guava Cache来将所有的推荐商品的信息缓存起来,并且设置每隔30s重新从数据库中加载最新的所有商品。
首先,我们初始化Guava的Loading Cache:
CacheBuilder<String, List<Product>> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //设置缓存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); //设置刷新间隔
LoadingCache<String, List<Product>> cache = cacheBuilder.build(new CacheLoader<String, List<Product>>() {
@Override
public List<Product> load(String k) throws Exception {
return productService.loadAll(); // 获取所有商品
}
});
这样你在获取所有商品信息的时候可以调用Loading Cache的get方法,就可以优先从本地缓存中获取商品信息,如果本地缓存不存在,会使用CacheLoader中的洛基从数据库中加载所有的商品。
由于本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。
缓存的不足
首先,缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。这是因为缓存毕竟会受限于存储介质不可能缓存所有数据。
其次,缓存会给整体系统带来复杂度,并且会有数据不一致的风险。当更新数据库成功,更新缓存失败的情况下,缓存在就会存在脏数据,对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。
缓存通常使用内存作为存储介质,但是内存并不是无限的。
最后,缓存会给运维也带来一定的成本。
虽然有这么多不足,但是缓存对于性能的提升是毋庸置疑的。做具体方案的时候需要对缓存的设计有更细致的思考才能最大化的发挥缓存的优势。