前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次redis命令异常:参数截断

记一次redis命令异常:参数截断

原创
作者头像
大平
发布2019-12-10 22:12:49
1.9K0
发布2019-12-10 22:12:49
举报
文章被收录于专栏:大平的专栏

# 问题背景

最近项目在使用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

图1
图1

图2是执行的redis命令,这里说一下redis命令的协议格式:

*[命令行参数个数]\r\n$[参数1长度]\r\n[参数1字符串]\r\n$[参数2长度]\r\n[参数2字符串]\r\n

例如:

RPUSH mylist Lippman

redis网络传输的命令传如下:

"*3\r\n$5\r\nRPUSH\r\n$6\r\nmylist\r\n$7\r\nLippman\r\n"

图2
图2

从图2看出,我们的1029长度的消息,莫名其妙变为了97!!!

# 问题解决

结合代码层面的命令行拼接方式是基于字符串的fmt方式,怀疑是业务pb本身某些字段含有\0, 导致序列化后的字符串被截断了。

做个c预研字符串fmt遇到/0的实验:实验可以验证,

字符串 s = “abcded\n\0xxxxxxxxxxxxx”

s.length=21

s.size=21。因为C++类中的字符串长度是记录buffer使用的实际字节长度。

strlen(s.c_str())=7。 因为C语言以\0作为字符串结束符。

字符串通过printf("%s", s.c_str) 结果只打印了 abcded\n。因为遇到\0被截断了

## hiredis的两种命令行形式

方式1:redisvFormatCommand

从如下代码可看出,字符串的结束判定是\0

```

  1. int redisvFormatCommand(char **target, const char *format, va_list ap) {
  2. const char *c = format;
  3. ...
  4. while(*c != '\0') {
  5. if (*c != '%' || c[1] == '\0') {
  6. ...
  7. switch(c[1]) {
  8. case 's':
  9. arg = va_arg(ap,char*);
  10. size = strlen(arg); // strlen 以\0判定字符串结束,所以如果字符串乱码,可能被判定为\0
  11. if (size > 0)
  12. newarg = sdscatlen(curarg,arg,size);
  13. break;
  14. case 'b':
  15. arg = va_arg(ap,char*);
  16. size = va_arg(ap,size_t);
  17. if (size > 0)
  18. newarg = sdscatlen(curarg,arg,size);
  19. break;
  20. case '%':
  21. newarg = sdscat(curarg,"%");
  22. break;
  23. ...
  24. }

```

方式2 redisFormatSdsCommandArgv

从如下代码可看出,字符串的拼接使用的是strcat+字符串实际长度。

```

  1. /* Format a command according to the Redis protocol using an sds string and
  2. * sdscatfmt for the processing of arguments. This function takes the
  3. * number of arguments, an array with arguments and an array with their
  4. * lengths. If the latter is set to NULL, strlen will be used to compute the
  5. * argument lengths.
  6. */
  7. int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv,
  8. const size_t *argvlen)
  9. {
  10. sds cmd;
  11. unsigned long long totlen;
  12. int j;
  13. size_t len;
  14. /* Abort on a NULL target */
  15. if (target == NULL)
  16. return -1;
  17. /* Calculate our total size */
  18. totlen = 1+countDigits(argc)+2;
  19. for (j = 0; j < argc; j++) {
  20. len = argvlen ? argvlen[j] : strlen(argv[j]); // ------ 确定这个是否用的strlen
  21. totlen += bulklen(len);
  22. }
  23. /* Use an SDS string for command construction */
  24. cmd = sdsempty();
  25. if (cmd == NULL)
  26. return -1;
  27. /* We already know how much storage we need */
  28. cmd = sdsMakeRoomFor(cmd, totlen);
  29. if (cmd == NULL)
  30. return -1;
  31. /* Construct command */
  32. cmd = sdscatfmt(cmd, "*%i\r\n", argc);
  33. for (j=0; j < argc; j++) {
  34. len = argvlen ? argvlen[j] : strlen(argv[j]); // --------确定这里是不是错用了strlen
  35. cmd = sdscatfmt(cmd, "$%T\r\n", len);
  36. cmd = sdscatlen(cmd, argv[j], len);
  37. cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1);
  38. }
  39. assert(sdslen(cmd)==totlen);
  40. *target = cmd;
  41. return totlen;
  42. }

```

## 解决方法:

业务代码切换为第二种方式进行命令拼接,如下所示:

# 总结

1。 业务在做redis命令拼接的时候,尽量避免%s形式,除非能保证字符串不会被\0截断。

2。业务代码抓包可以使用strace,方便快捷。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 问题背景
  • # 发现问题
  • # 问题解决
  • # 总结
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档