本文作者:张勇,现任科大讯飞高等架构师。
11年后端履历,曾就职于同程艺龙、神州优车等公司。
乐于分享、热衷通过自己的实践履历平铺对技能的理解。

一位七牛的资深架构师曾经说过这样一句话:

oscachejsp缓存一个架构师的缓存修炼之路 Docker

Nginx+业务逻辑层+数据库+缓存层+行列步队,这种模型险些能适配绝大部分的业务场景。

这么多年过去了,这句话或深或浅地影响了我的技能选择,以至于后来我花了很多韶光去重点学习缓存干系的技能。

我在10年前开始利用缓存,从本地缓存、到分布式缓存、再到多级缓存,踩过很多坑。
下面我结合自己利用缓存的进程,谈谈我对缓存的认识。

01 本地缓存

1. 页面级缓存

我利用缓存的韶光很早,2010年旁边利用过 OSCache,当时紧张用在 JSP 页面中用于实现页面级缓存。
代码类似这样:

<cache:cachekey="foobar"scope="session">some jspcontent</cache:cache>

中间的那段 JSP 代码将会以 key="foobar" 缓存在 session 中,这样其他页面就能共享这段缓存内容。
在利用 JSP 这种远古技能的场景下,通过引入 OSCache 之后 ,页面的加载速率确实提升很快。

但随着前后端分离以及分布式缓存的兴起,做事真个页面级缓存已经很少利用了。
但是在前端领域,页面级缓存仍旧很盛行。

2. 工具缓存

2011年旁边,开源中国的红薯哥写了很多篇关于缓存的文章。
他提到:开源中国每天百万的动态要求,只用 1 台 4 Core 8G 的做事器就扛住了,得益于缓存框架 Ehcache。

这让我非常憧憬,一个大略的框架竟能将单机性能做到如此这般,让我欲欲跃试。
于是,我参考红薯哥的示例代码,在公司的余额提现做事上第一次利用了 Ehcache。

逻辑也很大略,便是将成功或者失落败状态的订单缓存起来,这样下次查询的时候,不用再查询支付宝做事了。
伪代码类似这样:

添加缓存之后,优化的效果很明显 , 任务耗时从原来的40分钟减少到了5~10分钟。

上面这个示例便是范例的「工具缓存」,它是本地缓存最常见的运用处景。
比较页面缓存,它的粒度更细、更灵巧,常用来缓存很少变革的数据,比如:全局配置、状态已完结的订单等,用于提升整体的查询速率。

3. 刷新策略

2018年,我和我的小伙伴自研了配置中央,为了让客户端以最快的速率读取配置, 本地缓存利用了 Guava,整体架构如下图所示:

那本地缓存是如何更新的呢?有两种机制:

客户端启动定时任务,从配置中央拉取数据。
当配置中央有数据变革时,主动推送给客户端。
这里我并没有利用websocket,而是利用了 RocketMQ Remoting 通讯框架。

后来我阅读了 Soul 网关的源码,它确当地缓存更新机制如下图所示,共支持 3 种策略:

▍zookeeper watch机制

soul-admin 在启动的时候,会将数据全量写入 zookeeper,后续数据发生变更时,会增量更新 zookeeper 的节点。
与此同时,soul-web 会监听配置信息的节点,一旦有信息变更时,会更新本地缓存。

▍websocket 机制

websocket 和 zookeeper 机制有点类似,当网关与 admin 首次建立好 websocket 连接时,admin 会推送一次全量数据,后续如果配置数据发生变更,则将增量数据通过 websocket 主动推送给 soul-web。

▍http 长轮询机制

http要求到达做事端后,并不是立时相应,而是利用 Servlet 3.0 的异步机制相应数据。
当配置发生变革时,做事端会挨个移除行列步队中的长轮询要求,奉告是哪个 Group 的数据发生了变更,网关收到相应后,再次要求该 Group 的配置数据。

不知道大家创造了没?

pull 模式必不可少增量推送大同小异

长轮询是一个故意思的话题 , 这种模式在 RocketMQ 的消费者模型也同样被利用,靠近准实时,并且可以减少做事真个压力。

02 分布式缓存

关于分布式缓存, memcached 和 Redis 该当是最常用的技能选型。
相信程序员朋友都非常熟习了,我这里分享两个案例。

1. 合理掌握工具大小及读取策略

2013年,我做事一家彩票公司,我们的比分直播模块也用到了分布式缓存。
当时,碰着了一个 Young GC 频繁的线上问题,通过 jstat 工具排查后,创造新生代每隔两秒就被占满了。

进一步定位剖析,原来是某些 key 缓存的 value 太大了,均匀在 300K旁边,最大的达到了500K。
这样在高并发下,就很随意马虎导致 GC 频繁。

找到了根本缘故原由后,详细怎么改呢?我当时也没有清晰的思路。
于是,我去同行的网站上研究他们是怎么实现相同功能的,包括:360彩票,澳客网。
我创造了两点:

1、数据格式非常精简,只返回给前端必要的数据,部分数据通过数组的办法返回

2、利用 websocket,进入页面后推送全量数据,数据发生变革推送增量数据

再回到我的问题上,终极是用什么方案办理的呢?当时,我们的比分直播模块缓存格式是 JSON 数组,每个数组元素包含 20 多个键值对, 下面的 JSON 示例我仅仅列了个中 4 个属性。

[{ "playId":"2399", "guestTeamName":"小牛", "hostTeamName":"湖人", "europe":"123"}]

这种数据构造,一样平常情形下没有什么问题。
但是当字段数多达 20 多个,而且每天的比赛场次非常多时,在高并发的要求下实在很随意马虎引发问题。

基于工期以及风险考虑,终极我们采取了比较守旧的优化方案:

1)修正新生代大小,从原来的 2G 修正成 4G

2)将缓存数据的格式由 JSON 改成数组,如下所示:

[["2399","小牛","湖人","123"]]

修正完成之后, 缓存的大小从均匀 300k 旁边降为 80k 旁边,YGC 频率低落很明显,同时页面相应也变快了很多。

但过了一会,cpu load 会在瞬间颠簸得比较高。
可见,虽然我们减少了缓存大小,但是读取大工具依然对系统资源是极大的损耗,导致 Full GC 的频率也不低。

3)为了彻底办理这个问题,我们利用了更风雅化的缓存读取策略。

我们把缓存拆成两个部分,第一部分是全量数据,第二部分是增量数据(数据量很小)。
页面第一次要求拉取全量数据,当比分有变革的时候,通过 websocket 推送增量数据。

第 3 步完成后,页面的访问速率极快,做事器的资源利用也很少,优化的效果非常精良。

经由这次优化,我理解到: 缓存虽然可以提升整体速率,但是在高并发场景下,缓存工具大小依然是须要关注的点,稍不留神就会产生事件。
其余我们也须要合理地掌握读取策略,最大程度减少 GC 的频率 , 从而提升整体性能。

2. 分页列表查询

列表如何缓存是我非常渴望和大家分享的技能点。
这个知识点也是我 2012 年从开源中国上学到的,下面我以「查询博客列表」的场景为例。

我们先说第 1 种方案:对分页内容进行整体缓存。
这种方案会按照页码和每页大小组合成一个缓存key,缓存值便是博客信息列表。
如果某一个博客内容发生修正, 我们要重新加载缓存,或者删除整页的缓存。

这种方案,缓存的颗粒度比较大,如果博客更新较为频繁,则缓存很随意马虎失落效。
下面我先容下第 2 种方案:仅对博客进行缓存。
流程大致如下:

1)先从数据库查询当前页的博客id列表,sql类似:

selectidfromblogslimit0,10

2)批量从缓存中获取博客id列表对应的缓存数据 ,并记录没有命中的博客id,若没有命中的id列表大于0,再次从数据库中查询一次,并放入缓存,sql类似:

selectidfromblogswhereidin(noHitId1,noHitId2)

3)将没有缓存的博客工具存入缓存中

4)返回博客工具列表

理论上,假如缓存都预热的情形下,一次大略的数据库查询,一次缓存批量获取,即可返回所有的数据。
其余,关于缓存批量获取,如何实现?

本地缓存:性能极高,for 循环即可memcached:利用 mget 命令Redis:若缓存工具构造大略,利用 mget 、hmget命令;若构造繁芜,可以考虑利用 pipleline,lua脚本模式

第 1 种方案适用于数据极少发生变革的场景,比如排行榜,首页新闻资讯等。

第 2 种方案适用于大部分的分页场景,而且能和其他资源整合在一起。
举例:在搜索系统里,我们可以通过筛选条件查询出博客 id 列表,然后通过如上的办法,快速获取博客列表。

03 多级缓存

首先要明确为什么要利用多级缓存?

本地缓存速率极快,但是容量有限,而且无法共享内存。
分布式缓存容量可扩展,但在高并发场景下,如果所有数据都必须从远程缓存种获取,很随意马虎导致带宽跑满,吞吐量低落。

有句话说得好,缓存离用户越近越高效!

利用多级缓存的好处在于:高并发场景下, 能提升全体系统的吞吐量,减少分布式缓存的压力。

2018年,我做事的一家电商公司须要进行 app 首页接口的性能优化。
我花了大概两天的韶光完成了全体方案,采纳的是两级缓存模式,同时利用了 guava 的惰性加载机制,整体架构如下图所示:

缓存读取流程如下:

1、业务网关刚启动时,本地缓存没有数据,读取 Redis 缓存,如果 Redis 缓存也没数据,则通过 RPC 调用导购做事读取数据,然后再将数据写入本地缓存和 Redis 中;若 Redis 缓存不为空,则将缓存数据写入本地缓存中。

2、由于步骤1已经对本地缓存预热,后续要求直接读取本地缓存,返回给用户端。

3、Guava 配置了 refresh 机制,每隔一段韶光会调用自定义 LoadingCache 线程池(5个最大线程,5个核心线程)去导购做事同步数据到本地缓存和 Redis 中。

优化后,性能表现很好,均匀耗时在 5ms 旁边。
最开始我以为涌现问题的几率很小,可是有一天晚上,溘然创造 app 端首页显示的数据时而相同,时而不同。

也便是说: 虽然 LoadingCache 线程一贯在调用接口更新缓存信息,但是各个做事器本地缓存中的数据并非完成同等。
解释了两个很主要的点:

1、惰性加载仍旧可能造成多台机器的数据不一致

2、 LoadingCache 线程池数量配置的不太合理, 导致了线程堆积

终极,我们的办理方案是:

1、惰性加载结合机制来更新缓存数据,也便是:当导购做事的配置发生变革时,关照业务网关重新拉取数据,更新缓存。

2、适当调大 LoadigCache 的线程池参数,并在线程池埋点,监控线程池的利用情形,当线程繁忙时能发出告警,然后动态修正线程池参数。

写在末了

缓存是非常主要的一个技能手段。
如果能从事理到实践,不断深入地去节制它,这该当是技能职员最享受的事情。

这篇文章属于缓存系列的开篇,更多是把我 10 多年事情中碰着的范例问题娓娓道来,并没有非常深入地去磋商事理性的知识。

我想我更该当和朋友互换的是:如何体系化的学习一门新技能。

选择该技能的经典书本,理解根本观点 建立该技能的知识脉络 知行合一,在生产环境中实践或者自己造轮子不断复盘,思考是否有更优的方案

后续我会连载一些缓存干系的内容:包括缓存的高可用机制、codis 的事理等,欢迎大家连续关注。

关于缓存,如果你有自己的心得体会或者想深入理解的内容,欢迎评论区留言。

<END>