likes
comments
collection
share

Spring Boot「18」Lettuce 原生 API 与 RedisTemplate 对比

作者站长头像
站长
· 阅读数 20

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 02 天,点击查看活动详情

Spring Boot 中默认使用 Lettuce 作为 redis-cli 的底层实现。 Lettuce 基于 Netty 实现,提供了相对丰富地同步、异步操作接口。 今天我们将主要对比一下 Lettuce 原生 API 与 spring-boot-data-redis 对其封装后的接口(主要是 RedisTemplate)的使用。

01-Lettuce 原生 API

Lettuce 暴露给用户的接口是非常直观的,针对单实例、集群场景,提供了两个 client 接口:

  • io.lettuce.core.RedisClient
  • io.lettuce.core.cluster.RedisClusterClient

它们都继承自 io.lettuce.core.AbstractRedisClient,封装了 Netty、ClientOptions(控制 client 行为,例如重连、超时等)和连接过程的实现。

RedisClient 对象的创建是非常简单的:

final RedisURI uri = RedisURI.builder()
                .withPassword("redis123")
                .withHost("example.samson.self")
                .withPort(6379)
                .withDatabase(0)
                .build();
final RedisClient redisClient = RedisClient.create(uri);
/**
 * 上述 uri 对象等价于:
 * redis://redis123@example.samson.self:6379/0
 *
 */

创建了 RedisClient 实例后,可以通过 RedisClient#connect() 方法获得 StatefulRedisConnection 连接对象。 获得连接对象后,可以通过如下接口进行同步、异步及响应式命令的执行:

final StatefulRedisConnection<String, String> connection = client.connect();
final RedisCommands<String, String> sync = connection.sync();   // 同步操作
final RedisAsyncCommands<String, String> async = connection.async(); // 异步操作
final RedisReactiveCommands<String, String> reactive = connection.reactive(); // 响应式操作

下文以同步操作为例,演示下通过 Lettuce 原生 API 对 Redis 基本数据结构的操作。

// 字符串
sync.set("str", "str_value");
final String strValue = sync.get("str");
System.out.println("strValue: " + strValue);

// 列表
sync.lpush("list", "value1", "value2");
final Long listLen = sync.llen("list");
final List<String> allList = sync.lrange("list", 0, listLen);
System.out.println("allList: " + String.join(",", allList));

// 集合
sync.sadd("set", "set1", "set2");
final Long setSize = sync.scard("set");
final Set<String> allSet = sync.smembers("set");
System.out.println("setSize: " + setSize);
System.out.println("allSet: " + String.join(",", allSet));

// 哈希表
sync.hset("hashset", "key1", "value1");
sync.hset("hashset", "key2", "value2");
sync.hset("hashset", "key3", "value3");
final Long hlen = sync.hlen("hashset");
final Map<String, String> allHashset = sync.hgetall("hashset");
System.out.println("hlen: " + hlen);
System.out.println("allHashset: " + allHashset.entrySet().stream().map(k -> String.format("%s->%s", k.getKey(), k.getValue())).collect(Collectors.joining(",")));

01.1-使用 Lettuce 原生 API 实现订阅、发布功能

发布功能比较容易实现,只需要通过 RedisCommands#publish 接口向指定的 channel 发布消息即可。

final Long published = sync.publish("topic:foo", "hello, from idea!");

相比于发布,订阅实现则比较复杂。 Lettuce 提供了 RedisPubSubListener 接口,作为 channel 的监听器。 当特定事件(例如订阅、取消订阅、收到消息等)发生时,回调监听器的特定方法。 接下来,我将通过一个实例来演示,如果订阅一个 channel,并在收到消息时将消息打印到控制台。

RedisPubSubAdapter 是 Lettuce 提供的一个工具类,它实现了 RedisPubSubListener 接口,不过所有的方法都是空实现。

final LinkedBlockingQueue<String> messages = LettuceFactories.newBlockingQueue(); // 阻塞队列,用来存储 channel 中收到的消息

final RedisClient client = getClient();
final StatefulRedisPubSubConnection<String, String> pubSubConnection = client.connectPubSub(); // 获取订阅、发布连接
// 向连接中添加一个 listener,且重写其 message 方法,在收到消息时,将消息添加到阻塞队列 messages 中
pubSubConnection.addListener(new RedisPubSubAdapter<>(){
    @Override
    public void message(String channel, String message) {
        try {
            messages.put(message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
// 订阅 channel
final RedisPubSubCommands<String, String> sync = pubSubConnection.sync();
sync.subscribe("topic:foo");

避免主线程退出,我们新创建一个线程,负责从阻塞队列 messages 中读取消息并打印到控制台:

final Thread backThread = new Thread() {
    @Override
    public void run() {
        while (true) {
            try {
                final String message = messages.take(); // 从阻塞队列中获取消息并打印,若获取不到,则阻塞等待
                System.out.println("channel [topic:foo] received message: " + message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
backThread.start();

// 避免主线程退出
try {
    backThread.join();
} catch (InterruptedException ie) {
    ie.printStackTrace();
}

01.2-使用 Lettuce 原生 API 进行事务操作

使用 Lettuce 进行 Redis 事务操作与使用 redis-cli 的操作非常相似。

 final RedisClient client = getClient();
final StatefulRedisConnection<String, String> connection = client.connect();
final RedisCommands<String, String> sync = connection.sync();
sync.multi(); // 开启事务
        
// 事务执行
sync.set("one", "1");
sync.set("two", "2");
sync.set("three", "3");

final TransactionResult execResult = sync.exec();// 提交事务,Server 开始执行
execResult.forEach(System.out::println);

Lettuce 不支持 Redis pipeline。

02-Spring Boot 对 Lettuce 的封装

在前面的文章 Spring Boot「17」整合 Redis 中提到,spring-boot-data-redis 对 Redis 进行了封装,并向应用上下文容器中注入了两个工具类:

  • RedisTemplate
  • StringRedisTemplate

其中,后者是 RedisTemplate 的一个派生类,它按照 String 类型来处理 Redis 中的键、值。

Spring Boot 中,与 Redis 的连接被封装为 RedisConnection 接口,有两个实现类分别用来封装底层 redis-cli 连接:

  • LettuceConnection
  • JedisConnection

RedisTemplate 依靠 RedisConnectionFactory 来获取与 Redis 实例的连接对象。 Spring Boot 提供了 RedisConnectionFactory 的两个实现,分别针对 Lettuce 和 Jedis 两个常见的 redis-cli 实现:

  • LettuceConnectionFactory,获取 LettuceConnection 实例;
  • JedisConnectionFactory,获取 JedisConnection 实例;

RedisConnectionFactory 和 RedisConnection 屏蔽了底层不同 redis-cli 实现 API 的差异,使 RedisTemplate 与底层实现解耦。 该接口中有三个关键的接口,针对不同的场景获得连接:

public interface RedisConnectionFactory extends PersistenceExceptionTranslator {
    RedisConnection getConnection(); // 获取与单 Redis 实例的连接
    RedisClusterConnection getClusterConnection(); // 获取与 Redis 集群的连接
    RedisSentinelConnection getSentinelConnection(); // 获取与 Redis 哨兵的连接
}

LettuceConnectionFactory 是与 Lettuce 原生 API 深度耦合的。 通过查看它的源码,不难发现我在前面章节中介绍的 Lettuce 原生 API。 它持有一个 AbstractRedisClient client 对象,是与 Redis 交互的句柄; LettuceConnectionProvider connectionProvider 对象,负责从 RedisClient 中获取 Lettuce StatefulConnection<K, V> 连接。

在 Spring Boot 应用中,通过 StringRedisTemplate 进行基本数据结构的操作示例如下:

// 字符串
final Valu  eOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
opsForValue.set("str", "str_value");
final String strValue = opsForValue.get("str");
System.out.println("strValue: " + strValue);

// 列表
final ListOperations<String, String> opsForList = stringRedisTemplate.opsForList();
opsForList.leftPush("list", "value1", "value2");
final Long listLen = opsForList.size("list");
final List<String> allList = opsForList.range("list", 0, listLen);
System.out.println("allList: " + String.join(",", allList));

// 集合
final SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet();
opsForSet.add("set", "set1", "set2");
final Long setSize = opsForSet.size("set");
final Set<String> allSet = opsForSet.members("set");
System.out.println("setSize: " + setSize);
System.out.println("allSet: " + String.join(",", allSet));

// 哈希表
final HashOperations<String, Object, Object> opsForHash = stringRedisTemplate.opsForHash();
opsForHash.put("hashset", "key1", "value1");
opsForHash.put("hashset", "key2", "value2");
opsForHash.put("hashset", "key3", "value3");
final Long hlen = opsForHash.size("hashset");
final Map<Object, Object> allHashset = opsForHash.entries("hashset");
System.out.println("hlen: " + hlen);
System.out.println("allHashset: " + allHashset.entrySet().stream().map(k -> String.format("%s->%s", k.getKey(), k.getValue())).collect(Collectors.joining(",")));

02.1-使用 StringRedisTemplate 实现订阅、发布功能

与 Lettuce 一样,发布消息非常简单:

stringRedisTemplate.convertAndSend("topic:foo", "hello, all! _from redis-template");

订阅实现起来相对复杂一点:

final RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();
connection.subscribe((m, p) -> {
    try {
        messages.put(m.toString()); // 将消息添加到阻塞队列中
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "topic:foo".getBytes());

可以看到,StringRedisTemplate 实现订阅与 Lettuce 实现订阅的接口基本类似,都是在 Connection 上调用订阅方法,然后传递一个回调,在收到消息后调用。

除了上述这种方式外,Spring Boot 中还设计了一个 RedisMessageListenerContainer 的类。 官方文档对它的介绍是:

Container providing asynchronous behaviour for Redis message listeners. Handles the low level details of listening, converting and message dispatching.

final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(stringRedisTemplate.getConnectionFactory());
container.addMessageListener((m, p) -> {
    try {
        messages.put(m.toString());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, ChannelTopic.of("topic:foo"));

container.afterPropertiesSet();
container.start();

02.2-使用 StringRedisTemplate 进行事务操作

Spring Boot 官网给出的 StringRedisTemplate 执行事务的方式是通过 StringRedisTemplate#execute 方法。 例如:

List<String> results = stringRedisTemplate.execute(new SessionCallback<>() {
    @Override
    public List<String> execute(RedisOperations operations) throws DataAccessException {
        operations.multi(); // 开启事务

        ValueOperations opsForValue = operations.opsForValue();
        opsForValue.set("one", "1");
        opsForValue.set("two", "2");
        opsForValue.set("three", "3");

        return operations.exec(); // 提交事务
    }
});
System.out.println(results.size());

03-总结

通过上面的 API 对比,不难发现 spring-boot-data-redis 对 redis-cli 的封装后暴露的接口是比较简单易用的。 在开发过程中,如果是 Spring Boot 应用,建议使用 RedisTemplate 及其子类来与 Redis 服务端进行交互。 这样做的好处是可以将业务实现与底层的 redis-cli 实现解耦,当底层不满足需求时,更换底层而不会影响到业务层的代码。