本次解析一下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,即命令解析完成;
详细请参看流程图
源码解析
功能入口:channelRead
- 获取msg
- 根据input是否可读、是否被释放判断是否返回
- 将input的数据写入到buffer中(bufferCommandHandler中成员变量);如果buffer的内存大小不足以装下input的数据,会自动扩容;
- 写入后,开始解码
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 整体框架
- 判断是否进行解码,(stack不为空、且buffer可读)
- 调用RedisStateMachine.decode进行解码
- 若decode失败,则查看是否丢弃;discardReadBytesIfNecessary,然后返回
- 解析成功, 查看是否在protectMode;如果是ProtectMode,则设置Error信息
- 若不是,则查看是否已经成功了,将命令弹出站,同时回调CommandOutput.complete回调通知后续事件
- 做一些命令完成后的清理动作;
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
- 如果第一次进入RSM,那么会往stack中加入一个新创建的State对象
- 做一些非空判断
- 进行While循环,进行判断了;
- 首先从stack的栈顶peek一个元素,如果type=null,则读取第一个字节进行判断是什么类型数据;
- 然后进行switch-case进行判断走哪种类型的解析流程;值得注意的是:Bulk类型会再走一遍BYTE流程,Array类型,会根据Array里面的类型多走几遍;
- 如果命令还没有传递完全,那么就会导致命令解析失败;同时这个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
- 计算当前已经读到的数据量占总量的百分比
- 如果已经使用的数据大于要丢弃的比率(默认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版本对比
- CommandHandler.decode的过程增加了pubsub功能流程解析
- RedisStateMachine针对RESP的解析更加优雅,不再使用switch-case;实现更加优雅,利用Enum封装每种类型以及其对应的解析方式;
- 针对于afterDecode,为了增强其扩展性,引入了DecodeBufferPolicy;
总结
- 即便是大佬,其代码实现也有不足之处,经典的代码总是在迭代中不断地优化,这也就是一种对代码负责的表现;
- 对代码怀抱一种热忱,正如孔子所言:知之好之乐之;