一文了解 CORS 跨域

作为一个 Web 开发,一定不会对下面的跨域报错陌生。

当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。例如站点 http://www.aliyun.com 的某 HTML 页面请求 http://www.alibaba.com/image.jpg。

出于安全原因,浏览器限制从页面脚本内发起的跨域请求,有些浏览器不会限制跨域请求的发起,但是会将结果拦截。 这意味着使用这些 API 的 Web 应用只能加载同一个域下的资源,除非使用 CORS 机制(Cross-Origin Resource Sharing 跨源资源共享)获取目标服务器的授权来解决这个问题。

这也是本文将要探讨的主要问题,需要额外强调的是,跨域问题产生的主体是“浏览器”,这也是为什么,当我们使用 curl、postman、各种语言的 HTTP 客户端等工具时,从来没有被跨域问题困扰过。


EDAS 让 Spring Cloud Gateway 生产可用的二三策

Spring Cloud Gateway 是 Spring Cloud 微服务生态下的网关组件,一直以来备受 Java 社区的用户关注,很多企业选择使用其作为微服务网关或者业务网关。在阿里云上,也不乏有很多网关类型的产品供用户使用,例如 API Gateway 和 MSE Higress,使用 PaaS 化的方式提供网关能力,用户不再需要关注网关的实现,直接获得开箱即用的能力。在从前,用户只能选择自建 Spring Cloud Gateway,或者购买云产品,而今天介绍的 EDAS 增强 Spring Cloud Gateway 的新姿势,给用户提供了一个新的选择。

让 Spring Cloud Gateway 生产可用

开源 Spring Cloud Gateway 存在一些让企业级用户担忧的因素,包括内存泄漏问题,以及路由设计问题,EDAS 根据云服务总线 CSB 多年沉淀下来的 Spring Cloud Gateway 使用经验,对诸多已经存在的问题进行了治理,对诸多的风险因素也进行了规避,彻底打消用户使用 Spring Cloud Gateway 技术侧的顾虑。

  • 内存泄漏问题,该问题来自于 CSB 的生产实践,Spring Cloud Gateway 底层依赖 netty 进行 IO 通信,熟悉 netty 的人应当知道其有一个读写缓冲的设计,如果通信内容较小,一般会命中 chunked buffer,而通信内容较大时,例如文件上传,则会触发内存的新分配,而 Spring Cloud Gateway 在对接 netty 时存在逻辑缺陷,会导致新分配的池化内存无法完全回收,导致堆外内存泄漏。并且这块堆外内存时 netty 使用 unsafe 自行分配的,通过常规的 JVM 工具还无法观测,非常隐蔽。

EDAS 建议为 Spring Cloud Gateway 应用增加启动参数 -Dio.netty.allocator.type=unpooled,使得请求未命中 chunked buffer 时,分配的临时内存不进行池化,规避内存泄漏问题。-Dio.netty.allocator.type=unpooled 不会导致性能下降,只有大报文才会触发该内存的分配,而网关的最佳实践应该是不允许文件上传这类需求,加上该参数是为了应对非主流场景的一个兜底行为。

  • 开源 Spring Cloud Gateway 并未提供路由配置校验能力,当路由配置出错时,可能会带来灾难性的后果,例如在配置路由时,误将 POST 写成了 PEST: predicates: Method=PEST,可能会导致网关中所有路由失效,爆炸半径极大。

EDAS 建议为 Spring Cloud Gateway 应用配置 spring.cloud.gateway.fail-on-route-definition-error: false ,降低爆炸半径。通过 EDAS 创建的路由,将会经过校验,确保路由的格式正确,提前规避问题。

以上只是 EDAS 增强 Spring Cloud Gateway 方案的部分案例,EDAS 围绕性能、安全、稳定性等方面,全面为用户的网关保驾护航,让用户彻底回归到业务本身。

围绕让 Spring Cloud Gateway 生产可用这个基本话题,让用户在云上放心的使用 Spring Cloud Gateway,EDAS 推出了一个新的功能,使用无侵入式的方式增强 Spring Cloud Gateway。


SpringCloud Gateway 在微服务架构下的最佳实践

前言

本文整理自云原生技术实践营广州站 Meetup 的分享,其中的经验来自于我们团队开发的阿里云 CSB 2.0 这款产品,这是一款基于开源 SpringCloud Gateway 为 code base 开发的产品,在完全兼容开源用法的前提下,做了非常多企业级的改造,涉及功能特性、稳定性、安全、性能等方面。

为什么需要微服务网关

网关流量

从功能角度来看,微服务网关通常用来统一提供认证授权、限流、熔断、协议转换等功能。

从使用场景上来看

  • 南北向流量,需要流量网关和微服务网关配合使用,主要是为了区分外部流量和微服务流量,将内部的微服务能力,以统一的 HTTP 接入点对外提供服务
  • 东西向流量,在一些业务量比较大的系统中,可能会按照业务域隔离出一系列的微服务,在同一业务域内的微服务通信走的是服务发现机制,而跨业务域访问,则建议借助于微服务网关。

Kirito 全屋定制记 | 纯小白向全屋定制攻略

前言

继上一篇文章《Kirito 杭州买房记 | 纯小白向杭州购房攻略》过去已经有 2 年了,预计在今年 6~9 月,我的房子就要交付了,所以我也开始了全屋定制之路。现在杭州的期房大多数是精装修交付,也就是说厨房和卫生间等部分都包含在房价中,我需要操心的全屋定制部分仅包含:

柜子部分

  • 卧室衣柜
  • 书房书柜
  • 餐边柜
  • 电视柜

家具部分

  • 沙发
  • 茶几
  • 餐桌

最近一两个月,我跑遍了附近各个全屋定制的商家,从一个装修小白,成长为了一个略懂的小白,本文记录了我这段时间的积累,可以当做一份入门攻略。

本文主要介绍打柜子的部分,家具部分捎带提一下。

本文偏小白向,如果描述有偏差,欢迎指正,适合阅读人群:从未经历过但即将需要全屋定制,未亲自参与过全屋定制,关注了 Kirito 并好奇怎么发了一个技术无关的文章的读者。


记一次 Redis 连接问题排查

问题发现

客户端:业务应用使用 lettuce 客户端

服务端:Redis server 部署架构采用 1 主 + 1 从 + 3 哨兵

Redis 和业务应用部署在同一个 K8s 集群中,Redis Server 暴露了一个 redis-service,指向到 master 节点,业务应用通过 redis-service 连接 Redis。

某个时刻起,开始发现业务报错,稍加定位,发现是 Redis 访问出了问题,搜索业务应用日志,发现关键信息:

1
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: READONLY You can't write against a read only replica.

这是一个 Redis 访问的报错,看起来跟 Redis 的读写配置有关。


聊聊服务发现的推拉模型

前言

过去一年,我的工作重心投入到了 API 网关(阿里云 CSB)中,这对于我来说是一个新的领域,但和之前接触的微服务治理方向又密不可分。API 网关适配微服务场景需要完成一些基础能力的建设,其一便是对接注册中心,从而作为微服务的入口流量,例如 Zuul、SpringCloud Gateway 都实现了这样的功能。实际上很多开源网关在这一特性上均存在较大的局限性,本文暂不讨论这些局限性,而是针对服务发现这一通用的场景,分享我对它的一些思考。


浅析 Open API 设计规范

背景

最近由于业务需求,我负责的一块系统需要对外开放 Open API,原本不是什么难事,因为阿里云内部的 Open API 开放机制已经非常成熟了,根本不需要我去设计,但这次的需求主要是因为一些原因,需要自己设计一些规范,那就意味着,需要对 Open API 进行一些规范约束了,遂有此文。

Open API 和前端页面一样,一直都是产品的门面, Open API 不规范,会拉低产品的专业性。在云场景下,很多用户会选择自建门户,对接云产品的 Open API,这对我们提出的诉求便是构建一套成熟的 Open API 机制。

站在业务角度,有一些指导原则,指导我们完善 Open API 机制:

  • 前端页面使用的接口和 Open API 提供的接口是同一套接口
  • 任意的前端页面接口都应该有对应的 Open API

站在技术角度,有很多的 API 开放标准可供我们参考,一些开源产品的 Open API 文档也都非常完善。一方面,我会取其精华,另一方面,要考虑自身产品输出形态的特殊性。本文将围绕诸多因素,尝试探讨出一份合适的 Open API 开放规范。


构建多系统架构支持的 Docker 镜像

前言

陪伴了我 3 年的 Mac 在几个月前迎来了它的退休时刻,我将其置换成了公司新发的 Mac M1。对电子产品并不太感冒的我,并没有意识到 M1 是 ARM 架构的(除了个别软件的安装异常之外),显然,Mac M1 做的是不错的,我并没有太多吐槽它的机会。这也是我第一次近距离接触 ARM 架构的机会。

很快,在工作上,我遇到了第二次跟 ARM 打交道的机会。我们越来越多的客户,开始选择 ARM 架构的服务器作为 IaaS 层资源,这给我们的交付带来了一些工作量。适配工作中比较重要的一环便是 Docker 镜像,需要产出支持 ARM 架构的版本。

本文主要记录笔者在构建多系统架构支持的 Docker 镜像时的一些经验,以及一些个人的理解。


聊聊服务治理中的路由设计

前言

路由(Route)的设计广泛存在于众多领域,以 RPC 框架 Dubbo 为例,就有标签路由、脚本路由、权重路由、同机房路由等实现。

在框架设计层面,路由层往往位于负载均衡层之前,在进行选址时,路由完成的是 N 选 M(M <= N),而负载均衡完成的是 M 选一,共同影响选址逻辑,最后触发调用。

在业务层面,路由往往是为了实现一定的业务语义,对流量进行调度,所以服务治理框架通常提供的都是基础的路由扩展能力,使用者根据业务场景进行扩展。

路由过程

今天这篇文章将会围绕路由层该如何设计展开。


Guava Cache 使用小结

闲聊

话说原创文章已经断更 2 个月了,倒也不是因为忙,主要还是懒。但是也感觉可以拿出来跟大家分享的技术点越来越少了,一方面主要是最近在从事一些“内部项目”的研发,纵使我很想分享,也没法搬到公众号 & 博客上来;一方面是一些我并不是很擅长的技术点,在我还是新手时,我敢于去写,而有了一定工作年限之后,反而有些包袱了,我的读者会不会介意呢?思来想去,我回忆起了写作的初心,不就是为了记录自己的学习过程吗?于是乎,我还是按照我之前的文风记录下了此文,以避免成为一名断更的博主。

以下是正文。

前言

“缓存”一直是我们程序员聊的最多的那一类技术点,诸如 Redis、Encache、Guava Cache,你至少会听说过一个。需要承认的是,无论是面试八股文的风气,还是实际使用的频繁度,Redis 分布式缓存的确是当下最为流行的缓存技术,但同时,从我个人的项目经验来看,本地缓存也是非常常用的一个技术点。

分析 Redis 缓存的文章很多,例如 Redis 雪崩、Redis 过期机制等等,诸如此类的公众号标题不鲜出现在我朋友圈的 timeline 中,但是分析本地缓存的文章在我的映像中很少。

在最近的项目中,有一位新人同事使用了 Guava Cache 来对一个 RPC 接口的响应进行缓存,我在 review 其代码时恰好发现了一个不太合理的写法,遂有此文。

本文将会介绍 Guava Cache 的一些常用操作:基础 API 使用,过期策略,刷新策略。并且按照我的写作习惯,会附带上实际开发中的一些总结。需要事先说明的是,我没有阅读过 Guava Cache 的源码,对其的介绍仅仅是一些使用经验或者最佳实践,不会有过多深入的解析。

先简单介绍一下 Guava Cache,它是 Google 封装的基础工具包 guava 中的一个内存缓存模块,它主要提供了以下能力:

  • 封装了缓存与数据源交互的流程,使得开发更关注于业务操作
  • 提供线程安全的存取操作(可以类比 ConcurrentHashMap)
  • 提供常用的缓存过期策略,缓存刷新策略
  • 提供缓存命中率的监控

基础使用

使用一个示例介绍 Guava Cache 的基础使用方法 – 缓存大小写转换的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private String fetchValueFromServer(String key) {
return key.toUpperCase();
}

@Test
public void whenCacheMiss_thenFetchValueFromServer() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

assertEquals(0, cache.size());
assertEquals("HELLO", cache.getUnchecked("hello"));
assertEquals("HELLO", cache.get("hello"));
assertEquals(1, cache.size());
}

使用 Guava Cache 的好处已经跃然于纸上了,它解耦了缓存存取与业务操作。CacheLoaderload 方法可以理解为从数据源加载原始数据的入口,当调用 LoadingCache 的 getUnchecked 或者 get方法时,Guava Cache 行为如下:

  • 缓存未命中时,同步调用 load 接口,加载进缓存,返回缓存值
  • 缓存命中,直接返回缓存值
  • 多线程缓存未命中时,A 线程 load 时,会阻塞 B 线程的请求,直到缓存加载完毕

注意到,Guava 提供了两个 getUnchecked 或者 get 加载方法,没有太大的区别,无论使用哪一个,都需要注意,数据源无论是 RPC 接口的返回值还是数据库,都要考虑访问超时或者失败的情况,做好异常处理。

预加载缓存

预加载缓存的常见使用场景:

  • 老生常谈的秒杀场景,事先缓存预热,将热点商品加入缓存;
  • 系统重启过后,事先加载好缓存,避免真实请求击穿缓存

Guava Cache 提供了 putputAll 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void whenPreloadCache_thenPut() {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

String key = "kirito";
cache.put(key,fetchValueFromServer(key));

assertEquals(1, cache.size());
}

操作和 HashMap 一模一样。

这里有一个误区,而那位新人同事恰好踩到了,也是我写这篇文章的初衷,请务必仅在预加载缓存这个场景使用 put,其他任何场景都应该使用 load 去触发加载缓存。看下面这个反面示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意这是一个反面示例
@Test
public void wrong_usage_whenCacheMiss_thenPut() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return "";
}
});

String key = "kirito";
String cacheValue = cache.get(key);
if ("".equals(cacheValue)) {
cacheValue = fetchValueFromServer(key);
cache.put(key, cacheValue);
}
cache.put(key, cacheValue);

assertEquals(1, cache.size());
}

这样的写法,在 load 方法中设置了一个空值,后续通过手动 put + get 的方式使用缓存,这种习惯更像是在操作一个 HashMap,但并不推荐在 Cache 中使用。在前面介绍过 get 配合 load 是由 Guava Cache 去保障了线程安全,保障多个线程访问缓存时,第一个请求加载缓存的同时,阻塞后续请求,这样的 HashMap 用法既不优雅,在极端情况下还会引发缓存击穿、线程安全等问题。

请务必仅仅将 put 方法用作预加载缓存场景。

缓存过期

前面的介绍使用起来依旧没有脱离 ConcurrentHashMap 的范畴,Cache 与其的第一个区别在“缓存过期”这个场景可以被体现出来。本节介绍 Guava 一些常见的缓存过期行为及策略。

缓存固定数量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void whenReachMaxSize_thenEviction() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().maximumSize(3).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

cache.get("one");
cache.get("two");
cache.get("three");
cache.get("four");
assertEquals(3, cache.size());
assertNull(cache.getIfPresent("one"));
assertEquals("FOUR", cache.getIfPresent("four"));
}

使用 ConcurrentHashMap 做缓存的一个最大的问题,便是我们没有简易有效的手段阻止其无限增长,而 Guava Cache 可以通过初始化 LoadingCache 的过程,配置 maximumSize ,以确保缓存内容不导致你的系统出现 OOM。

值得注意的是,我这里的测试用例使用的是除了 getgetUnchecked 外的第三种获取缓存的方式,如字面意思描述的那样,getIfPresent 在缓存不存在时,并不会触发 load 方法加载数据源。

LRU 过期策略

依旧沿用上述的示例,我们在设置容量为 3 时,仅获悉 LoadingCache 可以存储 3 个值,却并未得知第 4 个值存入后,哪一个旧值需要淘汰,为新值腾出空位。实际上,Guava Cache 默认采取了 LRU 缓存淘汰策略。Least Recently Used 即最近最少使用,这个算法你可能没有实现过,但一定会听说过,在 Guava Cache 中 Used 的语义代表任意一次访问,例如 put、get。继续看下面的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void whenReachMaxSize_thenEviction() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().maximumSize(3).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

cache.get("one");
cache.get("two");
cache.get("three");
// access one
cache.get("one");
cache.get("four");
assertEquals(3, cache.size());
assertNull(cache.getIfPresent("two"));
assertEquals("ONE", cache.getIfPresent("one"));
}

注意此示例与上一节示例的区别:第四次 get 访问 one 后,two 变成了最久未被使用的值,当第四个值 four 存入后,淘汰的对象变成了 two,而不再是 one 了。

缓存固定时间

为缓存设置过期时间,也是区分 HashMap 和 Cache 的一个重要特性。Guava Cache 提供了expireAfterAccessexpireAfterWrite 的方案,为 LoadingCache 中的缓存值设置过期时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void whenEntryIdle_thenEviction()
throws InterruptedException, ExecutionException {

LoadingCache<String, String> cache =
CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

cache.get("kirito");
assertEquals(1, cache.size());

cache.get("kirito");
Thread.sleep(2000);

assertNull(cache.getIfPresent("kirito"));
}

缓存失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void whenInvalidate_thenGetNull() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

String name = cache.get("kirito");
assertEquals("KIRITO", name);

cache.invalidate("kirito");
assertNull(cache.getIfPresent("kirito"));
}

使用 void invalidate(Object key) 移除单个缓存,使用 void invalidateAll() 移除所有缓存。

缓存刷新

缓存刷新的常用于使用数据源的新值覆盖缓存旧值,Guava Cache 提供了两类刷新机制:手动刷新和定时刷新。

手动刷新

1
cache.refresh("kirito");

refresh 方法将会触发 load 逻辑,尝试从数据源加载缓存。

需要注意点的是,refresh 方法并不会阻塞 get 方法,所以在 refresh 期间,旧的缓存值依旧会被访问到,直到 load 完毕,看下面的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Test
public void whenCacheRefresh_thenLoad()
throws InterruptedException, ExecutionException {

LoadingCache<String, String> cache =
CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws InterruptedException {
Thread.sleep(2000);
return key + ThreadLocalRandom.current().nextInt(100);
}
});

String oldValue = cache.get("kirito");

new Thread(() -> {
cache.refresh("kirito");
}).start();

// make sure another refresh thread is scheduling
Thread.sleep(500);

String val1 = cache.get("kirito");

assertEquals(oldValue, val1);

// make sure refresh cache
Thread.sleep(2000);

String val2 = cache.get("kirito");
assertNotEquals(oldValue, val2);

}

其实任何情况下,缓存值都有可能和数据源出现不一致,业务层面需要做好访问到旧值的容错逻辑。

自动刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void whenTTL_thenRefresh() throws ExecutionException, InterruptedException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key + ThreadLocalRandom.current().nextInt(100);
}
});

String first = cache.get("kirito");
Thread.sleep(1000);
String second = cache.get("kirito");

assertNotEquals(first, second);
}

和上节的 refresh 机制一样,refreshAfterWrite 同样不会阻塞 get 线程,依旧有访问旧值的可能性。

缓存命中统计

Guava Cache 默认情况不会对命中情况进行统计,需要在构建 CacheBuilder 时显式配置 recordStats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void whenRecordStats_thenPrint() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().maximumSize(100).recordStats().build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

cache.get("one");
cache.get("two");
cache.get("three");
cache.get("four");

cache.get("one");
cache.get("four");

CacheStats stats = cache.stats();
System.out.println(stats);
}
---
CacheStats{hitCount=2, missCount=4, loadSuccessCount=4, loadExceptionCount=0, totalLoadTime=1184001, evictionCount=0}

缓存移除的通知机制

在一些业务场景中,我们希望对缓存失效进行一些监测,或者是针对失效的缓存做一些回调处理,就可以使用 RemovalNotification 机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void whenRemoval_thenNotify() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().maximumSize(3)
.removalListener(
cacheItem -> System.out.println(cacheItem + " is removed, cause by " + cacheItem.getCause()))
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});

cache.get("one");
cache.get("two");
cache.get("three");
cache.get("four");
}
---
one=ONE is removed, cause by SIZE

removalListener 可以给 LoadingCache 增加一个回调处理器,RemovalNotification 实例包含了缓存的键值对以及移除原因。

Weak Keys & Soft Values

Java 基础中的弱引用和软引用的概念相信大家都学习过,这里先给大家复习一下

  • 软引用:如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足,就会回收这些对象。只要垃圾回收器没有回收它,该对象就可以被程序使用
  • 弱引用:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

在 Guava Cache 中,CacheBuilder 提供了 weakKeys、weakValues、softValues 三种方法,将缓存的键值对与 JVM 垃圾回收机制产生关联。

该操作可能有它适用的场景,例如最大限度的使用 JVM 内存做缓存,但依赖 GC 清理,性能可想而知会比较低。总之我是不会依赖 JVM 的机制来清理缓存的,所以这个特性我不敢使用,线上还是稳定性第一。

如果需要设置清理策略,可以参考缓存过期小结中的介绍固定数量和固定时间两个方案,结合使用确保使用缓存获得高性能的同时,不把内存打挂。

总结

本文介绍了 Guava Cache 一些常用的 API 、用法示例,以及需要警惕的一些使用误区。

在选择使用 Guava 时,我一般会结合实际使用场景,做出以下的考虑:

  1. 为什么不用 Redis?

    如果本地缓存能够解决,我不希望额外引入一个中间件。

  2. 如果保证缓存和数据源数据的一致性?

    一种情况,我会在数据要求敏感度不高的场景使用缓存,所以短暂的不一致可以忍受;另外一些情况,我会在设置定期刷新缓存以及手动刷新缓存的机制。举个例子,页面上有一个显示应用 developer 列表的功能,而本地仅存储了应用名,developer 列表是通过一个 RPC 接口查询获取的,而由于对方的限制,该接口 qps 承受能力非常低,便可以考虑缓存 developer 列表,并配置 maximumSize 以及 expireAfterAccess。如果有用户在 developer 数据源中新增了数据,导致了数据不一致,页面也可以设置一个同步按钮,让用户去主动 refresh;或者,如果判断当前用户不在 developer 列表,也可以程序 refresh 一次。总之非常灵活,使用 Guava Cache 的 API 可以满足大多数业务场景的缓存需求。

  3. 为什么是 Guava Cache,它的性能怎么样?

    我现在主要是出于稳定性考虑,项目一直在使用 Guava Cache。据说有比 Guava Cache 快的本地缓存,但那点性能我的系统不是特别关心。


Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×