前提#
Lettuce
是一个Redis
的Java
驱动包,初识她的时候是使用RedisTemplate
的时候遇到点问题Debug
到底层的一些源码,发现spring-data-redis
的驱动包在某个版本之后替换为Lettuce
。Lettuce
翻译为生菜,没错,就是吃的那种生菜,所以它的Logo
长这样:既然能被Spring
生态所认可,Lettuce
想必有过人之处,于是笔者花时间阅读她的官方文档,整理测试示例,写下这篇文章。编写本文时所使用的版本为Lettuce 5.1.8.RELEASE
,SpringBoot 2.1.8.RELEASE
,JDK [8,11]
。超长警告:这篇文章断断续续花了两周完成,超过4万字.....
小伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。我有一些面试题、架构、设计类资料可以说是程序员面试必备!所有资料都整理到网盘了,需要的话欢迎下载!私信我回复【000】即可免费获取
Lettuce简介#
Lettuce
是一个高性能基于Java
编写的Redis
驱动框架,底层集成了Project Reactor
提供天然的反应式编程,通信框架集成了Netty
使用了非阻塞IO
,5.x
版本之后融合了JDK1.8
的异步编程特性,在保证高性能的同时提供了十分丰富易用的API
,5.1
版本的新特性如下:
- 支持
Redis
的新增命令ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX
。 - 支持通过
Brave
模块跟踪Redis
命令执行。 - 支持
Redis Streams
。 - 支持异步的主从连接。
- 支持异步连接池。
- 新增命令最多执行一次模式(禁止自动重连)。
- 全局命令超时设置(对异步和反应式命令也有效)。
- ......等等
注意一点:Redis
的版本至少需要2.6
,当然越高越好,API
的兼容性比较强大。
只需要引入单个依赖就可以开始愉快地使用Lettuce
:
- Maven
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
- Gradle
dependencies {
compile 'io.lettuce:lettuce-core:5.1.8.RELEASE'
}
连接Redis#
单机、哨兵、集群模式下连接Redis
需要一个统一的标准去表示连接的细节信息,在Lettuce
中这个统一的标准是RedisURI
。可以通过三种方式构造一个RedisURI
实例:
- 定制的字符串
URI
语法:
RedisURI uri = RedisURI.create("redis://localhost/");
- 使用建造器(
RedisURI.Builder
):
RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
- 直接通过构造函数实例化:
RedisURI uri = new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);
定制的连接URI语法#
- 单机(前缀为
redis://
)
格式:redis://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]
完整:redis://mypassword@127.0.0.1:6379/0?timeout=10s
简单:redis://localhost
- 单机并且使用
SSL
(前缀为rediss://
) <== 注意后面多了个s
格式:rediss://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]
完整:rediss://mypassword@127.0.0.1:6379/0?timeout=10s
简单:rediss://localhost
- 单机
Unix Domain Sockets
模式(前缀为redis-socket://
)
格式:redis-socket://path[?[timeout=timeout[d|h|m|s|ms|us|ns]][&_database=database_]]
完整:redis-socket:///tmp/redis?timeout=10s&_database=0
- 哨兵(前缀为
redis-sentinel://
)
格式:redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]#sentinelMasterId
完整:redis-sentinel://mypassword@127.0.0.1:6379,127.0.0.1:6380/0?timeout=10s#mymaster
超时时间单位:
- d 天
- h 小时
- m 分钟
- s 秒钟
- ms 毫秒
- us 微秒
- ns 纳秒
个人建议使用RedisURI
提供的建造器,毕竟定制的URI
虽然简洁,但是比较容易出现人为错误。鉴于笔者没有SSL
和Unix Domain Socket
的使用场景,下面不对这两种连接方式进行列举。
基本使用#
Lettuce
使用的时候依赖于四个主要组件:
RedisURI
:连接信息。RedisClient
:Redis
客户端,特殊地,集群连接有一个定制的RedisClusterClient
。Connection
:Redis
连接,主要是StatefulConnection
或者StatefulRedisConnection
的子类,连接的类型主要由连接的具体方式(单机、哨兵、集群、订阅发布等等)选定,比较重要。RedisCommands
:Redis
命令API
接口,基本上覆盖了Redis
发行版本的所有命令,提供了同步(sync
)、异步(async
)、反应式(reative
)的调用方式,对于使用者而言,会经常跟RedisCommands
系列接口打交道。
一个基本使用例子如下:
@Test
public void testSetGet() throws Exception {
RedisURI redisUri = RedisURI.builder() // <1> 创建单机连接的连接信息
.withHost("localhost")
.withPort(6379)
.withTimeout(Duration.of(10, ChronoUnit.SECONDS))
.build();
RedisClient redisClient = RedisClient.create(redisUri); // <2> 创建客户端
StatefulRedisConnection<String, String> connection = redisClient.connect(); // <3> 创建线程安全的连接
RedisCommands<String, String> redisCommands = connection.sync(); // <4> 创建同步命令
SetArgs setArgs = SetArgs.Builder.nx().ex(5);
String result = redisCommands.set("name", "throwable", setArgs);
Assertions.assertThat(result).isEqualToIgnoringCase("OK");
result = redisCommands.get("name");
Assertions.assertThat(result).isEqualTo("throwable");
// ... 其他操作
connection.close(); // <5> 关闭连接
redisClient.shutdown(); // <6> 关闭客户端
}
注意:
- <5>:关闭连接一般在应用程序停止之前操作,一个应用程序中的一个
Redis
驱动实例不需要太多的连接(一般情况下只需要一个连接实例就可以,如果有多个连接的需要可以考虑使用连接池,其实Redis
目前处理命令的模块是单线程,在客户端多个连接多线程调用理论上没有效果)。 - <6>:关闭客户端一般应用程序停止之前操作,如果条件允许的话,基于后开先闭原则,客户端关闭应该在连接关闭之后操作。
API#
Lettuce
主要提供三种API
:
- 同步(
sync
):RedisCommands
。 - 异步(
async
):RedisAsyncCommands
。 - 反应式(
reactive
):RedisReactiveCommands
。
先准备好一个单机Redis
连接备用:
private static StatefulRedisConnection<String, String> CONNECTION;
private static RedisClient CLIENT;
@BeforeClass
public static void beforeClass() {
RedisURI redisUri = RedisURI.builder()
.withHost("localhost")
.withPort(6379)
.withTimeout(Duration.of(10, ChronoUnit.SECONDS))
.build();
CLIENT = RedisClient.create(redisUri);
CONNECTION = CLIENT.connect();
}
@AfterClass
public static void afterClass() throws Exception {
CONNECTION.close();
CLIENT.shutdown();
}
Redis
命令API
的具体实现可以直接从StatefulRedisConnection
实例获取,见其接口定义:
public interface StatefulRedisConnection<K, V> extends StatefulConnection<K, V> {
boolean isMulti();
RedisCommands<K, V> sync();
RedisAsyncCommands<K, V> async();
RedisReactiveCommands<K, V> reactive();
}
值得注意的是,在不指定编码解码器RedisCodec
的前提下,RedisClient
创建的StatefulRedisConnection
实例一般是泛型实例StatefulRedisConnection<String,String>
,也就是所有命令API
的KEY
和VALUE
都是String
类型,这种使用方式能满足大部分的使用场景。当然,必要的时候可以定制编码解码器RedisCodec<K,V>
。
同步API#
先构建RedisCommands
实例:
private static RedisCommands<String, String> COMMAND;
@BeforeClass
public static void beforeClass() {
COMMAND = CONNECTION.sync();
}
基本使用:
@Test
public void testSyncPing() throws Exception {
String pong = COMMAND.ping();
Assertions.assertThat(pong).isEqualToIgnoringCase("PONG");
}
@Test
public void testSyncSetAndGet() throws Exception {
SetArgs setArgs = SetArgs.Builder.nx().ex(5);
COMMAND.set("name", "throwable", setArgs);
String value = COMMAND.get("name");
log.info("Get value: {}", value);
}
// Get value: throwable
同步API
在所有命令调用之后会立即返回结果。如果熟悉Jedis
的话,RedisCommands
的用法其实和它相差不大。
异步API#
先构建RedisAsyncCommands
实例:
private static RedisAsyncCommands<String, String> ASYNC_COMMAND;
@BeforeClass
public static void beforeClass() {
ASYNC_COMMAND = CONNECTION.async();
}
基本使用:
@Test
public void testAsyncPing() throws Exception {
RedisFuture<String> redisFuture = ASYNC_COMMAND.ping();
log.info("Ping result:{}", redisFuture.get());
}
// Ping result:PONG
RedisAsyncCommands
所有方法执行返回结果都是RedisFuture
实例,而RedisFuture
接口的定义如下:
public interface RedisFuture<V> extends CompletionStage<V>, Future<V> {
String getError();
boolean await(long timeout, TimeUnit unit) throws InterruptedException;
}
也就是,RedisFuture
可以无缝使用Future
或者JDK
1.8中引入的CompletableFuture
提供的方法。举个例子:
@Test
public void testAsyncSetAndGet1() throws Exception {
SetArgs setArgs = SetArgs.Builder.nx().ex(5);
RedisFuture<String> future = ASYNC_COMMAND.set("name", "throwable", setArgs);
// CompletableFuture#thenAccept()
future.thenAccept(value -> log.info("Set命令返回:{}", value));
// Future#get()
future.get();
}
// Set命令返回:OK
@Test
public void testAsyncSetAndGet2() throws Exception {
SetArgs setArgs = SetArgs.Builder.nx().ex(5);
CompletableFuture<Void> result =
(CompletableFuture<Void>) ASYNC_COMMAND.set("name", "throwable", setArgs)
.thenAcceptBoth(ASYNC_COMMAND.get("name"),
(s, g) -> {
log.info("Set命令返回:{}", s);
log.info("Get命令返回:{}", g);
});
result.get();
}
// Set命令返回:OK
// Get命令返回:throwable
如果能熟练使用CompletableFuture
和函数式编程技巧,可以组合多个RedisFuture
完成一些列复杂的操作。
反应式API#
Lettuce
引入的反应式编程框架是Project Reactor,如果没有反应式编程经验可以先自行了解一下Project Reactor
。
构建RedisReactiveCommands
实例:
private static RedisReactiveCommands<String, String> REACTIVE_COMMAND;
@BeforeClass
public static void beforeClass() {
REACTIVE_COMMAND = CONNECTION.reactive();
}
根据Project Reactor
,RedisReactiveCommands
的方法如果返回的结果只包含0或1个元素,那么返回值类型是Mono
,如果返回的结果包含0到N(N大于0)个元素,那么返回值是Flux
。举个例子:
@Test
public void testReactivePing() throws Exception {
Mono<String> ping = REACTIVE_COMMAND.ping();
ping.subscribe(v -> log.info("Ping result:{}", v));
Thread.sleep(