资讯动态

hiredis-0.13.1 实战:C/C++ 直连 Redis 的同步异步与避坑指南

发布时间:2026/10/16 23:16:11 来源:尧图企业网站定制
简介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 CCarm-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再redisFreeTCP 握手和断开的开销会吃掉大部分性能。常见做法是自己维护一个连接池启动时建 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 stringREDIS_REPLY_STATUS是简单状态比如OKREDIS_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 用哪个版本心里都有底。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑