简介:hiredis-0.13.1 是一份面向 C 语言开发者与 Redis 后端工程师的轻量级客户端库源码包,专注解决高并发场景下与 Redis 高效通信的问题,适合需要深入理解 Redis 协议解析、连接管理与异步 I/O 的中高级读者。压缩包共 32 个文件,约 54KB,以 13 个 .h 头文件与 13 个 .c 源文件为核心,覆盖 hiredis 主库、sds 动态字符串、dict 哈希表、net 网络层及 read 协议解析等模块,另含 Makefile 构建脚本、CHANGELOG 更新日志、README 说明与 COPYING 许可文件,并附带 libev、libevent、glib、libuv 等适配器示例,便于对照学习不同事件循环的集成方式。目前已有 228 人学习下载。通过阅读源码,读者可掌握命令发送、响应解析与错误处理的具体实现,理解其低延迟、低资源占用的设计取舍,并借助示例与变更记录评估升级收益,为构建稳定高效的 Redis 应用提供参考。
1. hiredis-0.13.1:一个老版本 C 客户端,为什么还在被反复翻出来用
如果你在维护一套跑了七八年的 C/C++ 服务,某天发现 Redis 连接层用的是 hiredis-0.13.1,第一反应大概率是「这版本也太老了,要不要升」。但真去翻代码你会发现,这个版本在很多存量系统里活得相当稳:同步 API 简单直接,异步 API 配 libevent/libev 能扛住长连接,编译出来就一个静态库,没有乱七八糟的依赖。它解决的核心问题很朴素——让 C 程序用最小的代价跟 Redis 说上话,把 RESP 协议解析、连接管理、命令拼装这些脏活包掉。
适合谁看:手里有 C/C++ 服务、需要直连 Redis 做缓存或队列的工程师;被某个老项目的连接层卡住、想搞清 hiredis 到底怎么用的人;以及想评估「要不要从 0.13.1 往上动」的维护者。这篇不讲虚的,从编译、同步调用、异步事件循环到连接池和排错,按能复现的路子走一遍。需要先说明:hiredis 的版本迭代里,0.13.1 属于 API 相对稳定、但缺少后续一些便利接口的阶段,很多网上抄来的示例代码其实是更高版本的写法,直接套会编译不过,这是后面避坑章要重点说的。
2. 把 hiredis-0.13.1 编进项目:从源码到第一个能跑的命令
2.1 为什么这个版本常被静态编进主程序
hiredis 本身是纯 C 写的,核心就几个文件:hiredis.c负责同步接口和协议解析,net.c负责 TCP/Unix socket 连接,sds.c是它自带的一套动态字符串,read.c处理回复解析。0.13.1 这个阶段的目录结构比较扁平,没有后来拆得那么细。常见做法是直接把源码目录塞进主工程的 third_party 下,用主工程的构建系统一起编,而不是依赖系统里装的 libhiredis。
这么做的理由很实际:一是避免不同机器上系统包版本不一致,二是静态链接后部署时不用再管.so依赖,三是方便在编译期打补丁(比如改超时默认值)。代价是升级时要手动替换源码目录,不能靠包管理器一键升。我一般会保留一个third_party/hiredis目录,里面放固定版本的源码,构建脚本里显式指定头文件路径。
2.2 编译命令与关键参数
假设源码已经放在third_party/hiredis,用 Makefile 编静态库:
# 进入源码目录,生成静态库和动态库 cd third_party/hiredis make static # 产物:libhiredis.a # 如果只想编静态库、跳过测试和示例,可以只 make static编完后主工程链接时这样写:
gcc -o my_service main.c \ -Ithird_party/hiredis \ -Lthird_party/hiredis \ -lhiredis \ -lpthread逻辑说明:make static只产出libhiredis.a,不产出动态库,适合静态链接场景。-I指定头文件目录,-L指定库目录,-lhiredis链接静态库。-lpthread是因为 hiredis 内部在某些路径下会用到线程相关符号,虽然同步接口本身不依赖多线程,但链接时带上更保险。
参数上要注意:hiredis 的 Makefile 支持PREFIX、CC、CFLAGS等变量。如果主工程用的是交叉编译工具链,编译 hiredis 时也要用同一套工具链,否则会出现架构不匹配的链接错误。常见做法是:
make static CC=arm-linux-gnueabihf-gcc2.3 第一个同步命令:连接、SET、GET
同步 API 是最容易上手的部分,适合请求量不大、或者对延迟不敏感的场景。下面这段代码连上 Redis,写一个键再读回来:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <hiredis/hiredis.h> int main(void) { // 连接本机 6379,超时 1.5 秒 struct timeval timeout = { 1, 500000 }; redisContext *c = redisConnectWithTimeout("127.0.0.1", 6379, timeout); if (c == NULL || c->err) { if (c) { printf("连接错误: %s\n", c->errstr); redisFree(c); } else { printf("连接分配失败\n"); } return 1; } // SET 命令,reply 用完必须 freeReplyObject redisReply *reply = (redisReply *)redisCommand(c, "SET foo bar"); if (reply == NULL) { printf("命令执行失败\n"); redisFree(c); return 1; } printf("SET 返回类型: %d\n", reply->type); freeReplyObject(reply); // GET 命令 reply = (redisReply *)redisCommand(c, "GET foo"); if (reply != NULL && reply->type == REDIS_REPLY_STRING) { printf("GET foo = %s\n", reply->str); } freeReplyObject(reply); redisFree(c); return 0; }逻辑说明:redisConnectWithTimeout返回的redisContext里带err字段,连接失败时不会返回 NULL,而是返回一个err非零的 context,所以判断要写成c == NULL || c->err。redisCommand是可变参数函数,直接传格式化字符串,内部会拼 RESP 协议发出去并阻塞等回复。redisReply的type字段决定用哪个成员取值:字符串用str,整数用integer,数组用element和elements。
参数说明:timeout是连接超时,不是命令超时。0.13.1 里同步命令本身没有单独的超时控制,如果 Redis 卡住,redisCommand会一直阻塞。这是同步模式最大的坑,后面避坑章会展开。freeReplyObject必须调用,否则每次命令都会漏一块内存,跑久了内存曲线会很难看。
2.4 命令拼装的两种写法与注入边界
redisCommand支持%s、%b、%d等格式符,但用%s拼用户输入时要注意:如果输入里带空格或特殊字符,拼出来的命令会被 Redis 当成多个参数。正确做法是用%b指定长度,或者用redisCommandArgv:
// 不推荐:用户输入含空格时会拆错参数 redisCommand(c, "SET %s %s", key, value); // 推荐:用 %b 显式指定长度 redisCommand(c, "SET %b %b", key, (size_t)strlen(key), value, (size_t)strlen(value)); // 或者用 argv 形式,参数边界最清晰 const char *argv[3] = { "SET", key, value }; size_t argvlen[3] = { 3, strlen(key), strlen(value) }; redisReply *reply = (redisReply *)redisCommandArgv(c, 3, argv, argvlen);逻辑说明:%b需要两个参数——指针和长度,这样即使内容里有空格、换行、二进制字节,也会被当成一个完整的 bulk string 发出去。redisCommandArgv更直接,把参数数组和长度数组传进去,hiredis 负责拼 RESP。参数说明:argvlen里每个元素是对应参数的长度,命令名"SET"长度是 3。这两种写法在 0.13.1 里都支持,选哪种看团队习惯,但涉及用户输入时不要用裸%s。
3. 异步模式与事件循环:把 hiredis 接进自己的网络框架
3.1 异步 API 的适用场景与选型理由
同步模式在 QPS 上千、或者单次命令可能慢(比如大 key 的KEYS、复杂ZRANGE)时就会拖住整个线程。异步模式把「发命令」和「收回复」拆开,配合事件循环,一个线程能管几百上千个连接。常见做法是主服务本身就跑在 libevent/libev/libuv 上,hiredis 的异步接口正好能挂进同一套事件循环,不用额外起线程。
选型上,0.13.1 的异步接口依赖hiredis_ae.h和对应的适配层(async.c加adapters/下的 libevent/libev 适配文件)。如果你的主框架用的是 libevent,就编adapters/libevent.h对应的适配;用 libev 就换adapters/libev.h。这一步选错,编译时符号找不到,运行时段错误,是新手最容易翻车的地方。
3.2 建立异步连接与回调注册
下面用 libevent 适配层演示一个最小异步客户端:
#include <stdio.h> #include <string.h> #include <hiredis/hiredis.h> #include <hiredis/async.h> #include <hiredis/adapters/libevent.h> #include <event2/event.h> // 收到回复时的回调 void onReply(redisAsyncContext *c, void *reply, void *privdata) { redisReply *r = (redisReply *)reply; if (r == NULL) { printf("回复为空,连接可能已断开\n"); return; } if (r->type == REDIS_REPLY_STRING) { printf("收到: %s\n", r->str); } } // 连接状态变化回调 void onConnect(const redisAsyncContext *c, int status) { if (status != REDIS_OK) { printf("异步连接失败: %s\n", c->errstr); return; } printf("异步连接建立\n"); // 连接成功后再发命令 redisAsyncCommand(c, onReply, NULL, "GET foo"); } int main(void) { struct event_base *base = event_base_new(); redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); if (c == NULL || c->err) { printf("异步连接创建失败\n"); return 1; } // 把 hiredis 挂到 libevent 事件循环 redisLibeventAttach(c, base); redisAsyncSetConnectCallback(c, onConnect); event_base_dispatch(base); redisAsyncFree(c); event_base_free(base); return 0; }逻辑说明:redisAsyncConnect只创建上下文,不阻塞等连接完成,真正的连接结果通过redisAsyncSetConnectCallback注册的回调通知。redisLibeventAttach把 hiredis 的读写事件注册到 libevent 的 base 上,之后event_base_dispatch一跑,hiredis 就能在事件触发时收发数据。redisAsyncCommand是异步发命令,立即返回,回复到了才调onReply。
参数说明:onReply的第三个参数privdata是注册时传进去的用户数据,可以用来区分不同请求。redisAsyncCommand的格式符和同步版一致,同样注意%b和%s的区别。redisAsyncFree在事件循环退出后调用,释放上下文。
3.3 断线重连与回调里的资源管理
异步模式下连接断开是常态,必须处理。0.13.1 提供redisAsyncSetDisconnectCallback,断开时触发:
void onDisconnect(const redisAsyncContext *c, int status) { if (status != REDIS_OK) { printf("异常断开: %s\n", c->errstr); // 这里不要直接 free,交给外层统一管理 } else { printf("正常断开\n"); } } // 注册 redisAsyncSetDisconnectCallback(c, onDisconnect);逻辑说明:断开回调里只做标记和日志,不要在里面直接调redisAsyncFree,因为此时 hiredis 内部可能还在清理状态,直接 free 容易出问题。常见做法是设一个标志位,回到主循环后再决定是重连还是销毁。参数说明:status为REDIS_OK表示主动断开,非 OK 表示异常断开,c->errstr里有原因。
回调里的资源管理有个血泪经验:onReply里的reply对象由 hiredis 管理,回调返回后会被释放,不要保存指针到别处异步使用。如果需要把数据传出去,在回调里拷贝一份。另外,redisAsyncCommand返回REDIS_ERR时说明命令没发出去,此时不会有回调,要自己处理失败。
3.4 异步模式下的超时控制
0.13.1 的异步接口本身不提供命令级超时。常见做法是在应用层维护一个待回复队列,配合事件循环的定时器,超时后主动断开连接或标记该请求失败。具体实现上,可以在发命令时把请求 ID 和发送时间记下来,定时器每秒扫一遍,超过阈值的请求标记为超时并触发重连。这块没有现成 API,得自己写,但逻辑不复杂,关键是别在回调里做重活。
4. 连接池、超时与协议解析:三个最容易出问题的环节
4.1 连接池该自己写还是用现成的
hiredis 本身不带连接池。同步模式下,如果每个请求都redisConnect再redisFree,TCP 握手和断开的开销会吃掉大部分性能。常见做法是自己维护一个连接池:启动时建 N 个redisContext,请求来了从池里取一个,用完还回去。池的大小按并发量和 Redis 处理能力估,一般从 8 到 32 起步,压测后再调。
自己写池要注意几点:取连接时如果池空,是阻塞等还是直接失败,取决于业务能不能接受排队;连接用久了可能被 Redis 服务端断开(timeout配置),还回来之前最好用redisReconnect或发个PING探活;池的销毁要等所有连接归还后再统一redisFree。异步模式下一般不需要连接池,因为一个异步连接就能并发处理多个请求,连接数按 Redis 实例数配就行。
4.2 超时参数到底该设多少
同步模式的超时只有连接超时,命令执行没有超时。这意味着如果 Redis 因为大 key、慢查询、网络抖动卡住,调用线程会一直挂在那里。常见缓解办法是给 Redis 客户端连接设置 socket 级别的读超时,但 0.13.1 没有直接暴露这个接口,得改net.c里的redisContextConnectTcp相关代码,或者在应用层用带超时的网络库包一层。
异步模式没有这个问题,因为收发都在事件循环里,不会阻塞线程。但异步模式要自己实现命令级超时,前面 3.4 说过。参数上,连接超时一般设 1 到 3 秒,太短会在网络抖动时频繁失败,太长会在 Redis 挂掉时拖慢启动。命令级超时按业务定,缓存读一般 100 到 500 毫秒,写可以放宽到 1 秒。
4.3 协议解析的边界:二进制安全与回复类型
hiredis 的解析器是二进制安全的,reply->str里可能有\0,所以取字符串时要用reply->len而不是strlen。这一点在处理序列化数据(比如 protobuf、msgpack)时特别重要,用strlen会截断。回复类型上,REDIS_REPLY_STRING是 bulk string,REDIS_REPLY_STATUS是简单状态(比如+OK),REDIS_REPLY_INTEGER是整数,REDIS_REPLY_ARRAY是数组,REDIS_REPLY_NIL是空。判断类型再取值,不要假设GET一定返回字符串,键不存在时返回的是REDIS_REPLY_NIL。
数组回复要递归处理,element是redisReply **,elements是元素个数。嵌套数组(比如EXEC的返回)要逐层判断。这块写错会导致段错误,是调试时最常见的崩溃点之一。
5. 避坑与排查:hiredis-0.13.1 上踩过的五个坑
5.1 编译报错找不到redisLibeventAttach
现象:链接时提示undefined reference to redisLibeventAttach。原因:只编了libhiredis.a,没有编适配层。0.13.1 的适配层在adapters/目录下,需要单独编或者把adapters/libevent.c一起加进编译。解决:在主工程编译时把adapters/libevent.c加进源文件列表,或者编 hiredis 时用make而不是make static,让适配层一起产出。
5.2 同步命令偶发卡死,线程堆栈停在read
现象:服务跑一段时间后某个线程不响应,gdb 看堆栈停在read系统调用。原因:同步模式下redisCommand阻塞等回复,Redis 端慢查询或网络问题导致回复迟迟不来。解决:短期给 Redis 加慢查询监控,长期改用异步模式,或者给同步连接加 socket 读超时(需要改 hiredis 源码或换更高版本)。排查时先看 Redis 的slowlog,再看客户端和服务端之间的网络质量。
5.3freeReplyObject漏调导致内存缓慢上涨
现象:服务内存曲线每天涨几十兆,重启后恢复。原因:某条代码路径上redisCommand返回的 reply 没有freeReplyObject,常见于错误分支提前 return。解决:用代码扫描工具查所有redisCommand调用点,确保每条路径都有释放;或者封装一层统一释放的辅助函数。这个坑很隐蔽,因为单次泄漏很小,跑几天才看得出来。
5.4 异步回调里调同步命令导致死锁
现象:异步模式下程序卡住,gdb 看主线程在等锁。原因:在onReply回调里调了同步的redisCommand,而同步命令内部可能等事件循环,形成循环等待。解决:回调里只做数据拷贝和状态标记,需要发新命令时用redisAsyncCommand,不要混用同步接口。这个坑在把同步代码往异步迁移时特别容易踩。
5.5 连接被 Redis 服务端断开后没有重连
现象:异步客户端跑几小时后不再收数据,日志里没有明显错误。原因:Redis 配了timeout,空闲连接被服务端断开,客户端没有注册断开回调或回调里没做重连。解决:注册redisAsyncSetDisconnectCallback,在回调里标记需要重连,回到主循环后重建连接。同时给连接加心跳,定期发PING,避免被服务端判定为空闲。
6. 从 0.13.1 往上走:升级评估与一个实用的兼容技巧
升级这件事,先别急着动。0.13.1 到后续版本的主要变化集中在:异步接口的稳定性改进、新增了一些便利函数(比如redisAsyncCommandArgv的完善)、对 RESP3 的支持、以及一些内存和边界问题的修复。如果你的服务跑得稳、没有遇到上面那些坑,升级的收益主要是长期维护性和安全性;如果已经踩了同步阻塞或内存泄漏的坑,升级能省掉自己打补丁的功夫。
评估时按这几步走:先列出当前用到的所有 hiredis API,对照目标版本的变更说明看有没有签名变化;再在测试环境编一版,跑一遍核心业务的回归;重点测异步回调、断线重连、二进制数据读写这三块。常见做法是保留一个兼容层,把 hiredis 调用包在自己的小接口里,升级时只改兼容层,业务代码不动。
一个实用的兼容技巧:如果暂时不能升级,但需要命令级超时,可以在同步调用外面套一层「带超时的任务队列 + 工作线程」,主线程发任务后等结果,超时就放弃并标记连接不可用。这样不用改 hiredis 源码,也能避免线程被无限阻塞。代价是多了一层线程间通信,延迟会略增,适合对延迟不极端敏感的场景。
我自己的习惯是:任何直连 Redis 的 C 服务,上线前必须做三件事——压测时故意让 Redis 慢查询,看客户端会不会雪崩;用 valgrind 跑一遍核心路径,确认没有 reply 泄漏;模拟网络断开,看重连逻辑是否生效。这三件事做完,hiredis 用哪个版本心里都有底。希望帮到你。
本文还有配套的精品资源,点击获取