文章目录
概要
工作中涉及到需要了解一下Lettuce的工作流程,然后还要封装一下,涉及到Lettuce的连接实现,主要结构等等。
目前Spring2.x已经集成了Lettuce作为连接redis的默认客户端,此前是jedis。这两者最大的不同就是Lettuce是线程安全的,可以用一个TCP连接实现类似管道模式的命令执行。
结构图
2. Lettuce 6.3.2结构
Lettuce主要结构
lettuce支持同步阻塞、异步、reactive三种模式。
lettuce的几个核心类,包含了lettuce主要的功能逻辑,主要为:RedisURI、RedisClient、StatefulRedisConnectionImpl、RedisCommands。
// 创建RedisClient实例,指向IP为localhost的redis服务,端口默认为6379
RedisClient client = RedisClient.create("redis://localhost");
// 基于上面的客户端,打开一个单节点的连接
StatefulRedisConnection<String, String> connection = client.connect();
// 获取同步执行的命令API
RedisCommands<String, String> commands = connection.sync();
// 执行GET命令
String value = commands.get("foo");
...
// 关闭连接,这个操作一般在应用销毁的时候,lettuce设计的连接是长连接
connection.close();
//关闭客户端实例,释放线程和其他资源,这个操作一般在应用销毁的时候
client.shutdown();
Redus URI
主要涉及一些参数配置, 这里可以包装一层configFactory,配置的参数也很重要,比如连接池、socket相关、拓扑刷新等等。
redis.mode=standalone
.redis.host=127.0.0.1
# .redis.password=yourpassword
.redis.port=6379
.redis.timeout=500
# .redis.cluster-nodes=
# .redis.max-redirects=3
# .redis.topology-refresh-period=20
.redis.use-connection-pool=true
.redis.pool-max-active=16
.redis.pool-max-idle=16
.redis.pool-min-idle=3
.redis.pool-max-wait=100
.redis.is-block-when-exhausted=true
.redis.is-test-while-idle=true
.redis.pool-time-between-eviction-runs-millis=30000
.redis.pool-min-evictable-idle-time-millis=60000
.redis.is-auto-reconnect=true
.redis.socket-timeout=1000
.redis.socket-keep-alive-idle=30
.redis.socket-keep-alive-interval=10
.redis.socket-keep-alive-count=3
.redis.tcp-user-timeout=30
.redis.request-queue-size=50000
RedisClient
在创建实例的同时会初始化基于netty的底层基础通信设施,比如netty的EventLoopGroup。RedisClient持有了一个ClientResources实例的引用,RedisClient在创建的实例的时候会调用父类AbstractRedisClient的初始化方法创建ClientResources的实现类DefaultClientResources的实例对象,而netty的初始化操作正是在DefaultClientResources的构造方法中实现的。
ClientResources
ClientResources
为 RedisClient
提供了共享的基础设施和资源,这些资源包括 Netty 的事件循环、线程池、DNS 解析器、计时器等。ClientResources
的作用是为 RedisClient
提供一个高效、可配置的运行环境。
ClientResources是可共享的。
protected AbstractRedisClient(ClientResources clientResources) {
if (clientResources == null) {
sharedResources = false;
// 创建ClientResources的实例对象
this.clientResources = DefaultClientResources.create();
} else {
sharedResources = true;
// 使用外部的ClientResources实例对象
this.clientResources = clientResources;
}
genericWorkerPool = this.clientResources.eventExecutorGroup();
channels = new DefaultChannelGroup(genericWorkerPool.next());
timer = (HashedWheelTimer) this.clientResources.timer();
}
StatefulRedisConnectionImpl
RedisClient.connect --> RedisClient.connectStandaloneAsync
【new DefaultEndpoint()[创建一个closeFuture,CompletableFuture对象]
--> RedisClient.newStatefulRedisConnection
【(StatefulRedisConnectionImpl继承RedisChannelHandler)
--> StatefulRedisConnectionImpl初始化方法中,初始化四个组件
this.codec = codec;
this.async = newRedisAsyncCommandsImpl();
this.sync = newRedisSyncCommandsImpl(); // 代理对象
this.reactive = newRedisReactiveCommandsImpl();
】
--> 新建ConnectionFuture connectStatefulAsync
【初始化CommandHandler(继承于ChannelDuplexHandler,属于netty类)
--> RedisClient.getConnectionBuilder构建新的ConnectionBuilder
--> AbstractRedisClient.connectionBuilder(构建netty bootstrap)
--> connectionBuilder.connection将StatefulRedisConnectionImpl设置到ConnectionBuilder的connection属性
--> AbstractRedisClient.initializeChannelAsync「创建netty的channel,新建socketAddressFuture和channelReadyFuture,都是CompletableFuture类型」
--> 创建future(DefaultConnectionFuture类型,异步新建StatefulRedisConnectionImpl对象)
】
--> 返回future(DefaultConnectionFuture类型)
】
--> AbstractRedisClient.getConnection【调用connectionFuture.get()获取StatefulRedisConnectionImpl对象连接对象】
RedisCommands
命令执行核心流程源码(同步)connection.sync()这个方法返回的是StatefulRedisConnectionImpl对象的sync属性值,StatefulRedisConnectionImpl会在初始化的时候设值,方法如下:
/**
* Initialize a new connection.
*
* @param writer the channel writer
* @param codec Codec used to encode/decode keys and values.
* @param timeout Maximum time to wait for a response.
*/
public StatefulRedisConnectionImpl(RedisChannelWriter writer, RedisCodec<K, V> codec, Duration timeout) {
super(writer, timeout);
this.codec = codec;
this.async = newRedisAsyncCommandsImpl();
this.sync = newRedisSyncCommandsImpl();
this.reactive = newRedisReactiveCommandsImpl();
}
newRedisSyncCommandsImpl方法返回的就是一个代理对象【JDK动态代理】,源码如下:
/**
* Create a new instance of {@link RedisCommands}. Can be overriden to extend.
*
* @return a new instance.
*/
protected RedisCommands<K, V> newRedisSyncCommandsImpl() {
return syncHandler(async(), RedisCommands.class, RedisClusterCommands.class);
}
syncHandler源码:
protected <T> T syncHandler(Object asyncApi, Class<?>...interfaces) {
FutureSyncInvocationHandler h = newFutureSyncInvocationHandler((StatefulConnection<?, ?>) this, asyncApi, interfaces);
return (T) Proxy.newProxyInstance(AbstractRedisClient.class.getClassLoader(), interfaces, h);
}
调用connection.sync()
方法时,实际上返回的是StatefulRedisConnectionImpl
对象的sync
属性值,即前面创建的代理对象。这个代理对象会拦截所有的方法调用,并通过FutureSyncInvocationHandler
将其转换为异步调用,等待结果返回
调用get流程图
命令执行流程图
Lettuce读写命令流程
-
通过RedisClient初始化连接(netty参数,重试机制等等):
通过RedisClient.connect获取到连接StatefulRedisConnectionImpl
在创建连接StatefulRedisConnectionImpl过程中,会初始化BootStrap、Channel和DefaultEndpoint等重要成员。StatefulRedisConnectionImpl绑定了一个Channel,Channel也绑定了IO线程(Bootstrap#connect)。 -
通过StatefulRedisConnectionImpl获取同步/异步/响应式连接,再执行Redis命令。
命令首先被封装成AsyncCommand,然后通过DefaultEndpoint发送命令,DefaultEndpoint内部会依靠Netty IO线程使用Channel异步发送命令,此时用户线程已经能获取到一个AsyncCommand,只是没有响应的数据。
命令的响应首先被Netty IO线程接收到,从绑定的对应channel的CommandHandler里面取出对应的AsyncCommand,解析且封装响应到output,此时之前用户线程就能从AsyncCommand拿到响应了。
连接正常时的有序性
lettuce是通过DefaultEndpoint缓存本地命令,CommandHandler与redis服务器进行读写命令。每次在DefaultEndpoint会用sharedLock保证命令有序性,防止有重连或者未flush的情况;每次CommandHandler向服务端发送一条命令,就会往stack底部压入一条命令,TCP是有序的,Redis是单线程处理命令,所以从服务端响应过来了结果,能够对应上之前往stack压入的命令,这时CommandHandler只需要pop出来顶端的命令,即可对应上。
参考 Lettuce是如何发送Command命令到redis的 https://blog.csdn.net/weixin_45145848/article/details/103710855
https://developer.aliyun.com/article/1502748
小结
整理的内容有些杂乱,后面有时间就再梳理一下吧。
参考文档
Redis客户端Lettuce深度分析介绍 https://developer.aliyun.com/article/1502748
lettuce文档 https://lettuce.io/core/release/reference/
Lettuce源码分析 https://segmentfault.com/a/1190000041391240
Spring-Redis-验证lettuce共享和独占连接特性 https://blog.csdn.net/chl87783255/article/details/103293273
Lettuce 共享连接实现及command发送流程 https://blog.csdn.net/singgel/article/details/105552962