简介:面向需要在 Windows 平台集成 Redis 的 C/C++ 开发者,这份资源提供了一套可直接链接的编译产物,省去在非 Linux 环境下自行移植与编译的麻烦。压缩包共 20 个文件,约 6.9MB,以 lib 静态库为主,辅以头文件、示例源码、可执行程序与动态库,并附说明文档,按 x86 与 x64 分目录组织,Debug 与 Release 版本分别对应 /MTd 与 /MT 运行库配置,便于按目标平台与构建模式选用。核心的 hiredis 客户端库简化了与 Redis 服务器的通信与回复解析,Win32_Interop 则承担 Windows 下的系统调用兼容,配合头文件即可在项目中完成连接、命令收发等基础开发。已有 1723 人学习下载,适合希望快速验证 Windows 下 Redis 客户端接入、减少环境搭建成本的中级开发者参考。
1. 在 Windows 上把 Redis 客户端跑起来:一份开箱即用的编译产物
很多做 C/C++ 服务端开发的人第一次在 Windows 上接 Redis,都会卡在同一个地方:Linux 下apt install一行命令搞定的事,到了 Windows 就变成一场编译马拉松。Redis 官方源码本身对 Windows 支持有限,hiredis 虽然能编,但 Win32 相关的兼容层、socket 头文件、链接顺序,随便一个环节不对就是一堆LNK2019未解析外部符号。这份资源解决的正是这个痛点——它把已经编译好的hiredis.lib、Win32_Interop.lib以及配套头文件打包在一起,拿到手就能直接在自己的 Windows 工程里链接使用,不用再折腾 MSVC 工具链和第三方依赖。适合谁?适合那些在 Windows 上写 C/C++ 客户端、需要连 Redis 做缓存或消息队列,但又不想把时间耗在环境搭建上的开发者。下面我按「这东西是什么 → 怎么接进工程 → 怎么改」的顺序拆一遍。
2. 资源构成与工程接入:从 lib 到第一个 SET 命令
2.1 包里到底有什么,每个文件干什么用
先把压缩包摊开看。这类 Windows 编译产物通常包含三类东西:静态库、头文件、以及可能的示例工程。核心是下面这几个:
| 文件 | 类型 | 作用 |
|---|---|---|
hiredis.lib | 静态库 | hiredis 的核心实现,负责 RESP 协议解析、连接管理、命令收发 |
Win32_Interop.lib | 静态库 | Windows 平台兼容层,封装 socket、线程、错误码等 POSIX 与 Win32 的差异 |
hiredis.h | 头文件 | 对外 API 声明,redisContext、redisReply等结构体定义 |
read.h/sds.h | 头文件 | 回复读取与动态字符串的内部接口,部分 API 需要 |
win32fixes.h | 头文件 | Win32 类型修正,比如ssize_t、strcasecmp的替代定义 |
这里要理解一个关键点:hiredis.lib不是独立可用的,它内部调用了Win32_Interop.lib里的兼容函数。所以链接时两个库都得带上,而且顺序有讲究——MSVC 的链接器是从左到右解析符号的,hiredis.lib依赖Win32_Interop.lib,那么hiredis.lib要写在前面。这个顺序搞反了,报错信息会指向一堆你根本没调用过的函数,很容易让人误以为是库本身有问题。
头文件的包含顺序同样不能乱。win32fixes.h必须在hiredis.h之前引入,因为它定义了 hiredis 头文件里会用到的平台相关类型。常见做法是在自己的预编译头或者统一的redis_wrapper.h里按这个顺序排好,业务代码只包含 wrapper,避免每个.cpp都去操心顺序。
2.2 在 Visual Studio 工程里配置链接
假设你有一个现成的 VS 工程,现在要接入这份库。步骤不复杂,但每一步都有坑。
第一步,把include目录加到「附加包含目录」。右键项目 → 属性 → C/C++ → 常规 → 附加包含目录,填入头文件所在路径。注意用相对路径或者宏(比如$(SolutionDir)third_party\redis\include),别写死绝对路径,否则换台机器就崩。
第二步,把lib目录加到「附加库目录」,路径在 链接器 → 常规 → 附加库目录。
第三步,在 链接器 → 输入 → 附加依赖项 里填:
hiredis.lib Win32_Interop.lib ws2_32.libws2_32.lib是 Windows socket 库,Win32_Interop底层要用到它,漏了会报WSAStartup、socket之类的未解析符号。这个是最容易被忽略的一条,因为报错信息不会直接告诉你「你缺 ws2_32」。
第四步,代码里初始化 Winsock。hiredis 在 Windows 下不会自动帮你做这件事,必须在程序启动时调一次:
#include <winsock2.h> #include "win32fixes.h" #include "hiredis.h" int main(void) { WSADATA wsaData; // 初始化 Winsock 2.2,hiredis 的 socket 操作依赖它 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { return -1; } struct timeval timeout = { 2, 0 }; // 连接超时 2 秒 redisContext *c = redisConnectWithTimeout("127.0.0.1", 6379, timeout); if (c == NULL || c->err) { if (c) { printf("Connection error: %s\n", c->errstr); redisFree(c); } WSACleanup(); return -1; } redisReply *reply = (redisReply *)redisCommand(c, "SET %s %s", "foo", "bar"); if (reply == NULL) { printf("Command failed\n"); } else { printf("SET result: %s\n", reply->str); freeReplyObject(reply); } redisFree(c); WSACleanup(); return 0; }这段代码的逻辑很直白:先起 Winsock,再建连接,发一条SET,读回复,最后释放。几个参数值得说清楚。redisConnectWithTimeout的第三个参数是struct timeval,{2, 0}表示 2 秒 0 微秒,超时后c->err会被置位,errstr里是具体原因。redisCommand是可变参数版本,内部会做格式化,所以SET %s %s这种写法是安全的,但要注意它返回的redisReply必须手动freeReplyObject,否则每发一条命令漏一块内存,跑久了就是内存缓慢上涨——这种问题排查起来相当玄学,因为崩溃点往往不在漏内存的地方。
reply->str只在reply->type == REDIS_REPLY_STATUS或REDIS_REPLY_STRING时有效,直接访问其他类型的str会拿到野指针。稳妥的写法是先判断type,再取字段。这一点在官方文档里写得不算显眼,但实际项目里翻车的不少。
2.3 连接池与线程安全的边界
单连接在单线程里用没问题,但服务端场景基本都要多线程。hiredis 的redisContext本身不是线程安全的,多个线程共用一个 context 发命令,回复会串。常见做法是每个线程一个连接,或者自己封一个连接池。
连接池的核心逻辑不复杂:维护一个redisContext的队列,取的时候 pop,用完 push 回去。但要注意连接失效的处理——Redis 服务端有timeout配置,空闲太久的连接会被服务端断开,池子里拿出来的连接可能已经死了。所以取连接后最好做一次PING探活,失败就重建。这个探活有开销,折中方案是记录连接的最近使用时间,超过阈值才探活。
// 简化的连接池取连接逻辑 redisContext *pool_acquire(conn_pool_t *pool) { redisContext *c = NULL; pthread_mutex_lock(&pool->lock); if (pool->count > 0) { c = pool->conns[--pool->count]; } pthread_mutex_unlock(&pool->lock); if (c == NULL) { c = redisConnectWithTimeout(pool->host, pool->port, pool->timeout); if (c == NULL || c->err) { if (c) redisFree(c); return NULL; } } else { // 复用前探活,避免拿到被服务端断开的死连接 redisReply *r = redisCommand(c, "PING"); if (r == NULL || r->type != REDIS_REPLY_STATUS) { if (r) freeReplyObject(r); redisFree(c); c = redisConnectWithTimeout(pool->host, pool->port, pool->timeout); } else { freeReplyObject(r); } } return c; }这里的pthread_mutex在 Windows 下要换成CRITICAL_SECTION或者std::mutex,取决于你的工程用的是 C 还是 C++。如果用 C++,std::mutex加std::lock_guard更省心。池的容量、超时时间这些参数建议做成配置项,不同业务对延迟和连接数的要求差别很大,写死后期改起来麻烦。
3. 核心 API 拆解:命令收发、回复解析与错误处理
3.1 redisCommand 与 redisReply 的配合方式
hiredis 的 API 设计很克制,核心就两个结构体:redisContext代表连接,redisReply代表一次回复。所有命令都通过redisCommand或其变体发出,返回一个堆上的redisReply。
redisReply的type字段决定了怎么读数据:
| type 值 | 含义 | 有效字段 |
|---|---|---|
REDIS_REPLY_STATUS | 状态回复,如+OK | str |
REDIS_REPLY_ERROR | 错误回复,如-ERR ... | str |
REDIS_REPLY_INTEGER | 整数回复,如:1 | integer |
REDIS_REPLY_STRING | 批量字符串 | str,len |
REDIS_REPLY_ARRAY | 数组回复 | elements,element[] |
REDIS_REPLY_NIL | 空回复 | 无 |
数组回复要递归处理,element是一个redisReply**,每个元素又可能是上面任意类型。写解析代码时建议封一个递归函数,别在业务逻辑里一层层if嵌套,否则代码很快就没法看了。
void print_reply(redisReply *r, int depth) { if (r == NULL) return; switch (r->type) { case REDIS_REPLY_STATUS: case REDIS_REPLY_STRING: printf("%*s%s\n", depth * 2, "", r->str); break; case REDIS_REPLY_INTEGER: printf("%*s%lld\n", depth * 2, "", r->integer); break; case REDIS_REPLY_ARRAY: for (size_t i = 0; i < r->elements; i++) { print_reply(r->element[i], depth + 1); } break; case REDIS_REPLY_NIL: printf("%*s(nil)\n", depth * 2, ""); break; case REDIS_REPLY_ERROR: printf("%*s(error) %s\n", depth * 2, "", r->str); break; } }r->integer是long long,在 32 位 Windows 上别用%d打印,会截断。r->len是字符串长度,str不一定以\0结尾(二进制安全),所以涉及二进制数据时要用len而不是strlen。这两点在实际项目里都踩过,尤其是存 protobuf 序列化结果的时候,用strlen会丢数据。
3.2 错误处理与重连策略
redisContext的err字段是判断连接状态的关键。err非 0 表示连接已不可用,此时errstr里有描述。注意:命令执行返回NULL不一定代表连接断了,也可能是内存分配失败;但c->err被置位就一定得重建连接。
一个健壮的封装通常长这样:
redisReply *safe_command(redisContext **pc, const char *fmt, ...) { va_list ap; va_start(ap, fmt); redisReply *r = redisvCommand(*pc, fmt, ap); va_end(ap); if (r == NULL) { // 连接可能已断,尝试重连一次 redisFree(*pc); *pc = redisConnectWithTimeout("127.0.0.1", 6379, (struct timeval){2, 0}); if (*pc == NULL || (*pc)->err) { return NULL; } va_start(ap, fmt); r = redisvCommand(*pc, fmt, ap); va_end(ap); } return r; }这里用redisvCommand是因为要把va_list透传,redisCommand内部也是转成redisvCommand的。重连只做一次,避免在服务端彻底挂掉时无限重试把线程卡死。重连后要不要重发原命令,取决于命令是否幂等——SET可以重发,INCR重发就会多算一次。这个判断得在业务层做,库本身给不了答案。
3.3 管道与批量命令的性能差异
逐条发命令时,每条都要等一个 RTT。本地 Redis 的 RTT 可能只有几十微秒,但跨机器就是毫秒级,批量写 1000 条就是 1 秒起步。hiredis 提供了管道(pipeline)模式,把多条命令攒起来一次发出去,再一次性读回复。
// 管道模式:批量写入,减少网络往返 redisAppendCommand(c, "SET key1 v1"); redisAppendCommand(c, "SET key2 v2"); redisAppendCommand(c, "SET key3 v3"); for (int i = 0; i < 3; i++) { redisReply *r = NULL; if (redisGetReply(c, (void **)&r) == REDIS_OK) { // 逐条取回复,顺序与发送顺序一致 freeReplyObject(r); } else { // 连接异常,后续回复不可信,需重建连接 break; } }redisAppendCommand只负责把命令写进发送缓冲区,不读回复;redisGetReply才真正读。管道模式下如果中途某条命令出错,后续回复的对应关系会乱,所以一旦redisGetReply返回非REDIS_OK,就得放弃这批剩余回复,重建连接。这个细节不注意,会出现「回复错位」——你以为读到的是第二条的回复,其实是第三条的,数据就悄悄错了。
4. 避坑与排查:Windows 下链接和运行时的常见问题
4.1 链接报 LNK2019 未解析符号
现象:编译通过,链接阶段报一堆unresolved external symbol,符号名里带_imp__或者指向WSAStartup、socket、close之类。
原因:要么漏了ws2_32.lib,要么库的链接顺序不对,要么 Debug/Release 混用——用 Debug 配置去链 Release 版的.lib,运行库不匹配(/MDvs/MT)也会出这类错。
解决:先确认附加依赖项里有hiredis.lib、Win32_Interop.lib、ws2_32.lib三个,且hiredis.lib在前。再检查项目属性 → C/C++ → 代码生成 → 运行库,确保和库编译时用的运行库一致。如果库是/MT编的,你的工程也得/MT,混用会在链接期或运行期出问题。
4.2 运行时崩溃在 redisConnect 内部
现象:程序启动后调redisConnect直接崩,调用栈停在 socket 相关函数。
原因:没调WSAStartup。Windows 下 socket API 必须先初始化 Winsock,hiredis 不会替你做。
解决:在main或程序初始化阶段调一次WSAStartup(MAKEWORD(2, 2), &wsaData),退出时WSACleanup()。如果是 DLL 工程,放在DllMain的DLL_PROCESS_ATTACH里,注意别在DllMain里做太重的操作。
4.3 中文或二进制数据读出来是乱码/截断
现象:SET进去的中文,GET出来长度不对或者尾部丢字节。
原因:用了strlen而不是reply->len。hiredis 是二进制安全的,str字段不保证以\0结尾,尤其是数据里本身含\0的时候。
解决:所有读字符串的地方统一用reply->len作为长度,memcpy到自己的缓冲区时也按len拷。存 protobuf、图片这类二进制数据时尤其要注意。
4.4 多线程下回复串包
现象:两个线程同时用一个redisContext,A 线程读到了 B 线程命令的回复。
原因:redisContext不是线程安全的,内部有共享的读写缓冲区。
解决:每线程独立连接,或者用连接池。如果非要用一个连接,必须在外层加锁把「发命令 + 读回复」整体包住,但这样并发度就没了,不如直接上池。
4.5 长时间运行后连接假死
现象:服务跑几个小时后,某些连接发命令一直超时,但 Redis 服务端看起来正常。
原因:服务端timeout配置会断开空闲连接,客户端这边redisContext还以为是好的,发命令时才发现写不出去。
解决:连接池取连接时做PING探活,或者设置redisOptions里的connect_timeout和command_timeout(如果用的版本支持),让超时可控。另外可以在应用层加心跳,定期对空闲连接发PING。
5. 二次开发与改造:把这份库包成顺手的客户端封装
拿到这份编译产物,直接裸调 hiredis API 在 demo 里没问题,但真接到业务里,迟早要封一层。我一般的做法是分三层:底层连接管理、中层命令封装、上层业务接口。
连接管理这层负责池化、探活、重连,对外只暴露acquire/release两个函数。命令封装层把常用的GET、SET、HGETALL包成带类型检查的函数,返回值统一转成std::string或std::vector,把redisReply的生命周期关在这一层里,业务代码碰不到裸指针。上层就按业务语义组织,比如cache_get_user、cache_set_session。
改造时有几个点值得提前想清楚。第一,错误码体系。hiredis 的错误信息是字符串,业务层判断起来不方便,建议在封装层映射成枚举:连接错误、超时、服务端错误、协议错误,分开处理。第二,超时配置。redisConnectWithTimeout只管连接超时,命令执行超时在旧版 hiredis 里没有直接支持,得靠redisSetTimeout或者自己用非阻塞模式加select,这块要按实际版本确认。第三,内存分配器。如果工程里统一用了自定义 allocator,hiredis 默认用malloc/free,跨分配器释放会崩,要么统一,要么在封装层做拷贝隔离。
验证改造是否到位,我习惯跑三个用例:单线程连续发一万条SET/GET看内存是否平稳;多线程各发一千条看回复是否错位;手动kill掉 Redis 再发命令,看重连逻辑是否按预期工作。这三个过了,基本能上测试环境。
说个血泪教训:早期我图省事,在业务代码里直接redisCommand然后freeReplyObject,结果有个分支忘了释放,压测时内存曲线缓慢爬升,查了两天才定位到。从那以后我每次封装客户端,都强制让redisReply只在封装层内部出现,业务代码拿到的永远是值类型。希望帮到你。
本文还有配套的精品资源,点击获取