记一次redis命令异常:参数截断
- 2019 年 12 月 10 日
- 筆記
# 问题背景
最近项目在使用linux平台c++做开发,redis用到了hiredis库。项目中用到redis list结构作为队列,生产者和消费者模式解耦异步任务:
生产者:
1. 将业务pb结构序列化为字符串 pbstr
2. 将字符串通过 rpush list-queue pbstr
消费者:
1. 从list-queue获取任务:lpop list-queue 获得字符串 pbstr
2. 将pbstr反向序列化为pb结构,执行业务逻辑
遇到问题:
消费者在步骤2中,获取到的pbstr反序列化为pb结构失败了!!!导致消费者后续的业务逻辑无法处理。
# 排查思路
1. 怀疑序列化问题,单独从业务层面对pb结构进行序列pbstr,然后在将pbstr反向序列化为pb结构,没有遇到问题,排除pb的问题。
2. 怀疑redis队列除了问题。有一下几个排查思路:
a. 系统多线程,比较难调试。
b. strace 对进程进行跟踪,比较容易,本文采用这种方法。
工具:strace -p [pid] -s 1024 -o s.out
# 发现问题
图1是pb转为一个pbstr字符串:m_msgBody, 可见序列化后的长度是1029

图2是执行的redis命令,这里说一下redis命令的协议格式:
*[命令行参数个数]rn$[参数1长度]rn[参数1字符串]rn$[参数2长度]rn[参数2字符串]rn
例如:
RPUSH mylist Lippman
redis网络传输的命令传如下:
"*3rn$5rnRPUSHrn$6rnmylistrn$7rnLippmanrn"

从图2看出,我们的1029长度的消息,莫名其妙变为了97!!!
# 问题解决
结合代码层面的命令行拼接方式是基于字符串的fmt方式,怀疑是业务pb本身某些字段含有, 导致序列化后的字符串被截断了。

做个c预研字符串fmt遇到/0的实验:实验可以验证,
字符串 s = “abcdednxxxxxxxxxxxxx”
s.length=21
s.size=21。因为C++类中的字符串长度是记录buffer使用的实际字节长度。
strlen(s.c_str())=7。 因为C语言以作为字符串结束符。
字符串通过printf("%s", s.c_str) 结果只打印了 abcdedn。因为遇到被截断了

## hiredis的两种命令行形式
方式1:redisvFormatCommand
从如下代码可看出,字符串的结束判定是
“`
int redisvFormatCommand(char
**target,
const
char
*format, va_list ap)
{
-
const
char
*c = format;
-
...
-
while(*c !=
'')
{
-
if
(*c !=
'%'
|| c[1]
==
'')
{
-
...
-
switch(c[1])
{
-
case
's':
arg = va_arg(ap,char*);
size = strlen(arg);
// strlen 以判定字符串结束,所以如果字符串乱码,可能被判定为
-
if
(size >
0)
newarg = sdscatlen(curarg,arg,size);
-
break;
-
case
'b':
arg = va_arg(ap,char*);
size = va_arg(ap,size_t);
-
if
(size >
0)
newarg = sdscatlen(curarg,arg,size);
-
break;
-
case
'%':
newarg = sdscat(curarg,"%");
-
break;
-
...
}
“`
方式2 redisFormatSdsCommandArgv
从如下代码可看出,字符串的拼接使用的是strcat+字符串实际长度。
“`
/* Format a command according to the Redis protocol using an sds string and
* sdscatfmt for the processing of arguments. This function takes the
* number of arguments, an array with arguments and an array with their
* lengths. If the latter is set to NULL, strlen will be used to compute the
* argument lengths.
*/
int redisFormatSdsCommandArgv(sds *target,
int argc,
const
char
**argv,
-
const
size_t
*argvlen)
{
sds cmd;
-
unsigned
long
long totlen;
-
int j;
-
size_t len;
-
/* Abort on a NULL target */
-
if
(target == NULL)
-
return
-1;
-
/* Calculate our total size */
totlen =
1+countDigits(argc)+2;
-
for
(j =
0; j < argc; j++)
{
len = argvlen ? argvlen[j]
: strlen(argv[j]);
// ------ 确定这个是否用的strlen
totlen += bulklen(len);
-
}
-
/* Use an SDS string for command construction */
cmd = sdsempty();
-
if
(cmd == NULL)
-
return
-1;
-
/* We already know how much storage we need */
cmd = sdsMakeRoomFor(cmd, totlen);
-
if
(cmd == NULL)
-
return
-1;
-
/* Construct command */
cmd = sdscatfmt(cmd,
"*%irn", argc);
-
for
(j=0; j < argc; j++)
{
len = argvlen ? argvlen[j]
: strlen(argv[j]);
// --------确定这里是不是错用了strlen
cmd = sdscatfmt(cmd,
"$%Trn", len);
cmd = sdscatlen(cmd, argv[j], len);
cmd = sdscatlen(cmd,
"rn",
sizeof("rn")-1);
-
}
-
assert(sdslen(cmd)==totlen);
-
*target = cmd;
-
return totlen;
}
“`
## 解决方法:
业务代码切换为第二种方式进行命令拼接,如下所示:

# 总结
1。 业务在做redis命令拼接的时候,尽量避免%s形式,除非能保证字符串不会被截断。
2。业务代码抓包可以使用strace,方便快捷。