从零开始写redis客户端(deerlet-redis-client)之路——第一个纠结很久的问题,restore引发的血案
引言
正如之前的一篇博文,LZ最近正在从零开始写一个redis的客户端,主要目的是为了更加深入的了解redis,当然了,LZ也希望deerlet客户端有一天能有一席之地。在写的过程当中,LZ遇到了一个非常奇葩的问题。虽然现在看起来是一个非常低级的错误,但是在未打开这个谜底之前,着实让LZ抓耳挠腮了一番,毕竟难者不会嘛。
接下来,大家就来一起看下到底是什么问题吧。
restore命令的奇葩之处
刚开始写redis客户端时,LZ只支持了一些常用的命令,比如get,set。初次写这个客户端时,LZ采取的办法就是使用Socket和服务器进行TCP通信,传输的内容就是模拟在telnet端输入的命令。比如在telnet端使用set和get命令时,是如下的方式。

因此在写deerlet时,LZ也是模仿的这种方式。比如在往服务器发送set命令时,LZ会采取以下的方式。
if (command.name().indexOf(COMMAND_SEPARATOR) > 0) {
String[] commands = command.name().split(COMMAND_SEPARATOR);
outputStream.writeObject(commands[0]);
outputStream.writeSpace();
outputStream.writeObject(commands[1]);
} else {
outputStream.writeObject(command.name());
}
if (arguments != null) {
for (int i = 0; i < arguments.length; i++) {
outputStream.writeSpace();
outputStream.writeObject(arguments[i]);
}
}
outputStream.writeEnter();
outputStream.flush();
这段代码的逻辑很简单,也是LZ目前deerlet客户端当中统一的发送命令的方法。这段代码的逻辑如下。
1,如果命令不包含下划线(_),则直接写入命令。否则的话,将下划线分割的两个命令依次写入,中间加一个空格('\r'),比如script_flush命令。
2,写入命令后,如果参数不为空,则循环写入参数,每个参数用空格隔开。
3,结束时,写入一个回车符('\n')。
所以,如果是set命令的话,假设我们设置someKey的值为value,那么这段代码写入的实际内容就是如下这个字节数组。
['s', 'e', 't', '\r', 's', 'o', 'm', 'e', 'K', 'e', 'y', '\r', '\'', 'v', 'a', 'l', 'u', 'e', '\'', '\n']
实践证明,这种方式支持很多redis的命令,比如get,set,flushall等等。这些命令,LZ的单元测试都完美通过。
但是,问题来了,当LZ试图加入restore命令的支持时,竟然不管怎样都不行。这对于初次研究redis的LZ来说,真的是一个梦魇。因为尝试了各种办法,都无法让restore的单元测试通过,而且最要命的是,因为restore命令的参数中有字节数组,因此LZ无法在telnet端进行测试。
求助于“专业人士”
LZ最后实在没办法了,只能求助于“专业人士”。只不过不同的是,这个“专业人士”并不是某一个人,而是jedis。是的,LZ去翻阅了jedis的源码。
jedis作为redis比较知名的java客户端,对于LZ来说,肯定是有一定的参考价值的。只不过为了保证deerlet是纯净的,因此LZ一开始没有去翻阅jedis的源码,避免思维受到影响,最终把deerlet写的和jedis如出一辙。
不过现在遇到了这么奇葩的问题,而且迟迟没有解决,LZ也就顾不上那么多了。在深入研究了jedis的源码之后,LZ发现jedis发送命令的核心代码是以下这段代码。
try {
write(ASTERISK_BYTE);
writeIntCrLf(args.length + 1);
write(DOLLAR_BYTE);
writeIntCrLf(command.length);
write(command);
writeCrLf();
for (final byte[] arg : args) {
write(DOLLAR_BYTE);
writeIntCrLf(arg.length);
write(arg);
writeCrLf();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
同样的,假设还是set命令,同样的参数,jedis发送的数据是以下这种形式的。
*3
$3
set
$7
someKey
$5
value
以上的数据,如果转换成字节数组的话,是如下的形式。
['*', '3', '\r', '\n', '$', '3', '\r', '\n', 's', 'e', 't', '\r', '\n', '$', '7', '\r', '\n', 's', 'o', 'm', 'e', 'K', 'e', 'y', '\r', '\n', '$', '5', '\r', '\n', 'v', 'a', 'l', 'u', 'e', '\r', '\n']
LZ这里对以上的数据格式做一个简单的介绍。星号(*)后面的3代表的是有三个参数。第一个美元符号($)后面的3是代表的set的长度,以此类推,第四行的美元符号后面的7代表的是someKey的长度。jedis就是把这么一个字符串发送给了服务器,让LZ惊讶的是,使用这种方式去进行restore命令的操作,服务器竟然正确的返回了响应。
为什么这么一大串看似规整但又看似杂乱的命令,redis服务器会正确的返回结果呢?
从问题的本质出发
因为LZ实在想不通为什么redis会接受两种形式的命令,而且就算是redis接受,LZ也不明白为什么偏偏restore就不行。
无奈之下,LZ只好从问题的本质出发。是的,LZ去翻阅了redis的源码。为此,LZ还专门在自己的Mac上面下载了xcode,学习了一番lldb,去尝试跟踪redis的服务器代码。
经过一番折腾,LZ终于找到了根源。请看如下的代码,以下代码来自于networking.c。
void processInputBuffer(redisClient *c) {
server.current_client = c;
/* Keep processing while there is something in the input buffer */
while(sdslen(c->querybuf)) {
/* Return if clients are paused. */
if (!(c->flags & REDIS_SLAVE) && clientsArePaused()) break;
/* Immediately abort if the client is in the middle of something. */
if (c->flags & REDIS_BLOCKED) break;
/* REDIS_CLOSE_AFTER_REPLY closes the connection once the reply is
* written to the client. Make sure to not let the reply grow after
* this flag has been set (i.e. don't process more commands). */
if (c->flags & REDIS_CLOSE_AFTER_REPLY) break;
/* Determine request type when unknown. */
if (!c->reqtype) {
if (c->querybuf[] == '*') {
c->reqtype = REDIS_REQ_MULTIBULK;
} else {
c->reqtype = REDIS_REQ_INLINE;
}
}
if (c->reqtype == REDIS_REQ_INLINE) {
if (processInlineBuffer(c) != REDIS_OK) break;
} else if (c->reqtype == REDIS_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != REDIS_OK) break;
} else {
redisPanic("Unknown request type");
}
/* Multibulk processing could see a <= 0 length. */
if (c->argc == ) {
resetClient(c);
} else {
/* Only reset the client when the command was executed. */
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
}
server.current_client = NULL;
}
请注意循环当中的一句注释“Determine request type when unknown”,处在它下面的if判断,判断了命令的开头是否是星号(*)开头,并根据判断的结果,赋予了相应的类型——inline和multibulk。接下来,程序会根据命令的类型,分别调用相应的处理方法processInlineBuffer和processMultibulkBuffer。
知道这个以后,LZ去翻阅了redis的官方文档,找到这样一句话,是用来解释inline格式的。
Sometimes you have only telnet in your hands and you need to send a command to the Redis server. While the Redis protocol is simple
to implement it is not ideal to use in interactive sessions, and redis-cli may not always be available. For this reason Redis also
accepts commands in a special way that is designed for humans, and is called the inline command format.
这段话简单翻译过来就是:有时你可能只有telnet,并且你需要给redis服务器发送命令。redis的协议在交互式会话当中使用起来并不理想,而且redis-cli也不总是好用的。因此redis就专门为此设计了一套特殊的命令方式,称之为inline命令格式。
总的来说,这下LZ总算是彻底明白了。inline协议,也就是deerlet客户端之前所使用的协议是redis为交互式会话提供的(比如telnet),主要目的是为了操作方便。如果要想做应用之间的交互,还是要使用multibulk协议,比如jedis在发送命令时,格式就是遵循multibulk协议的。如果大家想了解更多关于resp(即redis序列化协议)的内容,可以翻阅官方文档(地址:http://www.redis.io/topics/protocol),LZ这里就不再多做介绍了,只是起到一个抛砖引玉的作用。
水落石出
知道了以上内容,就不难去测试为什么restore命令不能使用了。我们可以猜想出来,之前restore单元测试失败的原因大概是因为dump后的字节数组中包含了空格字符。为了确认我们的猜测是正确的,LZ将dump命令执行后的数组在程序中打印了出来,如下。
['', ' ', 'T', 'e', 's', 't', 'V', 'a', 'l', 'u', 'e', '', '', '(', 'B', 'ᄁ', 'ヨ', 'ᅩ', 'ム', 'ᅧ', '!']
可以看到,第二个字符是一个空格字符,因此在使用inline格式发送时,会导致redis服务器进行错误的解析,它会把一个参数当作两个参数去解析,最终导致参数的数量不符合命令要求。
这里也能够看出来,inline协议的好处在于方便简单,但是坏处也很明显,就是在某些情况下会导致出错,比如当传输的参数内容当中包含空格时就会导致redis解析失败。
小结
经过这一番问题的查找,可以看出,翻阅源码(如果有的话)是最有效直接的问题解决方式。LZ也建议大家,在遇到问题的时候,不要着急着百度,尝试去翻阅一下源码,这样能够帮助你对遇到的问题有一个比较深入的了解,以后再遇到的话,你将会游刃有余。
好了,本文就到此结束,感谢大家的收看,如果deerlet再遇到问题的话,LZ再来与大家一起分享,也非常欢迎有志之士为deerlet贡献源码。
从零开始写redis客户端(deerlet-redis-client)之路——第一个纠结很久的问题,restore引发的血案的更多相关文章
- 大数据学习day31------spark11-------1. Redis的安装和启动,2 redis客户端 3.Redis的数据类型 4. kafka(安装和常用命令)5.kafka java客户端
1. Redis Redis是目前一个非常优秀的key-value存储系统(内存的NoSQL数据库).和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list ...
- 使用StackExchange.Redis客户端进行Redis访问出现的Timeout异常排查
问题产生 这两天业务系统在redis的使用过程中,当并行客户端数量达到200+之后,产生了大量timeout异常,典型的异常信息如下: Timeout performing HVALS Parser2 ...
- Redis客户端ServiceStack.Redis的简单使用
在nuget中下载ServiceStack.Redis,但是运行之后会出现一个问题: Exception: "Com.JinYiWei.Cache.RedisHelper"的类型初 ...
- 一文彻底理解Redis序列化协议,你也可以编写Redis客户端
前提 最近学习Netty的时候想做一个基于Redis服务协议的编码解码模块,过程中顺便阅读了Redis服务序列化协议RESP,结合自己的理解对文档进行了翻译并且简单实现了RESP基于Java语言的解析 ...
- redis查看redis 客户端状态
查看redis客户端连接 redis-cli info clients # Clients connected_clients:6000 client_longest_output_list:0 cl ...
- Redis客户端命令
Redis客户端命令 Redis 命令用于在 redis 服务上执行操作. 要在 redis 服务上执行命令需要一个 redis 客户端.Redis 客户端在我们之前下载的的 redis 的安装包中. ...
- Redis客户端、服务端的安装以及命令操作
目的: redis简介 redis服务端安装 redis客户端安装 redis相关命令操作 redis简介 官网下载(https://redis.io/) Redis 是完全开源免费的,遵守BSD协议 ...
- Redis 客户端 Jedis、lettuce 和 Redisson 对比
Redis 支持多种语言的客户端,下面列举了部分 Redis 支持的客户端语言,大家可以通过官网查看 Redis 支持的客户端详情. C语言 C++ C# Java Python Node.js PH ...
- Redis 客户端重试指南
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可. 在互联网服务中,特别是在云环境下,网络及硬件环境复杂,所有应用程序都可能遇到暂时性故障.暂时性故障包括瞬时的网络抖动,服务暂时不可 ...
随机推荐
- MyCat 学习笔记 第十三篇.数据分片 之 通过HINT执行存储过程
1 环境说明 VM 模拟3台MYSQL 5.6 服务器 VM1 192.168.31.187:3307 VM2 192.168.31.212:3307 VM3 192.168.31.150: 330 ...
- javascript 特效实现(3)—— 鼠标滑过显示二级菜单效果
1. 关键代码:使用 switch 或 if 判断语句,改变对应的二级菜单显示方式为 block 或 none function selectTabMenu(i){ switch(i){ case 7 ...
- APACHE重写去除入口文件index.php
下面我说下 apache 下 ,如何 去掉URL 里面的 index.php 例如: 你原来的路径是: localhost/index.php/index 改变后的路径是: localhost/ind ...
- 对Jena的简单理解和一个例子
本文简单介绍Jena(Jena 2.4),使用Protégé 3.1(不是最新版本)创建一个简单的生物(Creature)本体,然后参照Jena文档中的一个例子对本体进行简单的处理,输出本体中的Cla ...
- kvm解决1000M网卡问题
1.当我们安装完虚拟机, 发现虚拟机竟然是 100M 网络, 传输速率很低, 那是怎么导致的呢,如何来解决呢? 需要我们修改 vm01.xml 配置文件网卡段,添加如下红色标记行,改 为 e1000, ...
- vim自定义配色方案,图文并茂
1.先上图 下面是tcpdump的源码.颜色根据自己的喜好配置,我比较喜欢亮的颜色,看的清楚! 2.下载辅助配置文件 ...
- 浅谈export 以及环境变量
简要说一下env,set,export的区别:env命令显示环境变量,set和export显示环境变量和自定变量. export:可以讲自定变量转化为环境变量之前有一个疑惑,我们定义环境变量PATH时 ...
- [转]Google Cloud Messaging: Overview
本文转自:https://developers.google.com/cloud-messaging/gcm
- CANopen DS301协议中文翻译V03版
V0.1版PDF格式供下载参考,只是全面框架翻译,会有大量错误和不确定的地方,希望读者积极参与校对,提供修改意见,完善译文.下载 V0.2版校对提前完成,下载地址 V0.3版使用GitBook编辑(h ...
- runc的detach, console, tty等相关问题
runc 端解析: 1. runc/utils_linux.go func (r *runner) run(config *specs.Process) (int , error) 在该函数中第一次对 ...