介绍新的 Redis API - 如何使用 Redis 进行缓存?

在 Quarkus 2.11 中,我们引入了一个新的 Redis 交互 API。Redis 数据源 API 旨在更简单、更广泛、类型安全。底层采用高性能的非阻塞客户端(如果您喜欢低级 API,也可以使用它)。

在本篇博文中,我们将介绍这个新 API,并使用它构建一个缓存,这是 Redis 的主要用例之一。

什么是 Redis?

Redis 是一个开源的内存数据存储,可用作数据库、缓存、流引擎和消息代理。Redis 常用于实时数据存储、缓存后端、地理空间实体的数据存储等。要与 Redis 交互,您需要发出命令并接收响应。这些命令以为目标并操作相关数据。有许多命令分为几组,包括

  • 用于操作位向量的 BitMap 组

  • 用于操作键的 Generic 组

  • 用于操作地理位置项的 Geospatial 组

  • 用于操作field -> item对集合(如 Java 中的Map)的 Hash 组

  • 用于存储项的列表、集合和有序集合的 List、Set 和 Sorted Set 组

  • 用于在频道上发送消息并接收消息的 Pub/Sub 组

  • 用于操作值的 String 组(在 Redis 中,Strings 表示值,包括二进制、数字等)

  • 用于执行事务的 Transaction 组

您可以在 Redis 命令页面 上找到完整的命令列表。

新的 Quarkus Redis API

新的 Quarkus Redis API 的入口点是两个数据源接口

  • io.quarkus.redis.datasource.RedisDataSource - 命令式(阻塞)API

  • io.quarkus.redis.datasource.ReactiveRedisDataSource - 响应式 API

如上所述,这些 API 是在低级客户端之上实现的

redis clients

数据源 API 遵循命令组结构。对于每个组,您都可以检索一个专门用于发出该组命令的对象。在这方面,这个新 API 并不是对 Redis 的抽象。您仍然需要知道您需要的命令。

例如,要操作一个Set<Person>,您将使用以下代码

record Person(String firstName, String lastName) {}

@ApplicationScoped
class PersonService {

    private final SetCommands<String, Person> commands;

    public PersonService(RedisDataSource ds) {
	    // Retrieve the `set` group
        commands = ds.set(Person.class);
    }

    public void add(Person person) {
	    // Emit the `sadd` command
        commands.sadd("key", person);
    }
}

API 会为您处理序列化和反序列化。目前,它使用 JSON(通过 Jackson)处理对象,但很快 API 将提供更高级的功能。

此示例使用命令式 API,但响应式 API 对称。

实现 Redis 缓存

是时候编写更连贯的代码了。让我们想象一下以下的 应用程序

application

我们有一个存储Heroes的数据库,数量非常多。您需要根据等级返回最强大的 3 位英雄。当然,您可以使用您的 SQL 达人技能,但让我们想象一下,这段代码写于很久以前,无法更改,并且非常耗时。

// Dumb approach, don't do this
return new Ranking(Hero.<Hero>listAll()
	.stream()
	.sorted((o1, o2) -> Integer.compare(o2.level, o1.level))
	.peek(h -> {
		// do something very long...
		nap();
	})
	.limit(3)
	.collect(Collectors.toList()));

因此,为了避免每次调用都重新计算这组英雄,一种解决方案是将结果缓存一段时间,比如说 10 秒。在这种情况下,可以接受返回可能过时的结果集。

要使用新的 Redis API,我们需要使用 `redis-client` 扩展。对于旧 API 的用户来说,这是同一个扩展。旧 API 仍然可用,但已被弃用,我们计划在某个时候将其删除。

现在我们可以使用 `RedisDataSource` 了,我们可以按如下方式实现 MyRedisCache

package me.escoffier.quarkus.supes;

import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.string.SetArgs;
import io.quarkus.redis.datasource.string.StringCommands;

import javax.enterprise.context.ApplicationScoped;
import java.time.Duration;
import java.util.function.Supplier;

@ApplicationScoped
public class MyRedisCache {

    private final StringCommands<String, Ranking> commands;

    public MyRedisCache(RedisDataSource ds) {
        this.commands = ds.string(Ranking.class);
    }

    public Ranking get(String key) {
        return commands.get(key);
    }

    public void set(String key, Ranking result) {
        commands.set(key, result, new SetArgs().ex(Duration.ofSeconds(10)));
    }

    public void evict(String key) {
        commands.getdel(key);
    }

    public Ranking getOrSetIfAbsent(String key,
           Supplier<Ranking> computation) {
        var cached = get(key);
        if (cached != null) {
            return cached;
        } else {
            var result = computation.get();
            set(key, result);
            return result;
        }
    }
}

请注意,这是一个简单的缓存,没有花哨的功能。Redis 提供了更高级的命令来实现更复杂的策略。

构造函数接收 `RedisDataSource` 并获取一个用于操作 Redis的对象。在我们的例子中,是 Ranking(排名前 3 的英雄)

`get` 方法发出 Redis `get` 命令以检索已存储的 `Ranking`(如果不存在,则为 `null`)。

`set` 方法发出 Redis `set` 命令并将 `Ranking` 存储到传入的键中。该命令还配置了过期时间。因此,10 秒后,该值将由 Redis 删除。如上所述,Ranking 实例被序列化为 JSON 文档。

`evict` 方法允许删除存储的值。多个命令可以做到这一点,例如 `del` 或 `getdel`(它也返回存储的值)。

对于我们的应用程序,我们需要更高级一些的功能。我们希望检查 Redis 中是否存在某个值。如果存在,则使用该值;如果不存在,则计算该值并存储它。这在 `getOrSetIfAbsent` 中实现。

现在,我们可以使用这个缓存来避免每次调用都进行繁重的计算(查看 HeroService 类以查看完整代码)

@Inject
MyRedisCache cache;

public Ranking getTopHeroes() {
    return cache.getOrSetIfAbsent("top", () -> {
                // Dumb approach, don't do this
                return new Ranking(Hero.<Hero>listAll()
                        .stream()
                        .sorted((o1, o2) -> Integer.compare(o2.level, o1.level))
                        .peek(h -> {
                            // do something very long...
                            nap();
                        })
                        .limit(3)
                        .collect(Collectors.toList()));
            });
}

要运行应用程序,只需启动 `mvn quarkus:dev` 并在浏览器中打开 https://:8080

screenshot

要查看缓存的运行情况,请查看页面上显示的时间并刷新页面。请记住,缓存的值仅在 10 秒内有效(如 `MyRedisCache` 中设置的)。因此,如果您等待 10 秒,它将重新计算结果。

Quarkus 附带了一个 Redis 开发服务,它会在您的计算机上自动启动一个 Redis 实例并配置应用程序。请注意,您需要能够本地运行容器才能使用此功能。

结论

这篇博文简要介绍了新的 Redis API,并通过缓存实现示例演示了其用法。完整代码可在以下 GitHub 仓库 中找到。

该 API 支持更多功能,如地理空间数据、发布/订阅和事务,这些功能可用于改进 `getOrSetIfAbsent` 方法。我们将在未来的博文中介绍更高级的用例。

您可以在以下位置找到有关新 API 的更多详细信息:

此外,Quarkus 团队正在致力于将 Redis 作为缓存实现。因此,最终您只需使用 `@CacheResult` 即可实现自动缓存。