数据存储与缓存

在 Vert.x 应用中使用 Caffeine 的 AsyncLoadingCache

本文档将向你展示如何在 Vert.x 应用程序中使用 Caffeine 提供的 AsyncLoadingCache 异步加载缓存。

你将构建什么

你将构建一个在网页中轮播切换猫咪图片的应用程序。

图片将由服务端从公开 API https://http.cat 下载。

每张图片代表一个以 🐱 形象呈现的 HTTP 状态码。

准备好了吗?

100
图片资源由 Tomomi Imura (@girlie_mac) 创作。

前置要求

  • 文本编辑器或 IDE

  • Java 11 或更高版本

  • Maven 或 Gradle

创建工程项目

本项目代码同时提供了功能完全等价的 Maven 与 Gradle 构建配置。

使用 Maven

这是你应当使用的 pom.xml 文件内容:

使用 Gradle

假设你使用基于 Kotlin DSL 的 Gradle 构建,这是你的 build.gradle.kts 文件内容:

网页端实现

index.html 网页主体主要由以下部分构成:

  1. 页面 body 中的一个 <img> 标签;

  2. 一个在页面加载完成后动态更改图片 src 属性的脚本。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Rotating Cats Page</title>
  <script type="application/javascript">
    window.onload = function () { (2)
      const codes = [ (3)
        100,
        200, 201, 204, 206,
        301, 302, 303, 304, 307, 308,
        401, 403, 404, 406, 407, 409, 410, 412, 416, 418, 425, 451,
        500, 501, 502, 503, 504,
      ]
      setInterval(function () { (4)
        let img = document.getElementById("cat-img"); (5)
        img.src = "/api/cats/" + codes[Math.floor(Math.random() * codes.length)]; (6)
      }, 250)
    }
  </script>
</head>
<body>
<div>
  <img src="/api/cats/200" id="cat-img" alt="Cat image"/> (1)
</div>
</body>
</html>
1 body 中的 img 标签,id 设置为 cat-img
2 当页面加载完成时触发执行脚本函数
3 定义一组候选 HTTP 状态码
4 安排一个以 250 毫秒固定间隔周期性执行的调度函数
5 通过元素 id 从 DOM 中获取 img 元素
6 随机选取一个 HTTP 状态码并更新 src 属性

服务端实现

我们需要一个 Vert.x Web Client 实例来远程获取图片:

Web Client 初始化
    request = WebClient.create(vertx)
      .request(GET, new RequestOptions().setHost("http.cat").setPort(443).setSsl(true))
      .as(BodyCodec.buffer());

我们还需要一个缓存,以避免对后端 API 造成过载:

Caffeine 缓存初始化
    cache = Caffeine.newBuilder() (1)
      .expireAfterWrite(Duration.ofMinutes(1)) (2)
      .recordStats() (3)
      .executor(cmd -> context.runOnContext(v -> cmd.run())) (4)
      .buildAsync((key, exec) -> CompletableFuture.supplyAsync(() -> { (5)
        Future<Buffer> future = fetchCatImage(key); (6)
        return future.toCompletionStage(); (7)
      }, exec).thenComposeAsync(Function.identity(), exec));

    vertx.setPeriodic(20000, l -> { (8)
      CacheStats stats = cache.synchronous().stats();
      log.info("Stats: " + stats);
    });
1 创建缓存构建器 Builder
2 配置缓存条目在写入 1 分钟后过期
3 启用统计信息指标收集
4 定义一个在当前 Verticle 上下文中触发执行任务的调度器 Executor
5 利用缓存调度器创建异步加载器,该加载器必须返回一个 CompletableFuture
6 远程拉取猫咪图片
7 将 Vert.x 的 Future 转换为 Java 标准的 CompletionStage
8 周期性打印缓存统计信息

在这里,自定义执行器(Executor)定义与相对复杂的加载器实现并非绝对必需。

事实上,当我们仅部署单个 Verticle 实例、将缓存绑定到私有成员字段、且始终在 Event Loop 事件循环中调用缓存方法时,你可以将其极简化为:

    cache = Caffeine.newBuilder()
      .expireAfterWrite(Duration.ofMinutes(1))
      .recordStats()
      .buildAsync((key, exec) -> {
        Future<Buffer> future = fetchCatImage(key);
        return future.toCompletionStage().toCompletableFuture();
      });

然而,如果你计划部署多个 Verticle 实例并在它们之间共享该缓存,请坚持使用前面的完整实现。它能确保异步加载器始终在正确的 Verticle 执行上下文中被调用。

获取猫咪图片的具体逻辑是:将对应的 HTTP 状态码作为请求 URI 发送给后端:

拉取图片
  private Future<Buffer> fetchCatImage(int code) {
    return request.uri("/" + code)
      .send()
      .expecting(HttpResponseExpectation.SC_OK)
      .map(HttpResponse::body);
  }

借助 Vert.x Web,为我们的 API 以及静态资源文件搭建 HTTP 服务器非常直观:

服务器配置
    Router router = Router.router(vertx);
    router.get("/api/cats/:id").produces("image/*").handler(this::handleImageRequest);
    router.get().handler(StaticHandler.create());

    return vertx.createHttpServer()
      .requestHandler(router)
      .listen(8080)
      .onSuccess(v -> log.info("Server started on port 8080"));

这是处理图片请求的具体实现方式:

处理图片请求
  private void handleImageRequest(RoutingContext rc) {
    Integer code = Integer.valueOf(rc.pathParam("id")); (1)
    CompletableFuture<Buffer> completableFuture = cache.get(code); (2)
    Future<Buffer> future = Future.fromCompletionStage(completableFuture, context); (3)
    future.onComplete(ar -> { (4)
      if (ar.succeeded()) {
        rc.response()
          .putHeader("Cache-Control", "no-store") (5)
          .end(ar.result());
      } else {
        rc.fail(ar.cause());
      }
    });
  }
1 从请求路径中提取指定的状态码
2 调用 Caffeine 缓存(如果缓存未命中,将透明地从后端加载图片)
3 将 Caffeine 返回的 CompletableFuture 转换回 Vert.x 的 Future
4 异步完成后,将图片二进制字节(或错误信息)响应给客户端
5 通知浏览器禁用对该图片的本地缓存(否则浏览器针对同一状态码只会向服务端发起一次真实请求!)

运行应用程序

CatsVerticle 需要一个 main 入口方法:

Main 入口方法
  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx(); (1)
    vertx.deployVerticle(new CatsVerticle()).await(); (2)
  }
1 创建 Vert.x 实例
2 部署 CatsVerticle

你可以通过以下方式启动应用程序:

  1. 在你的 IDE 中直接运行 CatsVerticle 类中的 main 方法;

  2. 或通过 Maven 运行:mvn compile exec:java;

  3. 或通过 Gradle 运行:./gradlew run(Linux, macOS)或 gradlew run(Windows)。

在浏览器中打开 http://localhost:8080。

你将看到页面中猫咪图片在快速轮播切换:

cats

运行一段时间后,查看控制台输出。 你应该会看到类似以下的日志:

Mar 22, 2022 3:45:17 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=52, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:45:37 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=132, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:45:57 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=212, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:46:17 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=267, missCount=53, loadSuccessCount=52, loadFailureCount=0, totalLoadTime=3337599348, evictionCount=28, evictionWeight=28}
Mar 22, 2022 3:46:37 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=344, missCount=56, loadSuccessCount=56, loadFailureCount=0, totalLoadTime=3480880213, evictionCount=28, evictionWeight=28}

观察 hitCount(命中数)、missCount(未命中数)和 evictionCount(驱逐数)随时间的动态变化。

总结

本文档涵盖了以下内容:

  1. 使用 Vert.x Web Client 发起异步 HTTP 请求;

  2. 使用 Vert.x Web 路由器与 HTTP 服务端;

  3. 在 Vert.x 应用中整合 Caffeine 异步加载缓存(AsyncLoadingCache);

  4. Vert.x 定时周期任务的使用。