在 Spring 中使用 Reactor Mono.cache()优化性能

Memoization 是一种优化技术,用于通过存储昂贵的函数调用的结果并在再次出现相同的输入时重用缓存的结果来加速应用程序。在反应式编程的上下文中,记忆化通过缓存结果来帮助避免重复执行昂贵的操作。让我们深入研究一下如何将 Spring Reactor Mono 用作缓存。

Reactor 中的 Mono.cache() 允许您缓存 Mono 的结果并将其重放给后续订阅者。这在处理网络调用或数据库查询等昂贵的操作时特别有用。

(Expensive operation) --Mono--> (cache) --Mono--> (result)

在此图中,第一个订阅者触发昂贵的操作,并且其结果被缓存。后续订阅者会收到缓存的结果,而不会再次触发昂贵的操作。

Mono.cache 优点

性能优化:缓存减少了对冗余计算或数据获取操作的需求,从而缩短了响应时间。缓存的 Mono 的后续订阅者可以重用缓存的结果,从而避免重复操作的开销。

资源优化:通过避免重复调用相同的数据来减少后端服务或数据库的负载。通过最大限度地减少请求数量来优化网络使用。

一致性:确保多个订阅者在缓存持续时间内获得一致的结果,因为它们都接收相同的缓存值。

可控性:允许对缓存持续时间进行精细控制,确保根据需要定期刷新数据。

Mono.cache 使用场景

昂贵的计算:在执行计算成本高昂的操作时,缓存结果可以显著减少后续请求的处理时间。比如:复杂数据转换、机器学习模型推理。

频繁的数据访问:当数据被多个使用者频繁访问但不经常更改时。比如:会话中的配置设置、用户配置文件数据。

API 速率限制:在处理具有速率限制的外部 API 时,缓存响应有助于保持在这些限制范围内。比如:API 调用数量受限的第三方服务集成。

静态数据:静态或不经常更改的数据可以从缓存中受益,以避免不必要的 fetch 操作。比如:国家/地区列表、网页的静态内容。

数据库查询结果:缓存在执行时间或资源使用方面成本高昂的数据库查询的结果。比如:聚合的分析数据、报告数据。

外部服务调用:当调用延迟可能较高的外部服务时,缓存响应可以提高应用程序的整体响应能力。比如:地理位置服务、货币兑换率。

Mono.cache 代码案例

  1. 创建Spring Boot应用,添加依赖,比如:
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.1</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>
 
<properties>
    <java.version>11</java.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2. 创建DataService,包含 fetchData 方法的数据服务类,通过使用 delayElement(...) 方法将数据的发出延迟 2 秒来模拟成本高昂的操作。Mono.fromSupplier 方法用于创建在订阅时发出数据的 Mono

@Service
public class DataService {
 
    public Mono<String> fetchData() {
        return Mono.fromSupplier(() -> {
            System.out.println("Fetching data from service...");
            return "Data from service";
        }).delayElement(Duration.ofSeconds(2));
    }
}

3. 创建Controller,实现以下接口

  • /data-no-cache:获取数据而不进行记忆化。每个请求都将触发 fetchData 方法,模拟 2 秒的延迟。
  • /data-cache:使用 Mono.cache() 通过记忆获取数据。第一个请求将触发 fetchData 方法,后续请求将返回缓存的结果。
  • /data-cache-duration:使用 Mono.cache(Duration.ofSeconds(10)) .第一个请求会触发 fetchData 方法,后续 10 秒内的请求会返回缓存的结果。10 秒后,缓存将过期,并将触发新的获取操作。
@RestController
public class DataController {
 
    private final DataService dataService;
    private final Mono<String> cachedData;
    private final Mono<String> cachedDataWithDuration;
 
    public DataController(DataService dataService) {
        this.dataService = dataService;
        this.cachedData = dataService.fetchData().cache();
        this.cachedDataWithDuration = dataService.fetchData().cache(Duration.ofSeconds(10));
    }
 
    @GetMapping("/data-no-cache")
    public Mono<String> getDataNoCache() {
        return dataService.fetchData();
    }
 
    @GetMapping("/data-cache")
    public Mono<String> getDataWithCache() {
        return cachedData;
    }
 
    @GetMapping("/data-cache-duration")
    public Mono<String> getDataWithCacheDuration() {
        return cachedDataWithDuration;
    }
}

4. 创建应用主类

@SpringBootApplication
public class MemoizationExampleApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(MemoizationExampleApplication.class, args);
    }
}

5. 测试

使用 mvn spring-boot:run 命令运行应用程序时,打开浏览器访问上面定义的接口。可以得到以下几个情况:

  • 对 /data-no-cache 的每个请求都会触发 fetchData 方法,从而导致每个请求延迟 2 秒
  • 对 /data-cache 的第一个请求会触发 fetchData 方法,后续请求会毫不延迟地返回缓存的结果。缓存日志在 Mono 中是隐式的。
  • 对 /data-cache-duration 的第一个请求将触发 fetchData 方法。后续 10 秒内的请求将返回缓存结果。10 秒后,缓存将过期,并将触发新的获取操作。缓存日志在 Mono 中是隐式的。

总结

在 Reactor 中使用 Mono.cache() 是一种通过记住昂贵操作的结果来优化反应式应用程序的强大方法。这可确保成本高昂的计算或 I/O 操作仅执行一次,从而提高性能和效率。

请登录后发表评论

    没有回复内容