基于 Netty 的 Lettuce 居然是这样解析RESP协议的

2024年 5月 20日 78.4k 0

今天来分享 Lettuce —— 基于 Netty 实现,Springboot2 中默认的 redis 客户端。

那它是不是直接用 Netty 中的那几个 handler 来处理 RESP 协议的呢?一起看看吧。

可以看到这里并没有 codec-redis 模块,所以 Lettuce 并没有使用 Netty 提供的 redis 模块。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-1图片

(⊙﹏⊙),问题解决得太快了,那就再来思考下,它是怎么做的呢?

既然 Lettuce 基于 Netty 实现,那么它必然在 ChannelHandler 上动手脚,直接搜索可以发现有 9 个实现类。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-2图片

这里我关心的就是它怎么编解码,所以直接来看 CommandEncoder 和 CommandHandler 。

打上断点,使用测试例子直接 debug。

代码

@Test
    void redisTest() {
        // 创建 redis 客户端
        RedisClient redisClient = RedisClient.create("redis://123456@192.168.200.128:6379/0");
        // 创建 channel
        StatefulRedisConnection connection = redisClient.connect();
        // 使用 sync 同步命令
        RedisCommands syncCommands = connection.sync();

        String name = syncCommands.get("name");
        System.out.println(name);
//        syncCommands.set("key", "Hello, Redis!");

        connection.close();
        redisClient.shutdown();
    }

刚开始时,要和服务器建立连接,发送数据,涉及到 encode 流程。

CommandHandler

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-3图片

如图,直接来到 nioEventLoop 线程,并调用了 write 方法。

write:382, CommandHandler (io.lettuce.core.protocol)

从右边可以看到,发了一个 HELLO 的命令出去,其中 CommandArgs 如下:

CommandArgs [buffer=$1
3
$4
AUTH
$7
default
$6
123456
]

CommandArgs⭐

直接来到 toString 方法,可以发现 encode 方法。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-4图片

如图,有 4 个 SingularArgument:

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-5图片

看看他们是怎么 encode 的 。

ProtocolKeywordArgument

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-6图片

StringArgument

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-7图片

对比 Netty

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-8图片

貌似没啥大的区别,可以看到 Lettuce 中,对 ByteBuf 的使用比较粗一些,Netty 中会计算这个 ByteBuf 的初始容量,而 Lettuce 就简单些处理,直接 singularArguments.size() * 10 。

还有一个 大小端序 的处理,只能说 Netty 太细了。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-9图片

CommandEncoder

直接 F9 来到这一个断点。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-10图片

继续 debug ,会来到 Command 类,在这里完成对发送数据的 encode。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-1图片

解析下要发送的数据。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-12图片

小结

那么到了这里,我们就了解完 encode 的实现了。

核心:CommandArgs 中的各种 SingularArgument

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-13图片

下面就是接受服务器数据,进行 decode 的流程了。

CommandHandler

来到 channelRead 。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-14图片

decode 时,会调用到 RedisStateMachine 的 decode ,它是这个流程的核心。

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-15图片

RedisStateMachine⭐

Redis 状态机:

基于 Netty 的 Lettuce 居然是这样解析RESP协议的-16图片

这里我直接 copy 了一份 。

static class State {

    // Callback interface to handle a {@link State}.
    @FunctionalInterface
    interface StateHandler {
        Result handle(RedisStateMachine rsm, State state, ByteBuf buffer, CommandOutput output,
                Consumer errorHandler);
    }

    enum Type implements StateHandler {

        SINGLE('+', RedisStateMachine::handleSingle),

        ERROR('-', RedisStateMachine::handleError),

        INTEGER(':', RedisStateMachine::handleInteger),

        // 下面开始都是 @since 6.0/RESP3
        FLOAT(',', RedisStateMachine::handleFloat),

        BOOLEAN('#', RedisStateMachine::handleBoolean),

        BULK_ERROR('!', RedisStateMachine::handleBulkError),

        VERBATIM('=', RedisStateMachine::handleBulkAndVerbatim), VERBATIM_STRING('=', RedisStateMachine::handleVerbatim),

        BIG_NUMBER('(', RedisStateMachine::handleBigNumber),

        MAP('%', RedisStateMachine::handleMap),

        SET('~', RedisStateMachine::handleSet),

        ATTRIBUTE('|', RedisStateMachine::handleAttribute),

        PUSH('>', RedisStateMachine::handlePushAndMulti),
       
        HELLO_V3('@', RedisStateMachine::handleHelloV3),

        NULL('_', RedisStateMachine::handleNull),

        BULK('$', RedisStateMachine::handleBulkAndVerbatim),

        MULTI('*', RedisStateMachine::handlePushAndMulti), BYTES('*', RedisStateMachine::handleBytes);

        final byte marker;

        private final StateHandler behavior;

        Type(char marker, StateHandler behavior) {
            this.marker = (byte) marker;
            this.behavior = behavior;
        }

        @Override
        public Result handle(RedisStateMachine rsm, State state, ByteBuf buffer, CommandOutput output,
                Consumer errorHandler) {
            return behavior.handle(rsm, state, buffer, output, errorHandler);
        }
    }

    enum Result {
        NORMAL_END, BREAK_LOOP, CONTINUE_LOOP
    }

    Type type = null;

    int count = NOT_FOUND;

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer();
        sb.append(getClass().getSimpleName());
        sb.append(" [type=").append(type);
        sb.append(", count=").append(count);
        sb.append(']');
        return sb.toString();
    }

}

继续 debug,会来到 doDecode 方法。

这里有两个核心步骤:

  • 根据读取到的第一个字节,判断是不是 RESP3。
  • 调用 状态机 中的 State.Type 枚举类,处理 handle。
  • 基于 Netty 的 Lettuce 居然是这样解析RESP协议的-17

    这里先手动解析下服务器返回的数据。

    ByteBufUtil.decodeString(buffer,0,146, Charset.defaultCharset());
    %7
    $6
    server
    $5
    redis
    $7
    version
    $6
    6.0.12
    $5
    proto
    :3
    $2
    id
    :74
    $4
    mode
    $10
    standalone
    $4
    role
    $6
    master
    $7
    modules
    *0

    handleMap

    %7 对应的 handler 处理。

    基于 Netty 的 Lettuce 居然是这样解析RESP协议的-18图片

    后面就进入 状态机 流程判断了,上面我们拿到的数据要循环好久,就不一一列举出来了。

    $6 对应的 handler 处理。

    基于 Netty 的 Lettuce 居然是这样解析RESP协议的-19图片

    最后解析出来刚好 7 个,可以对比上面手动解析的结果验证下。

    基于 Netty 的 Lettuce 居然是这样解析RESP协议的-20图片

    小结

    到了这里,decode 的流程也完毕了,画个图总结下👇。

    基于 Netty 的 Lettuce 居然是这样解析RESP协议的-21图片

    结尾

    Lettuce 的 decode 依赖于 状态机 RedisStateMachine 实现,encode 靠 SingularArgument 实现。

    基于 Netty 的 Lettuce 居然是这样解析RESP协议的-22图片

    这次我做了两种尝试:

  • 按以往的方式,从测试例子开始 debug。
  • 思考下框架的特性,直奔主题。
  • 两种方式都收获颇丰,但第二种尝试得比较少,以后可以多多实践,站在不同的角度去思考问题。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论