在微服务架构中,缓存中间件越来越成为不可或缺的组件,下面聊聊微服务环境下的缓存设计。
缓存在应用软件架构中是提高性能最直接的方式,如下
假设应用程序将数据存储在Mysql中,众所周知Mysql会将数据存储在硬盘上以防止掉电造成数据丢失,但是受限于硬盘的物理设计,即使是性能最好的SSD硬盘,也比内存这种高速设备IO层面上差,而以京东、拼多多这种电商为代表的互联网应用,都是典型的 读多写少 的场景,隐藏需要在设计上进行数据的读写分离,在数据写入时直接落盘,而大部分的读取操作则从以Redis为代表的Nosql数据库中读取,利用内存的高吞吐瞬间完成数据的读取。
缓存可不只有用内存代替硬盘这一种形式,在微服务架构中的缓存可以通过多级缓存的方式进行处理。
可以看出缓存设计主要包括:
某某商城的客户端为浏览器,在浏览器层面主要是对HTML中的图片、CSS、JS这些静态资源进行缓存。
以百度官网的log图片为例,看到响应头中通过Expires控制静态图片的有效期。通过在浏览器端设置 Expires 可以在很大程度减少重复请求静态资源带来的带宽损耗,这在高并发 Web 应用中是基础而重要的设置。
Expires在那里设置?对于浏览器来说,它只是客户端,只负责读取Expires响应头,则Expires要在应用层中进行设置,即CDN与Nginx中设置。
CDN 全称是 Content Delivery Network,即内容分发网络,是互联网静态资源分发的主要技术手段。
中国幅员辽阔,从北京到上海就有上千公里,如果大量的上海用户同时要访问千里之外的北京服务器的资源,这么长的通信必然带来高延迟与更多不可控因素影响数据传输,如果有某种机制允许将北京的静态文件缓存到上海的服务器,上海用户自动就近访问服务器获取资源,这样便可很大程度降低网络延迟,进而提高系统的可用性。而刚才提到的分布式缓存技术就是我们常提到的CDN(内容分发网络)。
对于广域的互联网应用,CDN 几乎是必需的基础设施,它有效解决了带宽集中占用以及数据分发的问题。像 Web 页面中的图片、音视频、CSS、JS 这些静态资源,都可以通过 CDN 服务器就近获取。
在互联网应用中,因为 CDN 涉及多地域多节点组网,前期投入成本较高,更多的中小型软件公司通常会选择阿里云、腾讯云等大厂提供的 CDN 服务,通过按需付费的方式降低硬件成本。而这些服务商又会为 CDN 赋予额外的能力,比如阿里云、腾讯云 CDN 除了缓存文件之外,还提供了管理后台能为响应赋予额外的响应头。如下所示在阿里云 CDN 后台,就额外设置了 Cache-Control 响应头代表缓存有效期为 1 小时。这里我们额外提一下 Expires 与的 Cache-Control 的区别,Expires 是指定具体某个时间点缓存到期,而 Cache-Control 则代表缓存的有效期是多长时间。Expires 设置时间,Cache-Control 设置时长,根据业务场景不同可以使用不同的响应头。
Nginx 是一款开源的、跨平台的高性能 Web 服务器,它有着高性能,稳定性好,配置简单,模块结构化,资源消耗低的优点。同时支持反向代理、负载均衡、缓存的功能。Nginx 是 Web 应用架构中的常客,例如后端 Tomcat 集群便可通过增加 Nginx 前置做软负载均衡,为应用提供高可用特性。
在互联网应用中,用户分布在全国各地,对资源的响应速度与带宽要求较高,因此部署 CDN 是十分有必要的。但在更多的企业应用中,其实大部分的企业用户都分布在指定的办公区域或者相对固定的场所,再加上并发用户相对较少,其实并不需要额外部署 CDN 这种重量级解决方案。在架构中只需要部署 Nginx 服务器,利用 Nginx 自带的静态资源缓存与压缩功能便可胜任大多数企业应用场景。
在 Nginx 中自带将后端应用中图片、CSS、JS 等静态资源缓存功能,我们只需在 Nginx 的核心配置 nginx.conf 中增加下面的片段,便可对后端的静态资源进行缓存,示例如下
# 设置缓存目录
# levels代表采用1:2也就是两级目录的形式保存缓存文件(静态资源css、js)
# keys_zone定义缓存的名称及内存的使用,名称为babytun-cache ,在内存中开始100m交换空间
# inactive=7d 如果某个缓存文件超过7天没有被访问,则删除
# max_size=20g;代表设置文件夹最大不能超过20g,超过后会自动将访问频度(命中率)最低的缓存文件删除
proxy_cache_path d:/nginx-cache levels=1:2 keys_zone=babytun-cache:100m inactive=7d max_size=20g;
#配置xmall后端服务器的权重负载均衡策略
upstream xmall {
server 192.168.31.181 weight=5 max_fails=1 fail_timeout=3s;
server 192.168.31.182 weight=2;
server 192.168.31.183 weight=1;
server 192.168.31.184 weight=2;
}
server {
#nginx通过80端口提供Web服务
listen 80;
# 开启静态资源缓存
# 利用正则表达式匹配URL,匹配成功的则执行内部逻辑
# ~* 代表URL匹配不区分大小写
location ~* \.(gif|jpg|css|png|js|woff|html)(.*){
# 配置代理转发规则
proxy_pass http://xmall;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache xmall-cache;
#如果静态资源响应状态码为200(成功) 302(暂时性重定向)时 缓存文件有效期1天
proxy_cache_valid 200 302 24h;
#301(永久性重定向)缓存保存5天
proxy_cache_valid 301 5d;
#其他情况
proxy_cache_valid any 5m;
#设置浏览器端缓存过期时间90天
expires 90d;
}
无论是CDN还是Nginx,都是对web应用中的静态资源文件进行缓存。但后端应用与服务更多的是访问接口与数据,对此,可以按部署方式分为进程内缓存与分布式缓存服务/
所谓进程内缓存,就是在应用中开辟的一块内存空间,数据在运行时被载入这块内存,通过本地内存的低延迟、高吞吐的特性提高程序的访问速度。进程内缓存在众多 Java 框架内都有广泛应用,例如 Hibernate、Mybatis 框架的一二级缓存、Spring MVC 的页面缓存都是进程内缓存的经典应用场景,这些进程内缓存在 Java 中也有着非常多优秀的开源实现,如 EhCache、Caffeine 都是代表性产品。
与进程内相对的,就是需要独立部署的分布式缓存服务。最常用的是基于 Redis 这种内存型 NoSQL 数据库,对整体架构中的应用数据进行集中缓存。
进行缓存设计时,下意识认为增加 Redis 分布式缓存服务器就够了,其实这是片面的做法。在 Java 的应用端也要设计多级缓存,我们将进程内缓存与分布式缓存服务结合,有效分摊应用压力。在 Java 应用层面,只有 EhCache 的缓存不存在时,再去 Redis 分布式缓存获取,如果 Redis 也没有此数据再去数据库查询,数据查询成功后对 Redis 与 EhCahce 同时进行双写更新。这样 Java 应用下一次再查询相同数据时便直接从本地 EhCache 缓存提取,不再产生新的网络通信,应用查询性能得到显著提高。
当引入多级缓存后,又会遇到缓存数据一致性的问题,如写操作,是不通过缓存的,那么如何主动向应用程序推送数据变更的消息来保证同步更新缓存呢?
此时可以引入MQ消息队列,利用 RocketMQ 的主动推送功能来向其他服务实例以及 Redis 缓存服务发起变更通知。虽然多级缓存设计带来了更好的应用性能,但也为了缓存一致性必须引入 MQ 增加了架构的复杂度。那到底多级缓存设计该如何取舍呢?根据具体的业务场景引入多级缓存。
缓存的数据是稳定的。例如邮政编码、地域区块、归档的历史数据这些信息适合通过多级缓存减小 Redis 与数据库的压力。
缓存设计时需要考虑具体的需求场景,若简单,访问量不大,在未来的1~2 年内利用 Redis 分布式缓存集群完全可以胜任应用性能要求,那自然就没有必要设计多级缓存。若是并发量大,会有流量洪峰,则可以考虑多级缓存的设计。