Lettuce----Redis高级客户端
基本介绍
这是Lettuce官方对他们产品的介绍:“Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.”
-
Advanced client:主要体现在实现的过程中不用管什么细节,直接可以new出来用,很方便。
-
thread-safe:Jedis在使用的时候需要使用线程连接池,这是因为它并不是一个线程安全的连接,每次使用过后都要归还连接,这在高并发的情况下可能会造成大量的资源浪费。
-
sync, async, and reactive usage:支持同步,异步,响应式API,Lettuce是一个基于Netty框架按照NIO进行设计的纯异步客户端。即使是同步命令,底层的通信过程仍然是异步模型,只是通过阻塞调用线程来模拟出同步效果而已。
-
Supports Cluster, Sentinel, Pipelining, and codecs:支持集群、哨兵、管道和编解码,很高级,但我不懂…
-
目前在spring中已经越来越多的使用Lettuce作为Redis客户端进行开发了。
-
官方文档:生菜连接被设计为线程安全的,因此一个连接可以在多个线程之间共享,并且生菜连接默认自动连接重启。虽然在大多数情况下不需要连接池,但它在某些用例中可能会有所帮助。生菜提供通用连接池支持。
基本使用
Lettuce一般主要依赖四个主要组件:
RedisURI
:连接信息。RedisClient
:客户端,一般通过URI实现。Connection
:redis连接,主要是StatefulConnection或者StatefulRedisConnection的子类。Command
:用户操作api,基本包含了redis全部命令。
使用流程也很简单:使用指定的URI创建Client,使用Client创建Connection,使用Connection创建Command,然后就可以操作Command的api了,最后关闭连接关闭客户端。一个简单的例子如下:
@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", "wzl", setArgs);
Assertions.assertThat(result).isEqualToIgnoringCase("OK");
result = redisCommands.get("name");
Assertions.assertThat(result).isEqualTo("wzl");
// ... 其他操作
connection.close(); // <5> 关闭连接
redisClient.shutdown(); // <6> 关闭客户端
}
这个例子其实和之前使用的RedisTemplate差别不大。
异步和同步
我一开始也不懂同步和异步的差别到底是什么,后面了解到同步类似于我玩奥德赛的时候一件任务做完还要回雅典卫城重新领取任务才坐下一件任务,异步是拿完所有任务后一起做,类似于出发的时候规划好任务路线,然后一路跑过去做过去。异步确实是要比同步快不少,所以我还是决定学习一下异步API的用法。
但是异步也并不是万能的,后台任务可以异步,提供数据给浏览器客户端一般使用同步。尤其是基于Servlet模型的同步访问时,连接池是有必要的。因此可以用commens pool对象池来存放redis连接。
public void init() {
GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(20);
poolConfig.setMaxIdle(5);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);
this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), poolConfig);
}
后面要使用到redis连接就直接从pool中borrowObject方法拿一下就行了,最后要记得释放(可以用try套住就不用释放了)。
public <T> T executeSync(SyncCommandCallback<T> callback) {
try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
connection.setAutoFlushCommands(true);
RedisCommands<String, String> commands = connection.sync();
return callback.doInConnection(commands);
} catch (Exception e) {
throw new RuntimeException(e);
}
Lettuce的流水线Pipeline
Lettuce 的连接有一个 AutoFlushCommands 配置,就是指在这个连接上执行的命令,如果发送到服务端。默认是 false,即收到一个命令就发到服务端一个。如果配置为 true,则将所有命令缓存起来,手动调用 flushCommands 的时候,将缓存的命令一起发到服务端,这样其实就是实现了 Pipeline。
函数式编程和函数式接口
这个主要是看别人博客为了模仿**JdbcTemplate.execute(ConnectionCallback)
**来减少样板代码以简化代码,顺便我也学一学函数式接口的用法。接口代码如下:
@FunctionalInterface
public interface SyncCommandCallback<T> {
// 在此操作Redis,别小看下面这就一行抽象函数,这就是lambda的魅力
T doInConnection(RedisCommands<String, String> commands);
}
函数式接口标签**@FunctionalInterface**代表这个接口只能且至少有一个抽象方法,无论调用什么单一方法,只要Java编译器具有兼容的方法签名,Java编译器就会将其匹配到您的lambda表达式。
以我的代码为例,我的execute函数包含一个接口参数,形参名为callback,这不重要,这只是代表它是一个callback而已。注意返回的是 callback的唯一一个方法doInConnection
,并且传入了一个rediscommands参数。这时候再用自己的方法加上lambda表达式对其进行封装,代码如下:
public String set(String key, String value) {
return executeSync(commands -> commands.set(key, value));
}
public String get(String key) {
return executeSync(commands -> commands.get(key));
}
我们对函数接口赋值了一个lambda表达式,此时可以理解成这个函数式接口里的方法doInConnection
接收一个commands参数并且返回一个commands.set(key, value)的结果。此时在executeSync函数中就完成了redis命令的执行。通过学习我知道了这就是函数式编程的意义,把函数作为基本运算单元了,好像比javascript里面的函数式编程还牛逼一点,这边函数作为了变量,可以接受函数还可以返回函数。
我认为这个方法的好处还是挺大的,我不用每个get和set都写一个很复杂的execute方法,这样确实是极大地简化了代码。