今天来分享 Lettuce —— 基于 Netty 实现,Springboot2 中默认的 redis 客户端。
那它是不是直接用 Netty 中的那几个 handler 来处理 RESP 协议的呢?一起看看吧。
可以看到这里并没有 codec-redis 模块,所以 Lettuce 并没有使用 Netty 提供的 redis 模块。
图片
(⊙﹏⊙),问题解决得太快了,那就再来思考下,它是怎么做的呢?
既然 Lettuce 基于 Netty 实现,那么它必然在 ChannelHandler 上动手脚,直接搜索可以发现有 9 个实现类。
图片
这里我关心的就是它怎么编解码,所以直接来看 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
图片
如图,直接来到 nioEventLoop 线程,并调用了 write 方法。
write:382, CommandHandler (io.lettuce.core.protocol)
从右边可以看到,发了一个 HELLO 的命令出去,其中 CommandArgs 如下:
CommandArgs [buffer=$1
3
$4
AUTH
$7
default
$6
123456
]
CommandArgs⭐
直接来到 toString 方法,可以发现 encode 方法。
图片
如图,有 4 个 SingularArgument:
图片
看看他们是怎么 encode 的 。
ProtocolKeywordArgument
图片
StringArgument
图片
对比 Netty
图片
貌似没啥大的区别,可以看到 Lettuce 中,对 ByteBuf 的使用比较粗一些,Netty 中会计算这个 ByteBuf 的初始容量,而 Lettuce 就简单些处理,直接 singularArguments.size() * 10 。
还有一个 大小端序 的处理,只能说 Netty 太细了。
图片
CommandEncoder
直接 F9 来到这一个断点。
图片
继续 debug ,会来到 Command 类,在这里完成对发送数据的 encode。
图片
解析下要发送的数据。
图片
小结
那么到了这里,我们就了解完 encode 的实现了。
核心:CommandArgs 中的各种 SingularArgument
图片
下面就是接受服务器数据,进行 decode 的流程了。
CommandHandler
来到 channelRead 。
图片
decode 时,会调用到 RedisStateMachine 的 decode ,它是这个流程的核心。
图片
RedisStateMachine⭐
Redis 状态机:
图片
这里我直接 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 方法。
这里有两个核心步骤:
这里先手动解析下服务器返回的数据。
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 处理。
图片
后面就进入 状态机 流程判断了,上面我们拿到的数据要循环好久,就不一一列举出来了。
$6 对应的 handler 处理。
图片
最后解析出来刚好 7 个,可以对比上面手动解析的结果验证下。
图片
小结
到了这里,decode 的流程也完毕了,画个图总结下👇。
图片
结尾
Lettuce 的 decode 依赖于 状态机 RedisStateMachine 实现,encode 靠 SingularArgument 实现。
图片
这次我做了两种尝试:
两种方式都收获颇丰,但第二种尝试得比较少,以后可以多多实践,站在不同的角度去思考问题。