@[toc]
为什么要用缓存?
首先先了解一下为什么在网关中我们需要用到缓存。 我们可以从如下几点来入手这个问题:
-
处理大规模流量: 网关是系统的入口,需要处理大规模的请求流量。高性能的网关能够快速而有效地处理大量的请求,确保系统对外提供稳定和快速的服务。
-
复杂的请求处理: 网关可能需要执行复杂的请求处理逻辑,包括身份验证、授权、路由、转换等。高性能的网关可以更快速地执行这些操作,确保请求能够快速而正确地被处理。
-
保障低延迟: 对于许多应用场景,低延迟是关键。高性能的网关能够在最短的时间内完成请求处理,提供低延迟的服务,满足用户的性能期望。
-
应对突发流量: 突发流量是网络系统经常面临的挑战之一。高性能的网关能够更好地应对突发的请求流量,确保系统在高峰时期仍能够提供稳定的服务。
思考清楚了这些特点之后,我们就明白了我们需要为网关实现一个功能,这个功能可以为网关带来如上点所提到的性能提升。 最显而易见的方法就是使用缓存,比如使用分布式缓存Redis、本地缓存Guava、Caffeine。
使用缓存,可以为我们带来如下的好处:
-
减少后端服务压力: 网关通常负责处理大量的请求流量,并将请求路由到后端的服务。通过在网关上使用缓存,可以避免重复的请求传递给后端服务,从而减轻后端服务的压力。这对于保护后端服务免受过多请求的冲击非常重要,特别是在面对突发的高并发情况时。
-
提高请求响应速度: 缓存允许网关快速返回之前存储的响应,而无需再次向后端服务发起请求。这能够显著提高请求的响应速度,尤其是对于一些相对稳定的数据或者频繁请求的数据,通过缓存直接返回可以避免等待后端服务的处理时间。
-
降低网络延迟: 网关通常位于客户端和后端服务之间,而后端服务可能分布在不同的地理位置。通过在网关上使用缓存,可以减少对后端服务的实际请求次数,从而降低网络延迟。这对于跨地域或跨网络的架构来说尤其重要。
-
提高系统可伸缩性: 网关需要处理大量的请求,而后端服务的处理能力是有限的。通过使用缓存,可以减轻后端服务的负担,提高系统的可伸缩性。即使后端服务的处理能力有限,网关仍然可以通过缓存来应对更多的请求。
-
增强系统的可用性: 缓存可以提高系统的可用性。当后端服务发生故障或不可用时,缓存可以继续提供之前缓存的响应,保障系统的一部分功能仍然可用。这有助于降低单点故障的风险。
Caffeine Cache
这里,我是用目前我用的最多也是比较火热的一个缓存,Caffeine。 并且同时,我们知道Caffeine基于Guava做了进一步的优化,Guava Cache 利用了 ConcurrentHashMap 的并发特性,将缓存的键值对存储在多个分段中,每个分段独立加锁,以提高并发性。这使得在多线程环境下,多个线程可以同时读取不同的分段,从而减小了锁的粒度,提高了并发读取的性能。 而基于它的Caffeine也一样的提供了这种线程安全的操作。
同时,Caffeine其基于Tiny LFU 算法进一步改进,提出了Window Tiny LFU算法,进一步提高了内存淘汰策略的效率和精确性。
Window Tiny LFU算法介绍 以下是 W-TinyLFU 算法的主要特点和原理:
-
基于Counter的访问频率追踪: W-TinyLFU 和 TinyLFU 一样,通过使用计数器来追踪每个缓存项的访问频率。每个缓存项都有一个计数器,表示它被访问的次数。
-
Adaptive Counting: W-TinyLFU 使用 Adaptive Counting(自适应计数)的方式,而不是传统的精确计数。Adaptive Counting 允许在占用更小空间的情况下估计元素的访问频率。这对于大规模的缓存系统来说是一种效率优化。
-
时间窗口: W-TinyLFU 引入了时间窗口的概念,将计数器的增加限制在一个固定的时间窗口内。这个时间窗口可以是固定大小的,也可以是根据缓存的大小和访问模式动态调整的。这样做的目的是适应访问模式的变化,让缓存更加灵活地适应不同的工作负载。
-
定期清零计数器: 在时间窗口的末尾,W-TinyLFU 对所有计数器进行一次定期清零操作。这意味着在每个时间窗口内,计数器只反映了最近一段时间内的访问情况。这样可以防止计数器过于陈旧,更好地适应动态变化的访问模式。
-
缓存淘汰策略: W-TinyLFU 根据计数器的值来选择淘汰缓存项。具有较低计数器值的缓存项更容易被淘汰,以保留访问频率较高的缓存项。
Caffeine有如下的一些优点:
- 异步加载
- 自定义过期策略
- 最佳化内存管理
- 缓存清理策略
- 支持并发访问
最后,我列出本地缓存之间的差异,可以自行分析他们的优缺点和区别,并且合理的在不同的场合中使用他们。
使用Caffeine
javascript
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.5</version>
</dependency>
使用方式也比较简单,之前我们的请求在进入后都需要重新的完整的请求信息去构建过滤器链,因此如果我们能根据固定的请求信息,来缓存固定的过滤器链,那么我们创建过滤器链的时间就被节省了,性能也就得到了提升。
java
/**
* 使用Caffeine缓存 并且设定过期时间10min
*/
private Cache<String,GatewayFilterChain> chainCache = Caffeine.newBuilder().recordStats().expireAfterWrite(10, TimeUnit.MINUTES).build();
@Override
public GatewayFilterChain buildFilterChain(GatewayContext ctx) throws Exception {
return chainCache.get(ctx.getRule().getId(),k->doBuildFilterChain(ctx.getRule()));
}
public GatewayFilterChain doBuildFilterChain(Rule rule) {
GatewayFilterChain chain = new GatewayFilterChain();
List<Filter> filters = new ArrayList<>();
//filters.add(getFilterInfo(FilterConst.GRAY_FILTER_ID));
//filters.add(getFilterInfo(FilterConst.MONITOR_FILTER_ID));
//filters.add(getFilterInfo(FilterConst.MONITOR_END_FILTER_ID));
//filters.add(getFilterInfo(FilterConst.MOCK_FILTER_ID));
if(rule != null){
Set<Rule.FilterConfig> filterConfigs = rule.getFilterConfigs();
Iterator iterator = filterConfigs.iterator();
Rule.FilterConfig filterConfig;
while(iterator.hasNext()){
filterConfig = (Rule.FilterConfig)iterator.next();
if(filterConfig == null){
continue;
}
String filterId = filterConfig.getId();
if(StringUtils.isNotEmpty(filterId) && getFilterInfo(filterId) != null){
Filter filter = getFilterInfo(filterId);
filters.add(filter);
}
}
}
//添加路由过滤器-这是最后一步
filters.add(getFilterInfo(FilterConst.ROUTER_FILTER_ID));
//排序
filters.sort(Comparator.comparingInt(Filter::getOrder));
//添加到链表中
chain.addFilterList(filters);
return chain;
}
效果演示
可以发现使用了缓存之后的请求响应时间大幅减少,当然,使用这种直接请求然后查看请求响应时间的方式去直接武断的说是由于Caffeine缓存导致了性能的提升是不恰当的,但是我相信这也能从中看出一些端倪,就是使用了Caffeine对性能有一定的提升。