Lettuce中的CommandHandler源码解析以及高低版本对比

21 阅读6分钟

本次解析一下Lettuce5.2.0版本的CommandHandler中针对Redis的命令的响应结构进行解析的流程 (最新的Lettuce版本添加了RESP3.0的内容,支持的范围更加广泛,但整体逻辑是没有变的)

前置背景知识

背景知识一:RESP

RESP2.0中,Redis的命令格式一共有五种

类型含义
-error
!代表整型数字
+简单字符串
$复杂字符串
*数组类型

举例:
整型

!123\r\n

简单字符串

+OK

复杂字符串

$3\r\nabc

数组类型 以 setex key 100 value 为例

*4\r\n$5\r\nsetex\r\n$3key\r\n!100\r\n$5\r\nvalue

换种方式:(把回车换行形象化)
*4
$5
setex
$3
key
!100
$5
value

背景知识二:Netty的Handler

Netty是一款高性能的网络通信框架,通过EventLoop机制进行收发网络包,通过ChannelPipeline将ChannelHandler串联起来,达到对网络包编解码、过滤、增强等功能;其中Lettuce是依靠于Netty来做的网络通信,基于Netty提供的Handler机制做的命令的编解码工作,其中CommandHandler就是对Redis Server的返回结果进行做解码工作的;CommandEncoder对命令进行编码

原理

一句话原理

通过从Netty的Unsafe中接收到数据后,通过ChannelPipeline传递到CommandHandler、RedisStateMachine中进行解码工作,解析成功后,将其放到的CommandOutput,即命令解析完成;

详细请参看流程图

image.png

源码解析

功能入口:channelRead

  1. 获取msg
  2. 根据input是否可读、是否被释放判断是否返回
  3. 将input的数据写入到buffer中(bufferCommandHandler中成员变量);如果buffer的内存大小不足以装下input的数据,会自动扩容;
  4. 写入后,开始解码
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

    ByteBuf input = (ByteBuf) msg;

    if (!input.isReadable() || input.refCnt() == 0) {
        return;
    }

    try {
        if (buffer.refCnt() < 1) {
            return;
        }
       ......

        buffer.writeBytes(input);

        decode(ctx, buffer);
    } finally {
        input.release();
    }
}

decode 整体框架

  1. 判断是否进行解码,(stack不为空、且buffer可读)
  2. 调用RedisStateMachine.decode进行解码
  3. 若decode失败,则查看是否丢弃;discardReadBytesIfNecessary,然后返回
  4. 解析成功, 查看是否在protectMode;如果是ProtectMode,则设置Error信息
  5. 若不是,则查看是否已经成功了,将命令弹出站,同时回调CommandOutput.complete回调通知后续事件
  6. 做一些命令完成后的清理动作;
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer) throws InterruptedException {
    ......

    while (canDecode(buffer)) {

        RedisCommand<?, ?, ?> command = stack.peek();

        pristine = false;

        try {
            if (!decode(ctx, buffer, command)) {
                discardReadBytesIfNecessary(buffer);
                return;
            }
        } catch (Exception e) {

            ctx.close();
            throw e;
        }

        if (isProtectedMode(command)) {
            onProtectedMode(command.getOutput().getError());
        } else {

            if (canComplete(command)) {
                stack.poll();

                try {
                    complete(command);

                } catch (Exception e) {
                    logger.warn("{} Unexpected exception during request: {}", logPrefix, e.toString(), e);
                }
            }
        }

        afterDecode(ctx, command);
    }

    discardReadBytesIfNecessary(buffer);
}

针对RESP格式,进行解析命令:RSM.decode

  1. 如果第一次进入RSM,那么会往stack中加入一个新创建的State对象
  2. 做一些非空判断
  3. 进行While循环,进行判断了;
  4. 首先从stack的栈顶peek一个元素,如果type=null,则读取第一个字节进行判断是什么类型数据;
  5. 然后进行switch-case进行判断走哪种类型的解析流程;值得注意的是:Bulk类型会再走一遍BYTE流程,Array类型,会根据Array里面的类型多走几遍;
  6. 如果命令还没有传递完全,那么就会导致命令解析失败;同时这个state不会从stack中弹出,而是继续保存;等待下一次数据的解析;
public boolean decode(ByteBuf buffer, RedisCommand<?, ?, ?> command, CommandOutput<?, ?, ?> output) {
    int length, end;
    ByteBuffer bytes;

    if (debugEnabled) {
        logger.debug("Decode {}", command);
    }
    // 初始化的时候,将命令添加到stack中
    if (isEmpty(stack)) {
        add(stack, new State());
    }

    if (output == null) {
        return isEmpty(stack);
    }

    loop:

    while (!isEmpty(stack)) {
        State state = peek(stack);
        // 如果是初始化状态
        if (state.type == null) {
            if (!buffer.isReadable()) {
                break;
            }
            // 解析第一个字符,判断类型;
            state.type = readReplyType(buffer);
            // 打标
            buffer.markReaderIndex();
        }
        // 开始根据类型进行判断

        switch (state.type) {
            // 简单字符串类型
            case SINGLE:
                // 读取一行(以\r\n结尾),没读取到,直接跳出循环,结束;
                if ((bytes = readLine(buffer)) == null) {
                    break loop;
                }
                // 设置结果
                if (!QUEUED.equals(bytes)) {
                    safeSetSingle(output, bytes, command);
                }
                break;
            case ERROR:
                if ((bytes = readLine(buffer)) == null) {
                    break loop;
                }
                safeSetError(output, bytes, command);
                break;
            case INTEGER:
                // 找到\r\n的索引位置,没找到,跳出循环,结束
                if ((end = findLineEnd(buffer)) == -1) {
                    break loop;
                }
                // 根据Processor进行解析,设置
                long integer = readLong(buffer, buffer.readerIndex(), end);
                // 设定
                safeSet(output, integer, command);
                break;
            case BULK:
                // 解析第一行
                if ((end = findLineEnd(buffer)) == -1) {
                    break loop;
                }
                length = (int) readLong(buffer, buffer.readerIndex(), end);
                // 如果长度=-1,就设置结果;
                if (length == -1) {
                    safeSet(output, null, command);
                } else {
                    //否则就进行打标,然后设置BYTES类型,重新进行解析
                    state.type = BYTES;
                    state.count = length + 2;
                    buffer.markReaderIndex();
                    continue loop;
                }
                break;
            case MULTI:
                // ARRAY 类型,如果初始化状态,
                if (state.count == -1) {
                    // 解析第一行,解析失败,跳出循环,否则获取Array类型的长度
                    if ((end = findLineEnd(buffer)) == -1) {
                        break loop;
                    }
                    length = (int) readLong(buffer, buffer.readerIndex(), end);
                    // 这里的count表示的Array类型中元素的个数;
                    state.count = length;
                    buffer.markReaderIndex();
                    // 将count设置到multi中
                    safeMulti(output, state.count, command);
                }
                // 如果Array中的元素解析完以后,就跳出switch-case流程

                if (state.count <= 0) {
                    break;
                }
                // 否则就继续解析Array中的元素,将count--,创建一个新的state,代表即将要解析的下一个元素,然后继续循环

                state.count--;
                addFirst(stack, new State());

                // 继续循环
                continue loop;
            case BYTES:
                // 单纯是解析字符串,这个count代表读取字符串的长度,因为这个BYTES只能从BULK类型进来;而且这个count是包含的\r\n的长度
                if ((bytes = readBytes(buffer, state.count)) == null) {
                    break loop;
                }
                safeSet(output, bytes, command);
                break;
            default:
                throw new IllegalStateException("State " + state.type + " not supported");
        }
        // 当switch-case结束后, 就会打个标记;
        buffer.markReaderIndex();
        // 从stack中移除,表示这个命令已经执行完成了;
        remove(stack);

        output.complete(size(stack));
    }

    if (debugEnabled) {
        logger.debug("Decoded {}, empty stack: {}", command, isEmpty(stack));
    }
    //用于判断是否解析完成,如果stack里面还有数据,说明该条命令没有解析完成,说明有数据没有还没有到达;
    return isEmpty(stack);
}

discardReadBytesIfNecessary

  1. 计算当前已经读到的数据量占总量的百分比
  2. 如果已经使用的数据大于要丢弃的比率(默认75%),则会执行丢弃策略;

可能大家会有疑问? 如果一个命令的字符数特别多,多次请求解析都没有解析完成,此时已经使用的字节数大于了75%,如果丢弃,岂不是造成数据错乱了吗? 答案就在每个命令解析的时候,只是提前预读取,并不会移动readerIndex指针,而这个discardReadBytes是将[0,readerIndex)范围内的数据清空;所以才没有问题;

private void discardReadBytesIfNecessary(ByteBuf buffer) {

    float usedRatio = (float) buffer.readerIndex() / buffer.capacity();

    if (usedRatio >= discardReadBytesRatio && buffer.refCnt() != 0) {
        buffer.discardReadBytes();
    }
}

complete

protected void complete(RedisCommand<?, ?, ?> command) {
    command.complete();
}

和6.5.0版本对比

  1. CommandHandler.decode的过程增加了pubsub功能流程解析
  2. RedisStateMachine针对RESP的解析更加优雅,不再使用switch-case;实现更加优雅,利用Enum封装每种类型以及其对应的解析方式;
  3. 针对于afterDecode,为了增强其扩展性,引入了DecodeBufferPolicy;

总结

  1. 即便是大佬,其代码实现也有不足之处,经典的代码总是在迭代中不断地优化,这也就是一种对代码负责的表现;
  2. 对代码怀抱一种热忱,正如孔子所言:知之好之乐之;