面试官:谈谈你是怎么理解缓存的?
下面我结合自己使用缓存的历程,谈谈我对缓存的认识。
01 本地缓存
1. 页面级缓存
我使用缓存的时间很早,2010年左右使用过 OSCache,当时主要用在 JSP 页面中用于实现页面级缓存。伪代码类似这样:
"foobar"
some jsp content
中间的那段 JSP 代码将会以 key="foobar" 缓存在 session 中,这样其他页面就能共享这段缓存内容。在使用 JSP 这种远古技术的场景下,通过引入 OSCache 之后 ,页面的加载速度确实提升很快。
但随着前后端分离以及分布式缓存的兴起,服务端的页面级缓存已经很少使用了。但是在前端领域,页面级缓存仍然很流行。
2. 对象缓存
2011年左右,开源中国的红薯哥写了很多篇关于缓存的文章。他提到:开源中国每天百万的动态请求,只用 1 台 4 Core 8G 的服务器就扛住了,得益于缓存框架 Ehcache。
这让我非常神往,一个简单的框架竟能将单机性能做到如此这般,让我欲欲跃试。于是,我参考红薯哥的示例代码,在公司的余额提现服务上第一次使用了 Ehcache。
逻辑也很简单,就是将成功或者失败状态的订单缓存起来,这样下次查询的时候,不用再查询支付宝服务了。伪代码类似这样:
添加缓存之后,优化的效果很明显 , 任务耗时从原来的40分钟减少到了5~10分钟。
上面这个示例就是典型的「对象缓存」,它是本地缓存最常见的应用场景。相比页面缓存,它的粒度更细、更灵活,常用来缓存很少变化的数据,比如:全局配置、状态已完结的订单等,用于提升整体的查询速度。
客户端启动定时任务,从配置中心拉取数据。 当配置中心有数据变化时,主动推送给客户端。这里我并没有使用websocket,而是使用了 RocketMQ Remoting 通讯框架。
pull 模式必不可少 增量推送大同小异
02 分布式缓存
关于分布式缓存, memcached 和 Redis 应该是最常用的技术选型。相信程序员朋友都非常熟悉了,我这里分享两个案例。
1. 合理控制对象大小及读取策略
2013年,我服务一家彩票公司,我们的比分直播模块也用到了分布式缓存。当时,遇到了一个 Young GC 频繁的线上问题,通过 jstat 工具排查后,发现新生代每隔两秒就被占满了。
进一步定位分析,原来是某些 key 缓存的 value 太大了,平均在 300K左右,最大的达到了500K。这样在高并发下,就很容易导致 GC 频繁。
找到了根本原因后,具体怎么改呢?我当时也没有清晰的思路。于是,我去同行的网站上研究他们是怎么实现相同功能的,包括:360彩票,澳客网。我发现了两点:
1、数据格式非常精简,只返回给前端必要的数据,部分数据通过数组的方式返回 2、使用 websocket,进入页面后推送全量数据,数据发生变化推送增量数据
[{
"playId":"2399",
"guestTeamName":"小牛",
"hostTeamName":"湖人",
"europe":"123"
}]
[["2399","小牛","湖人","123"]]
2. 分页列表查询
列表如何缓存是我非常渴望和大家分享的技能点。这个知识点也是我 2012 年从开源中国上学到的,下面我以「查询博客列表」的场景为例。
我们先说第 1 种方案:对分页内容进行整体缓存。这种方案会按照页码和每页大小组合成一个缓存key,缓存值就是博客信息列表。假如某一个博客内容发生修改, 我们要重新加载缓存,或者删除整页的缓存。
这种方案,缓存的颗粒度比较大,如果博客更新较为频繁,则缓存很容易失效。下面我介绍下第 2 种方案:仅对博客进行缓存。流程大致如下:
1)先从数据库查询当前页的博客id列表,sql类似:
select id from blogs limit 0,10
2)批量从缓存中获取博客id列表对应的缓存数据 ,并记录没有命中的博客id,若没有命中的id列表大于0,再次从数据库中查询一次,并放入缓存,sql类似:
select id from blogs where id in (noHitId1, noHitId2)
3)将没有缓存的博客对象存入缓存中
4)返回博客对象列表
理论上,要是缓存都预热的情况下,一次简单的数据库查询,一次缓存批量获取,即可返回所有的数据。另外,关于缓存批量获取,如何实现?
本地缓存:性能极高,for 循环即可
memcached:使用 mget 命令
Redis:若缓存对象结构简单,使用 mget 、hmget命令;若结构复杂,可以考虑使用 pipleline,lua脚本模式
第 1 种方案适用于数据极少发生变化的场景,比如排行榜,首页新闻资讯等。
第 2 种方案适用于大部分的分页场景,而且能和其他资源整合在一起。举例:在搜索系统里,我们可以通过筛选条件查询出博客 id 列表,然后通过如上的方式,快速获取博客列表。
03 多级缓存
首先要明确为什么要使用多级缓存?
本地缓存速度极快,但是容量有限,而且无法共享内存。分布式缓存容量可扩展,但在高并发场景下,如果所有数据都必须从远程缓存种获取,很容易导致带宽跑满,吞吐量下降。
有句话说得好,缓存离用户越近越高效!
使用多级缓存的好处在于:高并发场景下, 能提升整个系统的吞吐量,减少分布式缓存的压力。
2018年,我服务的一家电商公司需要进行 app 首页接口的性能优化。我花了大概两天的时间完成了整个方案,采取的是两级缓存模式,同时利用了 guava 的惰性加载机制,整体架构如下图所示:
优化后,性能表现很好,平均耗时在 5ms 左右。最开始我以为出现问题的几率很小,可是有一天晚上,突然发现 app 端首页显示的数据时而相同,时而不同。
也就是说: 虽然 LoadingCache 线程一直在调用接口更新缓存信息,但是各个服务器本地缓存中的数据并非完成一致。说明了两个很重要的点:
1、惰性加载仍然可能造成多台机器的数据不一致
2、 LoadingCache 线程池数量配置的不太合理, 导致了线程堆积
最终,我们的解决方案是:
1、惰性加载结合消息机制来更新缓存数据,也就是:当导购服务的配置发生变化时,通知业务网关重新拉取数据,更新缓存。
2、适当调大 LoadigCache 的线程池参数,并在线程池埋点,监控线程池的使用情况,当线程繁忙时能发出告警,然后动态修改线程池参数。
缓存是非常重要的一个技术手段。如果能从原理到实践,不断深入地去掌握它,这应该是技术人员最享受的事情。
我想我更应该和朋友交流的是: 如何体系化的学习一门新技术。
选择该技术的经典书籍,理解基础概念 建立该技术的知识脉络 知行合一,在生产环境中实践或者自己造轮子 不断复盘,思考是否有更优的方案
推荐阅读:
喜欢我可以给我设为星标哦