記一次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,constchar*format, va_list ap){-
constchar*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,constchar**argv,-
constsize_t*argvlen) {sds cmd;-
unsignedlonglong 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]);// ------ 確定這個是否用的strlentotlen += 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]);// --------確定這裡是不是錯用了strlencmd = 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,方便快捷。

