引言

  

  正如之前的一篇博文,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引发的血案的更多相关文章

  1. 大数据学习day31------spark11-------1. Redis的安装和启动,2 redis客户端 3.Redis的数据类型 4. kafka(安装和常用命令)5.kafka java客户端

    1. Redis Redis是目前一个非常优秀的key-value存储系统(内存的NoSQL数据库).和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list ...

  2. 使用StackExchange.Redis客户端进行Redis访问出现的Timeout异常排查

    问题产生 这两天业务系统在redis的使用过程中,当并行客户端数量达到200+之后,产生了大量timeout异常,典型的异常信息如下: Timeout performing HVALS Parser2 ...

  3. Redis客户端ServiceStack.Redis的简单使用

    在nuget中下载ServiceStack.Redis,但是运行之后会出现一个问题: Exception: "Com.JinYiWei.Cache.RedisHelper"的类型初 ...

  4. 一文彻底理解Redis序列化协议,你也可以编写Redis客户端

    前提 最近学习Netty的时候想做一个基于Redis服务协议的编码解码模块,过程中顺便阅读了Redis服务序列化协议RESP,结合自己的理解对文档进行了翻译并且简单实现了RESP基于Java语言的解析 ...

  5. redis查看redis 客户端状态

    查看redis客户端连接 redis-cli info clients # Clients connected_clients:6000 client_longest_output_list:0 cl ...

  6. Redis客户端命令

    Redis客户端命令 Redis 命令用于在 redis 服务上执行操作. 要在 redis 服务上执行命令需要一个 redis 客户端.Redis 客户端在我们之前下载的的 redis 的安装包中. ...

  7. Redis客户端、服务端的安装以及命令操作

    目的: redis简介 redis服务端安装 redis客户端安装 redis相关命令操作 redis简介 官网下载(https://redis.io/) Redis 是完全开源免费的,遵守BSD协议 ...

  8. Redis 客户端 Jedis、lettuce 和 Redisson 对比

    Redis 支持多种语言的客户端,下面列举了部分 Redis 支持的客户端语言,大家可以通过官网查看 Redis 支持的客户端详情. C语言 C++ C# Java Python Node.js PH ...

  9. Redis 客户端重试指南

    本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可. 在互联网服务中,特别是在云环境下,网络及硬件环境复杂,所有应用程序都可能遇到暂时性故障.暂时性故障包括瞬时的网络抖动,服务暂时不可 ...

随机推荐

  1. 问题解决——warning C4503 超出修饰名的长度,名称被截断

    ========================声明============================ 本文原创,转载请注明作者和出处,并保证文章的完整性(包括本声明). 本文不定期修改完善,为 ...

  2. MyEclipse 6.5 代码自动提示功能配置教程

    1. 打开MyEclipse 6.0.1,然后“window”→“Preferences” 2. 选择“java”,展开,“Editor”,选择“Content Assist”. 3. 选择“Cont ...

  3. Windows x86/ x64 Ring3层注入Dll总结

    欢迎转载,转载请注明出处:http://www.cnblogs.com/uAreKongqi/p/6012353.html 0x00.前言 提到Dll的注入,立马能够想到的方法就有很多,比如利用远程线 ...

  4. SQLAlchemy 中文文档翻译计划

    SQLAlchemy 中文文档翻译计划已启动. Python 文档协作翻译小组人手紧缺,有兴趣的朋友可以加入我们,完全公益性质.交流群:467338606. 希望大家能够勇敢地去翻译和改进翻译.虽然我 ...

  5. 打造属于自己的vim利器

    毋庸置疑vim很强大,然而没有插件的话对于大多数人来说他的界面是很不友好的.下面简单写一下我对vim的配置 这是我的vim配置,装的插件不是很多,对我来说已经够用.左边的侧边栏是NERD插件提供的,还 ...

  6. Hive drop table batched

    if the hive version not support drop table tablename purge. your drop table command will move data t ...

  7. java Annotation Demo

    Java 1.5引入了annotation,这个功能非常好用,是用c#等语言借鉴过来的一个特性. 首先编译器本身支持一些像overrides,supresswarning之类的注解. Spring,j ...

  8. Hive remote install mode (mysql) step by step

    Prerequisite: Hadoop cluster is avalable; Mysql installed on namenode; Step1: download the latest hi ...

  9. BI Project Managerment

    Design doc is the meta data of the code. The project management plan is crucial to your project sinc ...

  10. Webpack+React+ES6入门指南[转]

    React无疑是今年最火的前端框架,github上的star直逼30,000,基于React的React Native的star也直逼20,000.有了React,组件化似乎不再步履蹒跚,有了Reac ...