1. 概述
在编程中,优化代码性能是关键,特别是在处理昂贵操作或数据检索过程时。一个有效的性能提升方法是使用缓存。Project Reactor
库提供了cache()
方法,用于缓存那些几乎不会改变的昂贵操作或数据,以避免重复操作并提高效率。
本教程将探讨记忆化(memoization),一种缓存形式,并展示如何使用Project Reactor
库的Mono.cache()
来缓存来自JSONPlaceholder API的HTTP GET请求结果。同时,我们将通过大理石图理解Mono.cache()
方法的内部工作原理。
2. 记忆化理解
记忆化是一种缓存机制,它存储昂贵函数调用的结果。当相同的函数再次被调用时,返回缓存的结果。
在涉及递归函数(/java-recursion)或对于给定输入始终产生相同输出的计算时,它非常有用。
让我们看一个Java示例,使用斐波那契数列来演示记忆化。首先,创建一个Map
对象来存储结果:
private static final Map<Integer, Long> cache = new HashMap<>();
接下来,定义一个计算斐波那契序列的方法:
long fibonacci(int n) {
if (n <= 1) {
return n;
}
if (cache.containsKey(n)) {
return cache.get(n);
}
long result = fibonacci(n - 1) + fibonacci(n - 2);
logger.info("First occurrence of " + n);
cache.put(n, result);
return result;
}
在这段代码中,我们在进一步计算之前检查整数n
是否已存储在Map
对象中。如果已存在,我们返回缓存的值;否则,我们递归计算结果并将其存储在Map
对象中供后续使用。
这个方法通过避免冗余计算显著提高了斐波那契计算的性能。
让我们为方法编写一个单元测试:
@Test
void givenFibonacciNumber_whenFirstOccurenceIsCache_thenReturnCacheResultOnSecondCall() {
assertEquals(5, FibonacciMemoization.fibonacci(5));
assertEquals(2, FibonacciMemoization.fibonacci(3));
assertEquals(55, FibonacciMemoization.fibonacci(10));
assertEquals(21, FibonacciMemoization.fibonacci(8));
}
在测试中,我们调用fibonacci()
来计算序列。
3. 描述Mono.cache()
的大理石图
Mono.cache()
操作器帮助缓存Mono
发布者的结果,并在后续订阅时返回缓存的值。
大理石图有助于理解反应式类的内部细节以及它们的工作方式。以下是一个说明cache()
操作行为的大理石图:
在上图中,对Mono
发布者的首次订阅会发出数据并将其缓存。后续订阅会检索缓存的数据,而不会触发新的计算或数据获取。
4. 示例设置
为了演示Mono.cache()
的用法,我们需要在pom.xml
中添加reactor-core
:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.5</version>
</dependency>
该库提供了Mono
、Flux
等操作符,用于在Java中实现反应式编程。
同时,我们也需要添加spring-boot-starter-webflux
到pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.2.5</version>
</dependency>
这个依赖提供了WebClient
类来消费API。
此外,让我们看一下GET请求到https://jsonplaceholder.typicode.com/users/2
时的样本响应:
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette"
// ...
}
接下来,创建一个名为User
的POJO类,用于解析从GET请求接收到的JSON响应:
public class User {
private int id;
private String name;
// standard constructor, getter and setter
}
然后,创建一个WebClient
对象并设置API的基URL:
WebClient client = WebClient.create("https://jsonplaceholder.typicode.com/users");
这将作为使用cache()
方法缓存HTTP响应的基础URL。
最后,创建一个AtomicInteger
对象:
AtomicInteger counter = new AtomicInteger(0);
这个对象用于跟踪我们向API发起GET请求的次数。
5. 不使用记忆化的数据获取
首先,定义一个从WebClient
对象获取用户的方法:
Mono<User> retrieveOneUser(int id) {
return client.get()
.uri("/{id}", id)
.retrieve()
.bodyToMono(User.class)
.doOnSubscribe(i -> counter.incrementAndGet())
.onErrorResume(Mono::error);
}
在这段代码中,我们根据特定ID获取用户,并将响应体映射到User
对象。每次订阅时,我们还会增加计数器。
这里有一个不使用缓存获取用户的测试案例:
@Test
void givenRetrievedUser_whenTheCallToRemoteIsNotCache_thenReturnInvocationCountAndCompareResult() {
MemoizationWithMonoCache memoizationWithMonoCache = new MemoizationWithMonoCache();
Mono<User> retrieveOneUser = MemoizationWithMonoCache.retrieveOneUser(1);
AtomicReference<User> firstUser = new AtomicReference<>();
AtomicReference<User> secondUser = new AtomicReference<>();
Disposable firstUserCall = retrieveOneUser.map(user -> {
firstUser.set(user);
return user.getName();
})
.subscribe();
Disposable secondUserCall = retrieveOneUser.map(user -> {
secondUser.set(user);
return user.getName();
})
.subscribe();
assertEquals(2, memoizationWithMonoCache.getCounter());
assertEquals(firstUser.get(), secondUser.get());
}
这里,我们两次订阅retrieveOneUser
Mono
,每次订阅都会触发对WebClient
对象的单独GET请求。我们断言计数器会增加两次。
6. 使用记忆化的数据获取
现在,我们修改先前的示例,利用Mono.cache()
并缓存第一次GET请求的结果:
@Test
void givenRetrievedUser_whenTheCallToRemoteIsCache_thenReturnInvocationCountAndCompareResult() {
MemoizationWithMonoCache memoizationWithMonoCache = new MemoizationWithMonoCache();
Mono<User> retrieveOneUser = MemoizationWithMonoCache.retrieveOneUser(1).cache();
AtomicReference<User> firstUser = new AtomicReference<>();
AtomicReference<User> secondUser = new AtomicReference<>();
Disposable firstUserCall = retrieveOneUser.map(user -> {
firstUser.set(user);
return user.getName();
})
.subscribe();
Disposable secondUserCall = retrieveOneUser.map(user -> {
secondUser.set(user);
return user.getName();
})
.subscribe();
assertEquals(1, memoizationWithMonoCache.getCounter());
assertEquals(firstUser.get(), secondUser.get());
}
与前一个示例的主要区别在于,我们在订阅retrieveOneUser
对象之前调用了cache()
操作。这将缓存第一次GET请求的结果,后续订阅将接收缓存的结果,而不是触发新的请求。
在测试案例中,我们断言计数器只增加一次,因为第二次订阅使用了缓存值。
7. 设置缓存持续时间
默认情况下,Mono.Cache()
无限期地缓存结果。但在数据可能随着时间而过时的情况下,设置缓存时长至关重要:
// ...
Mono<User> retrieveOneUser = memoizationWithMonoCache.retrieveOneUser(1)
.cache(Duration.ofMinutes(5));
// ...
在代码中,cache()
方法接受一个Duration
实例作为参数。缓存的值将在5分钟后过期,之后的任何订阅都将触发新的GET请求。
8. 总结
在这篇文章中,我们学习了记忆化的基本概念以及如何在Java中使用斐波那契数列示例实现。然后,我们深入探讨了Project Reactor
库中的Mono.cache()
的用法,并展示了如何缓存HTTP GET请求的结果。
缓存是提高性能的强大工具。然而,确保过时数据不会无限期提供至关重要,需要考虑缓存失效策略。
如往常一样,示例的完整源代码可在GitHub上找到。