3
头图

一、前言

  最近因为工作需求,需要了解lettuce的一些实现原理。从官方文档的目录可以看到,lettuce实现了非常丰富的各项业务功能,支持同步阻塞,异步(Future)以及Reactive三种执行模式,支持依赖第三方连接池组件common-pool2使用连接池技术,但是官方并不推荐使用连接池。底层通信是基于netty。如此丰富的实现,可想而知起源码的复杂性,第一次看lettuce的源码确实挺煎熬。

  本文基于lettuce同步阻塞的执行模式,进行源码阅读分析,后续会继续学习其他执行模式的源码实现。

  其实在这篇文章之前,有做过各连接池组件的性能测试,包括jedisPool、redisson、lettuce【同步模式下使用连接池和不使用池】。测试的结果出乎我的意料,同步模式下lettuce的性能是最好的,lettuce使用连接池的时候性能会有6%左右的下降,性能最差的竟然是redisson,当然也不排除自己使用不当(将redis操作封装成了restful接口,通过压测http接口进行测试),后续会继续验证。

二、Lettuce的几个核心类

lettuce的几个核心类,包含了lettuce主要的功能逻辑,主要为:RedisURIRedisClientStatefulRedisConnectionImplRedisCommands

先拿官网上的基础使用放一个使用案例:

// 创建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的底层基础通信设施,包括各种线程池的创建,比如nettyEventLoopGroup。因此尽量复用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返回数据

FutureSyncInvocationHandlerhandleInvocation方法源码:

    @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);
        }
    }

可以看下正常执行的线程栈,会更清晰一些:
image.png


Lettuce中大量使用了CompletableFuture异步框架,因此代码看起来会很不连贯,不太容易阅读。仅仅是同步执行也花费了不少时间学习😤。不过还是会加油抽时间把异步和reactive模式的源码看下。


开翻挖掘机
225 声望26 粉丝

不忘初心❤️,且行且思考