通过前面学习,你已经了解了在高并发大流量下,数据库层的演进,包含数据库层的主从分离和分库分表。 测试你的系统架构已经可以支撑几十万的DAU了,整体的架构如下图: 整体上来看,数据库分成了主库和从库,数据也被切分到了多个数据库节点上。但是随着并发的增多,存储数据的增多,数据库的磁盘IO逐渐成了系统的瓶颈,我们需要一种更快的组件来降低请求响应时间,提升系统性能。这时就引入了我们今天的主角: 缓存 什么是缓存,如何将它的优势最大化呢? 本节课缓存篇的总纲,从缓存的定义、缓存的分类和缓存的优劣势三个方面来了解缓存的设计思想和理念。再用4节课针对性的掌握缓存的正确使用姿势,以便在实际的工作中更好的使用缓存提升系统的性能。 什么是缓存 缓存是一种存储数据的组件,它的作用就是让对数据的请求更快的返回。 我们经常把缓存放到内存中来存储,所以就有人把内存跟缓存画上了等号,完全外行。作为业内人士,你要知道我们在某些场景下可能还会使用SSD作为冷数据的存储。比如360开源的Pika就是使用SSD解决Redis容量瓶颈的问题。 实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的,都可以称为缓存。那么说道这里,我们就需要知道常见硬件组件的延时情况是什么,这样做方案的时候可以有直观的印象。所幸,业内已经有人总结出这些数据了,我们整理如下: 从这些数据可以看到,做一次内存寻址大概需要100ms,而做一次磁盘的查找需要10ms,从这里 我们就可以看出内存作为存储介质和磁盘作为存储介质性能上的差距,所以,内存是一种最常见的缓存数据介质。 缓存作为一种常见的空间换时间的性能优化手段,很多地方应用案例: 1、缓存案例 Linux 内存管理是通过一个叫做 MMU(Memory Management Unit)的硬件,来实现从虚拟地址到物理地址的转换的,但是如果每次转换都要做这么复杂计算的话,无疑会造成性能的损耗,所以我们会借助一个叫做 TLB(Translation Lookaside Buffer)的组件来缓存最近转换过的虚拟地址,和物理地址的映射。TLB 就是一种缓存组件,缓存复杂运算的结果,就好比你做一碗色香味俱全的面条可能比较复杂,那么我们把做好的面条油炸处理一下做成方便面,你做方便面的话就简单多了,也快速多了。这个缓存组件比较底层,这里你只需要了解一下就可以了。 在大部分的笔记本,桌面电脑和服务器上都会有一个或者多个 TLB 组件,在不经意间帮助我们加快地址转换的速度。 再想一下,你经常刷的抖音,平台上的短视频实际使用内置的网络播放器来完成 ,网络播放器接受的是数据流,将数据下载下来之后,经过分离音视频流,解码等流程之后输出到外设设备上。 如果我们在打开一个视频的时候才开始下载数据的话,无疑会增加视频的打开速度,并且播放过程中会有卡顿,所以我们播放器中通常会设计一些缓存的组件,在未打开视频时候缓存一部分视频数据,比如我们打开抖音,服务端可能一次会返回三个视频信息,我们再播放第一个视频的时候,播放器已经帮我们缓存了第二个 三个视频数据,这样在播放第二个视频的时候,给用户 秒开的感觉。 除此之外,我们知道的http协议也是有缓存机制的。当我们第一次请求静态资源时,比如一张图片,服务端除了返回图片信息,在响应头里面还有一个 etag 的字段。浏览器会缓存图片信息及这个字段的值,当下一次在请求的时候,请求头里会有一个 if none match 字段,并且把 etag的值写进去发给服务端。服务端比对图片信息是否有变化,如果没有,返回一个304的状态,浏览器会继续使用缓存的图片信息,通过这种缓存协商的方式,可以减少网络传输数据大小,从而提升页面展示的性能。 2、缓存与缓冲区 讲了缓存案例,想必你对缓存已经有了一个直观且形象的了解了。除了缓存,我们再开发过程中还经常听到一个相似的名词—— 缓冲区。那么什么事缓冲区,跟缓存有什么区别呢? 缓存可以提高高低速设备的访问速度,或者减少复杂耗时计算带来的性能问题,理论上可以通过缓存解决所有关于慢的问题,比如从磁盘随机读取数据慢,从数据库查询数据慢只是不同的场景耗用的成本不同。 缓冲区则是一块临时存储数据的区域,这些数据后面会被传输到其他设备上缓冲区更像一种消息队列的模式,用以弥补高速设备和低速设备 的速度差,比如我们将数据写入磁盘时,并不是直接刷盘,而是写到一块缓冲区里面,内核会标记这个缓冲区为脏,当经过一段时间或者缓冲区比例到达一定阈值时,有单独的线程把脏数据刷到硬盘上。这样就避免了每次写数据带来的性能问题。 以上就是缓存和缓冲区的区别,那么我们常用的缓存有哪些?我们又该如何使用缓存才能将他的优势最大化呢? 缓存分类 我们日常开发中,常见的缓存主要有:静态缓存、分布式缓存、热点本地缓存。 对于日均PV几十亿的门户网站来说,如果每天发布的内容都是保存在数据库,页面展示的时候通过穿透数据库来读取,这样显然来说是不合适的,即使我们使用了分布式缓存,基于成本考虑仍然是不划算的。 所以我们的解决思路是每篇文章在录入的时候渲染成静态页面 ,放置在所有的前端Nginx或者squid等web服务器上,这样用户访问到的优先就是服务器上缓存的静态页面,再对旧的文章执行一定的清理之后依然可以保证90%以上的命中率。 这种缓存只能针对静态数据来缓存,对于动态的缓存你可以选择分布式缓存,那么为什么要考虑本地缓存呢? 答案是我们遇到极端的热点数据查询的时候,热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存或者数据库的压力。 比如某位明星的热点时间,会短时间引起很多围观用户,造成瞬间大量请求命中某一个缓存节点或者一个数据库分区,速度极快。 那么我们通常使用的本地缓存方案有哪些呢?如 hashmap,guava cache , ehcache等,他们和应用程序部署在同一个文件中,是不需要跨网络调度,速度极快,所有可以用来阻挡某些热点查询。 比如电商系统中有这样一个场景:首页的商品信息是每隔一段时间会更新的,但是展示是不需要实时的可以设置每隔30S更新一次,这个场景我们就可以利用guava cache 把商品信息缓存在本地,然后每隔30s 去后台重新拉取更新后的商品信息,在缓存起来。 首先,初始化的操作如下: CacheBuilder> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //设置缓存最大值 cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); //设置刷新间隔 LoadingCache> cache = cacheBuilder.build(new CacheLoader>() { @Override public List load(String k) throws Exception { return productService.loadAll(); // 获取所有商品 } }); 这样你获取商品信息的时候可以调用Loading Cache 的 get 方法,就可以优先从本地缓存中获取商品信息,如果缓存不存在就是使用CacheLoader中的逻辑从数据库加载数据。 由于本地缓存部署在应用服务器的,而我们的应用服务器通常部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期,因此这种缓存的有效期很短,通常为分总或者秒级,便面返回给前端脏数据。 缓存的不足 缓存的主要作用提升访问速度,从而抵抗住高并发请求,那么缓存是不是能解决一切问题呢,肯定不是,凡是事务都有两面性,我们更需要了解它的不足,从而更好的发挥缓存的优势。 首先,缓存适合读多写少的场景,最好带有一定的热点属性 ,缓存毕竟受限于存储介质不可能缓存所有的数据,当数据有热点属性才能保证一定的缓存命中率,所以一旦当场景读少写多的时候,缓存的作用就不那么明显了。 其次,缓存会带来系统的复杂度,并且存在数据不一致的风险 。当更新数据库成功,更新缓存失败的时候,缓存中就会存在脏数据,对于这种场景我可以考虑使用较短的国企时间和手动清理缓存方式解决。 再次,之前提到的缓存通常使用内存作为存储介质,但是内存不是无限的 。因此我们再使用缓存的时候要注意评估存储量级,对于存储成本极大的数据,慎用缓存方案。 最后,缓存给运维也带来一定的成本。 运维需要对组件有一定的了解。 缓存虽然有这么多问题,但是对系统的性能提升毋庸置疑的,所以我们在走架构方案设计的时候一定要把缓存考虑进去。 缓存不仅仅是一种组件的名字,更是一种设计思想,任务加速读请求的组件和设计方案都是缓存思想的体现。