资讯动态

hiredis-0.13.1 实战:C 客户端编译、连接池与异步接口避坑指南

发布时间:2026/10/17 0:41:04 来源:尧图企业网站定制
简介hiredis-0.13.1 是一份面向 C 语言后端开发者与 Redis 应用工程师的轻量级客户端库源码包适合需要在高并发场景下与 Redis 高效通信、又希望深入理解协议解析与网络 I/O 实现的技术人员研读。压缩包共 32 个文件约 54KB以 13 个 .h 头文件与 13 个 .c 源文件为主体涵盖 hiredis 核心接口、sds 动态字符串、dict 字典、net 网络读写及异步适配层另含 Makefile 构建脚本、README 与 CHANGELOG 说明文档、COPYING 许可协议及 .travis.yml 持续集成配置并附带 libev、libevent、glib、libuv 等多套事件循环的示例代码。已有 228 人学习关注。通过阅读源码读者可掌握 Redis 协议的解析流程、连接与命令收发机制、错误处理与多线程安全设计思路并借助示例快速将 hiredis 集成到自有服务中为排查性能瓶颈与二次开发提供直接参考。1. hiredis-0.13.1一个老版本 C 客户端为什么还在被翻出来用如果你在维护一套跑了七八年的 C/C 服务某天老板让你接一个 Redis 做缓存你翻遍内网镜像只找到 hiredis-0.13.1 这个包别急着骂人。这个版本发布于 Redis 还在 3.x 的年代API 简单、依赖极少、编译一把过很多嵌入式网关和老交易系统至今锁死在这个版本上。它解决的核心问题就一个让 C 程序用同步或异步方式跟 Redis 说话把 RESP 协议、连接管理、超时重连这些脏活封装掉。适合谁适合手头有存量 C 工程、不想引入 C 或 Go 运行时、又必须跟 Redis 打交道的后端和嵌入式开发者。这篇笔记不讲虚的从编译参数到连接池到异步回调的坑按我实际踩过的顺序捋一遍。2. 先把 hiredis-0.13.1 编出来Makefile 参数与静态库取舍2.1 为什么这个版本值得单独说编译hiredis 的构建系统在 0.13 这个阶段还是纯手写 Makefile没有 CMake也没有 meson。好处是依赖干净坏处是默认行为跟新版本不一样。默认make出来的是静态库libhiredis.a动态库要显式加USE_SHARED1。很多人在自己工程里链接报一堆 undefined reference八成就是拿了静态库却没把-lhiredis放在源文件后面或者漏了-lpthread。先看标准编译流程# 解压后进入源码目录 tar -xzf hiredis-0.13.1.tar.gz cd hiredis-0.13.1 # 默认编译静态库产物在源码根目录 make # 如果要动态库显式指定 make USE_SHARED1 # 安装到自定义前缀避免污染系统目录 make PREFIX/opt/hiredis-0.13.1 install逻辑说明make不带参数时Makefile 里USE_SHARED默认为空只生成libhiredis.a。PREFIX控制install目标把hiredis.h、read.h、sds.h和库文件拷到哪。参数说明USE_SHARED1会额外生成libhiredis.so.0.13并建立libhiredis.so软链PREFIX默认是/usr/local生产环境我一般指定到独立目录方便回滚。2.2 静态库还是动态库一个链接顺序的血泪经验选静态库的理由部署机器不需要额外装 so二进制拷过去就能跑适合容器镜像做小。选动态库的理由多个进程共享一份代码段内存省升级库不用重编业务。我一般这么定如果是单体服务且镜像大小不敏感用静态如果是同一台机器跑十几个小进程用动态。链接时的顺序问题值得单独拎出来。GCC 的链接器从左到右解析符号静态库必须放在引用它的目标文件后面# 正确源文件在前静态库在后 gcc -o demo demo.c -I/opt/hiredis-0.13.1/include \ -L/opt/hiredis-0.13.1/lib -lhiredis -lpthread # 错误库在前源文件在后会报 undefined reference to redisConnect gcc -o demo -lhiredis demo.c逻辑说明-I指定头文件搜索路径-L指定库搜索路径-lhiredis告诉链接器找libhiredis.a或libhiredis.so。-lpthread不能省hiredis 内部用 pthread 做超时控制。参数说明如果你的工程用-Wl,--as-needed动态库链接时也要注意顺序否则可能被优化掉。提示交叉编译时记得在make里传CCarm-linux-gnueabihf-gcc和ARCH相关变量0.13.1 的 Makefile 对交叉编译支持比较原始必要时手动改CFLAGS。3. 同步接口跑通最小闭环redisConnect 到 redisFree 的完整链路3.1 一个能抄的同步读写示例同步接口是 hiredis 最常用的部分核心就四个函数redisConnect、redisCommand、redisReply解析、redisFree。下面这段代码我用了很多次直接改改就能进项目#include stdio.h #include stdlib.h #include string.h #include hiredis/hiredis.h int main(void) { // 1. 建立连接带超时参数秒 微秒 struct timeval timeout { 2, 500000 }; // 2.5 秒 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; } // 2. 执行 SET 命令 redisReply *reply (redisReply *)redisCommand(c, SET foo bar); if (reply NULL) { printf(命令执行失败连接可能已断\n); redisFree(c); return 1; } printf(SET 返回类型: %d\n, reply-type); // 期望 REDIS_REPLY_STATUS freeReplyObject(reply); // 3. 执行 GET 并读取字符串 reply (redisReply *)redisCommand(c, GET foo); if (reply-type REDIS_REPLY_STRING) { printf(GET foo %s\n, reply-str); } freeReplyObject(reply); // 4. 释放连接 redisFree(c); return 0; }逻辑说明redisConnectWithTimeout比redisConnect多一个超时控制避免网络抖动时卡死。redisCommand是可变参数函数内部会格式化命令并发送返回的redisReply必须用freeReplyObject释放否则内存泄漏。参数说明timeout结构体第一个字段是秒第二个是微秒{2, 500000}表示 2.5 秒reply-type常见值有REDIS_REPLY_STRING、REDIS_REPLY_STATUS、REDIS_REPLY_INTEGER、REDIS_REPLY_ARRAY、REDIS_REPLY_NIL、REDIS_REPLY_ERROR。3.2 返回值类型判断别拿 status 当 string 用新手最容易翻车的地方是SET返回的是REDIS_REPLY_STATUSreply-str里是OK但如果你直接当 string 处理逻辑上没错可一旦 Redis 返回错误type会变成REDIS_REPLY_ERROR这时候reply-str是错误信息不是数据。我见过有人在生产代码里只判断reply ! NULL就取reply-str结果 Redis 认证失败时把错误信息当缓存值写进了数据库。正确的做法是每个命令都判断typereply (redisReply *)redisCommand(c, GET %s, key); if (reply NULL) { // 连接层错误需要重连 reconnect(c); } else if (reply-type REDIS_REPLY_ERROR) { // 命令层错误比如 WRONGTYPE printf(Redis 错误: %s\n, reply-str); } else if (reply-type REDIS_REPLY_NIL) { // key 不存在 } else if (reply-type REDIS_REPLY_STRING) { // 正常取值 } freeReplyObject(reply);逻辑说明reply NULL表示连接已断redisCommand内部发送或接收失败此时c-err会被置位需要redisFree后重连。REDIS_REPLY_ERROR是 Redis 服务端返回的错误连接本身还在。参数说明redisCommand支持 printf 风格格式化但注意%s传入的字符串如果含空格或特殊字符hiredis 不会自动转义需要自己保证 key/value 安全或者改用二进制安全的redisCommandArgv。注意0.13.1 的redisCommand在格式化%b时行为跟新版本有差异二进制数据建议直接用redisCommandArgv避免踩到长度截断的坑。4. 异步接口与连接池把吞吐从几百拉到几万的关键4.1 异步回调模型redisAsyncConnect 的正确打开方式同步接口一个连接一次只能跑一个命令QPS 上到几千就顶天了。异步接口基于事件循环一个连接可以同时发多个命令回调按回复到达顺序触发。hiredis 的异步部分依赖ae事件库0.13.1 自带一个简化版ae.c也可以接 libev、libevent。最小异步示例#include stdio.h #include hiredis/hiredis.h #include hiredis/async.h #include hiredis/adapters/ae.h static void getCallback(redisAsyncContext *c, void *r, void *privdata) { redisReply *reply (redisReply *)r; if (reply NULL) return; if (reply-type REDIS_REPLY_STRING) { printf(异步 GET 结果: %s\n, reply-str); } // 异步回调里不要 freeReplyObjecthiredis 会自动释放 } static void connectCallback(const redisAsyncContext *c, int status) { if (status ! REDIS_OK) { printf(异步连接失败: %s\n, c-errstr); return; } printf(异步连接成功\n); // 连接成功后发命令 redisAsyncCommand(c, getCallback, NULL, GET foo); } int main(void) { redisAsyncContext *c redisAsyncConnect(127.0.0.1, 6379); if (c-err) { printf(异步连接错误: %s\n, c-errstr); return 1; } // 绑定事件循环 aeEventLoop *loop aeCreateEventLoop(1024); redisAeAttach(loop, c); // 设置连接回调 redisAsyncSetConnectCallback(c, connectCallback); // 跑事件循环 aeMain(loop); aeDeleteEventLoop(loop); return 0; }逻辑说明redisAsyncConnect立即返回不阻塞真正的连接结果在connectCallback里通知。redisAeAttach把 Redis 的读写事件注册到ae事件循环。redisAsyncCommand发送命令并注册回调回调里拿到的reply由 hiredis 管理生命周期不能手动freeReplyObject。参数说明aeCreateEventLoop(1024)的参数是最大文件描述符数按实际连接数调整redisAsyncSetConnectCallback必须在aeMain之前调用否则可能错过连接事件。4.2 连接池自己写还是用现成的hiredis 本身不提供连接池0.13.1 更没有。生产环境常见做法是自己封装一个简单的池子或者用libevent的 bufferevent 做多连接管理。我一般会写一个固定大小的连接数组每个连接配一个互斥锁取用时加锁、用完归还。池子大小按QPS / 单连接吞吐估算单连接同步模式大概 5000 QPS异步模式能到 2 万以上。一个极简连接池结构typedef struct { redisContext **conns; // 连接数组 int *in_use; // 使用标记 int size; // 池大小 pthread_mutex_t lock; // 全局锁 } redis_pool_t; redisContext *pool_get(redis_pool_t *p) { pthread_mutex_lock(p-lock); for (int i 0; i p-size; i) { if (!p-in_use[i]) { p-in_use[i] 1; pthread_mutex_unlock(p-lock); // 检查连接是否还活着断了就重连 if (p-conns[i]-err) { redisFree(p-conns[i]); p-conns[i] redisConnect(127.0.0.1, 6379); } return p-conns[i]; } } pthread_mutex_unlock(p-lock); return NULL; // 池满调用方决定等待还是报错 } void pool_put(redis_pool_t *p, redisContext *c) { pthread_mutex_lock(p-lock); for (int i 0; i p-size; i) { if (p-conns[i] c) { p-in_use[i] 0; break; } } pthread_mutex_unlock(p-lock); }逻辑说明pool_get遍历找空闲连接标记占用后返回返回前检查c-err如果连接已断就重连避免把坏连接给业务。pool_put只做标记归还不关闭连接。参数说明size建议设为 CPU 核数的 2 到 4 倍太大反而增加 Redis 的连接负担锁的粒度可以优化成每个连接一把锁减少竞争。提示连接池里的连接如果长时间不用Redis 服务端可能主动断开timeout配置所以取用时必须检查err不能假设连接一直有效。5. 避坑与排查hiredis-0.13.1 上最容易翻车的 4 个点5.1 现象程序跑几小时后内存持续上涨原因redisCommand返回的redisReply没有freeReplyObject或者异步回调里手动释放了导致双重释放。0.13.1 的redisReply内部有嵌套数组只 free 外层不够。解决同步代码里每个redisCommand后面必须跟freeReplyObject包括错误分支。异步回调里绝对不要手动释放。用 valgrind 跑一遍看redisReply相关的分配是否全部释放。5.2 现象偶发Connection reset by peer重连后正常原因Redis 服务端timeout配置到期断开空闲连接或者网络中间设备清理了长连接。hiredis 不会自动重连c-err置位后所有命令都失败。解决在pool_get里检查err并重连或者在业务层捕获reply NULL后重建连接。更稳妥的做法是给连接加心跳定期发PING。5.3 现象异步回调里调用redisAsyncCommand导致崩溃原因在回调里直接操作redisAsyncContext可能重入事件循环0.13.1 对重入保护不完善。解决不要在回调里直接发命令把要发的命令塞进队列回到主循环后再发。或者用redisAsyncCommand的privdata传递状态避免在回调里做复杂逻辑。5.4 现象编译时报undefined reference to redisConnectWithTimeout原因链接了旧版本的libhiredis或者头文件和库文件版本不匹配。0.13.1 的头文件里redisConnectWithTimeout是存在的但如果系统里先搜到了/usr/lib下的老库就会链接错。解决用-L显式指定库路径并用ldd或nm确认链接的是哪个库。静态链接时检查ar t libhiredis.a里有没有对应的.o文件。6. 进阶技巧用 redisCommandArgv 处理二进制安全与批量管道6.1 二进制安全为什么 %s 不够用redisCommand的%s遇到\0会截断存图片、序列化后的 protobuf 就会丢数据。0.13.1 提供了redisCommandArgv直接传参数数组和长度完全二进制安全。// 存二进制数据 const char *key bin_key; const char *val \x00\x01\x02\x03; // 含 \0 size_t keylen strlen(key); size_t vallen 4; const char *argv[3]; size_t argvlen[3]; argv[0] SET; argvlen[0] 3; argv[1] key; argvlen[1] keylen; argv[2] val; argvlen[2] vallen; redisReply *reply (redisReply *)redisCommandArgv(c, 3, argv, argvlen); freeReplyObject(reply);逻辑说明redisCommandArgv的第二个参数是参数个数第三个是参数指针数组第四个是每个参数的长度数组。hiredis 按长度拼接 RESP 协议不会因为\0截断。参数说明argvlen必须准确传错会导致协议解析错位Redis 返回Protocol error。6.2 管道一次发多条命令减少 RTT管道pipeline是提升吞吐最直接的手段。同步模式下把多条命令写进缓冲区最后一次读回复。0.13.1 支持redisAppendCommand加redisGetReply的组合。// 管道写 3 条命令 redisAppendCommand(c, SET k1 v1); redisAppendCommand(c, SET k2 v2); redisAppendCommand(c, GET k1); // 依次读回复 redisReply *reply; for (int i 0; i 3; i) { if (redisGetReply(c, (void **)reply) REDIS_OK) { // 处理 reply freeReplyObject(reply); } else { // 连接错误 break; } }逻辑说明redisAppendCommand只把命令写入输出缓冲区不等待回复redisGetReply阻塞读取一个回复。三条命令一次网络往返RTT 从 3 次降到 1 次。参数说明管道里的命令数量不宜过多几千条以上会占用大量内存建议按 100 到 500 条一批。6.3 验证方法用 redis-benchmark 对比同步与管道改完代码别拍脑袋用redis-benchmark压一下# 同步模式基准 redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -t set,get -q # 管道模式-P 指定管道深度 redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -t set,get -P 16 -q逻辑说明-c是并发连接数-n是总请求数-P是管道深度。对比两次的requests per second管道模式通常能提升 3 到 5 倍。参数说明-q只输出 QPS 摘要方便脚本采集。我自己的习惯是任何 hiredis 相关的改动先在测试环境用redis-benchmark跑一轮再用 valgrind 跑一轮两个都过了才上生产。这个版本老归老把连接池、二进制安全、管道这三件事做扎实撑住几万 QPS 没问题。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑