一、前言
最近因为工作需求,需要了解lettuce
的一些实现原理。从官方文档的目录可以看到,lettuce
实现了非常丰富的各项业务功能,支持同步阻塞,异步(Future
)以及Reactive三种执行模式,支持依赖第三方连接池组件common-pool2
使用连接池技术,但是官方并不推荐使用连接池。底层通信是基于netty
。如此丰富的实现,可想而知起源码的复杂性,第一次看lettuce
的源码确实挺煎熬。
本文基于lettuce同步阻塞的执行模式,进行源码阅读分析,后续会继续学习其他执行模式的源码实现。
其实在这篇文章之前,有做过各连接池组件的性能测试,包括jedisPool、redisson、lettuce【同步模式下使用连接池和不使用池】。测试的结果出乎我的意料,同步模式下lettuce
的性能是最好的,lettuce
使用连接池的时候性能会有6%左右的下降,性能最差的竟然是redisson
,当然也不排除自己使用不当(将redis操作封装成了restful接口,通过压测http接口进行测试),后续会继续验证。
二、Lettuce的几个核心类
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();
(一)RedisURI
定义了需要连接的redis服务的IP、端口、超时时间(默认60s)、操作的数据库以及安全认证等等;
RedisURI的源码相对比较简单,主要的方法都是设置一些成员属性。
Example 1(使用host、port,并设置默认超时时间为20秒):
RedisClient client = RedisClient.create(RedisURI.create("localhost", 6379)); client.setDefaultTimeout(20, TimeUnit.SECONDS); // … client.shutdown();
Example 2(使用
RedisURI
构建client):RedisURI redisUri = RedisURI.Builder.redis("localhost") .withPassword("authentication") .withDatabase(2) .build(); RedisClient client = RedisClient.create(redisUri); // … client.shutdown();
其他详见官方文档。
(二)RedisClient
RedisClient
继承AbstractRedisClient
,是一个可伸缩的线程安全的redis客户端,支持上面提到的同步、异步、Reactive 3种执行模式。多个线程共享一个TCP连接。
另外使用RedisClusterClient
实现redis集群模式的客户端,MasterSlave
【5.2之后是MasterReplica
】实现redis主从或者哨兵集群模式的客户端。
RedisClient
是一个非常重的资源。在创建实例的同时,会初始化基于netty的底层基础通信设施,包括各种线程池的创建,比如netty
的EventLoopGroup
。因此尽量复用RedisClient
实例,或者在共享同一个ClientResources
实例。
实际上RedisClient
持有了一个ClientResources
实例的引用,RedisClient
在创建的实例的时候会调用父类AbstractRedisClient
的初始化方法创建ClientResources
的实现类DefaultClientResources
的实例对象,而netty
的初始化操作正是在DefaultClientResources
的构造方法中实现的。
RedisClient
的构造方法// RedisClient的构造方法 protected RedisClient(ClientResources clientResources, RedisURI redisURI) { // 调用父类的初始化方法 super(clientResources); assertNotNull(redisURI); this.redisURI = redisURI; setDefaultTimeout(redisURI.getTimeout()); }
AbstractRedisClient
的属性和构造方法public abstract class AbstractRedisClient { protected static final PooledByteBufAllocator BUF_ALLOCATOR = PooledByteBufAllocator.DEFAULT; protected static final InternalLogger logger = InternalLoggerFactory.getInstance(RedisClient.class); protected final Map<Class<? extends EventLoopGroup>, EventLoopGroup> eventLoopGroups = new ConcurrentHashMap<>(2); protected final ConnectionEvents connectionEvents = new ConnectionEvents(); protected final Set<Closeable> closeableResources = ConcurrentHashMap.newKeySet(); protected final EventExecutorGroup genericWorkerPool; protected final HashedWheelTimer timer; protected final ChannelGroup channels; // 持有ClientResources的对象引用 protected final ClientResources clientResources; protected volatile ClientOptions clientOptions = ClientOptions.builder().build(); protected Duration timeout = RedisURI.DEFAULT_TIMEOUT_DURATION; private final boolean sharedResources; private final AtomicBoolean shutdown = new AtomicBoolean(); /** * Create a new instance with client resources. * * @param clientResources the client resources. If {@code null}, the client will create a new dedicated instance of client * resources and keep track of them. */ 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
线程安全的redis连接,StatefulRedisConnectionImpl
底层维护一个tcp连接,多线程共享一个连接对象。同时会有一个ConnectionWatchdog
【继承netty的ChannelInboundHandlerAdapter】负责连接的维护,实现断线重连。
ConnectionWatchdog
部分源码:@ChannelHandler.Sharable public class ConnectionWatchdog extends ChannelInboundHandlerAdapter { ... @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { logger.debug("{} channelInactive()", logPrefix()); if (!armed) { logger.debug("{} ConnectionWatchdog not armed", logPrefix()); return; } channel = null; if (listenOnChannelInactive && !reconnectionHandler.isReconnectSuspended()) { // 进行重连当channel不可用或者不活跃的时候 scheduleReconnect(); } else { logger.debug("{} Reconnect scheduling disabled", logPrefix(), ctx); } super.channelInactive(ctx); } ... }
StatefulRedisConnectionImpl
实例是在第一次调用RedisClient.connect
方法的时候创建的。
// 基于上面的客户端,打开一个单节点的连接
StatefulRedisConnection<String, String> connection = client.connect();
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对象连接对象】
(四)RedisCommand
一个redis command对象持有一个output(CommandOutput类型对象,保存redis服务返回内容),arguments(需要发送到redis服务的命令内容),status(状态,标识一个command操作:初始化、完成、取消)
以上面官网get操作为例,Command对象实例如下(debug过程中截取的待执行Command):
Command(ProtocolKeyword type, CommandOutput<K, V, T> output, CommandArgs<K, V> args) {
LettuceAssert.notNull(type, "Command type must not be null");
this.type = type; // GET
this.output = output; // StatusOutput
this.args = args; // 实际发送内容:[buffer=$8Thread-2$23this is thread Thread-2]
}
三、同步执行核心源码
同步执行代码示例:
// 获取同步执行的命令API
RedisCommands<String, String> commands = connection.sync();
String value = commands.get("foo");
(一)代理对象
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 = new FutureSyncInvocationHandler((StatefulConnection<?, ?>) this, asyncApi, interfaces);
return (T) Proxy.newProxyInstance(AbstractRedisClient.class.getClassLoader(), interfaces, h);
}
(二)执行一个同步操作的流程
commands.get("foo");
执行这个方法的时候,是由对应的InvocationHandler
来实现的,而这边对应的Handler是FutureSyncInvocationHandler
,具体流程如下:
io.lettuce.core.internal.AbstractInvocationHandler.invoke
-> FutureSyncInvocationHandler.handleInvocation(Object result = targetMethod.invoke(asyncApi, args);)
-> AbstractRedisAsyncCommands.get【返回RedisFuture类型对象】
-> RedisCommandBuilder.get【构建redis命令对象】
-> AbstractRedisAsyncCommands.dispatch() 【new AsyncCommand<>(cmd),将普通Command对象封装成AsyncCommand对象】
-> StatefulRedisConnectionImpl.dispatch()
-> StatefulRedisConnectionImpl.preProcessCommand【1、首先判断是否需要安全验证 2、是否选择自定义库 3、是否只读模式 4、是否读写模式 5、是否DISCARD 6、是否为EXEC 7、是否为MULTI】
-> RedisChannelHandler.dispatch()【判断是否为debug或者tracingEnabled(默认false)】
-> DefaultEndpoint.write()
-> DefaultEndpoint.writeToChannelAndFlush()
-> DefaultEndpoint.channelWriteAndFlush()[交给底层netty进行传输,并设置重试监听器]
-> [netty部分]AbstractChannel.writeAndFlush
-> LettuceFutures.awaitOrCancel(command, timeout, TimeUnit.NANOSECONDS); // 等待redis返回数据
FutureSyncInvocationHandler
的handleInvocation
方法源码:
@Override
@SuppressWarnings("unchecked")
protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable {
try {
Method targetMethod = this.translator.get(method);
Object result = targetMethod.invoke(asyncApi, args);
if (result instanceof RedisFuture<?>) {
RedisFuture<?> command = (RedisFuture<?>) result;
if (!isTxControlMethod(method.getName(), args) && isTransactionActive(connection)) {
return null;
}
long timeout = getTimeoutNs(command);
return LettuceFutures.awaitOrCancel(command, timeout, TimeUnit.NANOSECONDS);
}
return result;
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
同步执行的核心是这个方法:
LettuceFutures.awaitOrCancel(command, timeout, TimeUnit.NANOSECONDS);
LettuceFutures.awaitOrCancel
源码:
/**
* Wait until futures are complete or the supplied timeout is reached. Commands are canceled if the timeout is reached but
* the command is not finished. A {@code timeout} value of zero or less indicates to not time out.
*
* @param cmd command to wait for.
* @param timeout maximum time to wait for futures to complete.
* @param unit unit of time for the timeout.
* @param <T> Result type.
* @return Result of the command.
*/
public static <T> T awaitOrCancel(RedisFuture<T> cmd, long timeout, TimeUnit unit) {
try {
if (timeout > 0 && !cmd.await(timeout, unit)) {
cmd.cancel(true);
throw ExceptionFactory.createTimeoutException(Duration.ofNanos(unit.toNanos(timeout)));
}
// 获取redis返回数据并返回
return cmd.get();
} catch (RuntimeException e) {
throw e;
} catch (ExecutionException e) {
if (e.getCause() instanceof RedisCommandExecutionException) {
throw ExceptionFactory.createExecutionException(e.getCause().getMessage(), e.getCause());
}
if (e.getCause() instanceof RedisCommandTimeoutException) {
throw new RedisCommandTimeoutException(e.getCause());
}
throw new RedisException(e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RedisCommandInterruptedException(e);
} catch (Exception e) {
throw ExceptionFactory.createExecutionException(null, e);
}
}
可以看下正常执行的线程栈,会更清晰一些:
Lettuce
中大量使用了CompletableFuture
异步框架,因此代码看起来会很不连贯,不太容易阅读。仅仅是同步执行也花费了不少时间学习😤。不过还是会加油抽时间把异步和reactive模式的源码看下。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。