Redis三件套深度解析:命令行、C客户端与压测实践 1. 三件套各自干什么选型之前先搞清楚分工我最早接触Redis的时候和大多数人一样只知道有一个redis-cli需要手动连上去敲命令。后来项目里要用C语言写一个抓取服务才认识了hiredis再后来要给新买的物理机做容量评估才系统性地用了redis-benchmark。说句实话这三样东西虽然都属于Redis生态但它们的定位完全不同混为一谈只会让后面每一步都别扭。redis-cli是一个交互式命令行客户端安装Redis服务端时基本会自带。它解决的是人直接操作Redis实例的问题比如查看某个key的值、检查主从复制状态、手动删除一批数据、执行info确认内存碎片率甚至调试一段Lua脚本。它的核心使用对象是运维、DBA和日常开发调试的人属于命令行瑞士军刀。hiredis是一个C语言客户端库解决的是程序代码里访问Redis的问题。它提供了同步API和异步API编译之后就是一个静态库或者动态库链接到你的C/C服务里。它和redis-cli最大的区别是redis-cli背后是一个人敲键盘hiredis背后是业务代码按规则发命令。无论是做缓存、消息队列还是排行榜只要你的服务是C/C写的和Redis打交道基本绕不开它。redis-benchmark则是Redis自带的压测工具解决的是这台机器或者这个Redis实例到底能抗多少读、多少写的问题。它会并发地往Redis发送大量命令然后统计每秒能处理的请求数、每个请求的延迟分布等数据。它的应用场景是性能测试、方案选型、容量规划以及每次调整内核参数或者修改Redis配置之后的回归对比。有一个很常见的误解值得先说清楚redis-cli和hiredis在功能上确实有交集因为两者都能发命令但有人会试图用redis-cli在shell脚本里批量执行逻辑结果遇到错误处理、二进制数据、性能瓶颈时才发现它本质上是个交互工具不应该承担业务代码的职责。反过来也有人觉得 hiredis 很复杂但在C项目里通过system(redis-cli ...)这种事情我见过不止一次那才是真正的灾难——每次执行都等于新起一个进程、重新建立TCP连接性能差两个数量级。所以我的建议是人机交互的场景用redis-cli机器交互的场景用hiredis容量评估和性能对比用redis-benchmark。这三个东西并不互斥很多正规的部署环境里命令行工具和C客户端库会长期共存压测工具则是在版本上线前后才频繁出场。2. redis-cli 的高效运维姿势一个命令敲对了能省半小时很多人在生产环境排查问题时只知道redis-cli能通过-h ip -p port连上去然后敲get key、set key value再高级一点的知道info memory、config get *。这些当然没错但redis-cli在运维场景里真正值钱的是下面这些看起来不起眼、实战里却非常救命的能力。2.1 各种连接方式以及哪些参数不能省最基础的是指定IP和端口redis-cli -h 10.0.1.20 -p 6379如果你的Redis设置了密码需要加-a我建议在脚本里使用REDISCLI_AUTH环境变量而不是直接在命令行上写密码否则很容易通过进程列表泄露密码。Redis 6.0以上还有ACL用户的概念这时候要用--user和--passREDISCLI_AUTHmyStrongPassword redis-cli -h 10.0.1.20 -p 6379 --user appuser如果Redis部署在Unix域套接字上-h参数不生效需要这么写redis-cli -s /usr/local/redis/redis.sock这里最容易被忽略的是--tls和--cacert。开启TLS的Redis实例用redis-cli连接时默认会因为没有证书而直接失败你需要指定CA文件类似redis-cli -h redis.example.com -p 6380 --tls --cacert /etc/ssl/certs/ca.crt --user default --pass secret我见过不止一次网上教程只说开启tls结果运维人员拿着裸redis-cli连半天连不上最后发现是缺了--cacert。记住一点启用了TLS的Redis实例默认redis-cli必须配合证书参数才能连上这不是bug这是安全基线。2.2 只看我要的数据--raw、--no-raw、--json另一个经常被忽略的参数是--raw。默认情况下redis-cli输出字符串值时会把内容用引号包起来类似hello。这在人看的时候没什么但如果你的命令输出要交给 awk、grep、jq 或者写入文件那么引号就是一种污染。# 默认输出 redis-cli get name redis # raw模式输出 redis-cli --raw get name redis如果存储的数据本身包含二进制比如\x00--raw下会原样输出不会做转义但这就可能污染你的终端。所以还有一个--no-raw用来强制加上引号转义。Redis 7.0 之后redis-cli还支持直接输出JSON格式配合--jsonredis-cli --json hgetall user:10086 {name:zhangsan,age:18}这一步省掉了以前必须靠python -c或者jq手工转换的麻烦在写脚本采集数据时非常香。2.3 长时间监控--stat、--intrinsic-latency、--latency排查Redis变慢问题时redis-cli --stat是第一步必用工具。它每隔一秒输出一次当前实例的实时状态包括连接数、内存用量、命中率、每秒命令数等redis-cli -h 10.0.1.20 -p 6379 --stat它会一直滚动刷新类似Linux的vmstat配合快照对比能快速判断当前实例是否处于高负载状态、内存是否有突增。但这个工具只能看到宏观指标如果想知道Redis服务本身在网络层和内核层多快要用延迟测试两兄弟redis-cli -h 10.0.1.20 -p 6379 --latency redis-cli -h 10.0.1.20 -p 6379 --intrinsic-latency 100--latency是持续测试客户端到服务器的整体网络延迟按回车或者CtrlC结束最终会给出 min、max、avg 延迟。--intrinsic-latency测的是机器本身的延迟基线它只做空转循环看内核调度、CPU延时对延迟的影响。两者一对比就能区分延迟高是网络引起的还是Redis进程本身引起的问题。2.4 生产环境的红线flushall 事件这次搜到的热词里有redis-cli flushall我得专门单独拎出来提醒一下。flushall表示清空整个Redis实例的所有数据flushdb表示清空当前数据库的所有数据。这两条命令在生产环境都是重点管控对象。如果误执行了flushall并且没有开启RDB备份或AOF文件是不可恢复的数据几乎等于永久丢失。即使开了AOF恢复也需要时间而且如果把AOF落盘策略配置成everysec丢失最近一秒的数据也是躲不开的。我的建议包含三方面给高权重实例改了命令名弱化误操作的风险面ACL层面做好用户权限控制普通程序员根本不给他执行flushall的权限内部发布的连接脚本统一封装禁止裸连生产Redis 6379端口。如果你确实需要清理数据用scan加批量删除比直接flushall安全得多。例如redis-cli --scan --pattern session:* | xargs -L 100 redis-cli DEL这种方式一次匹配并删除一百个key最终把符合前缀的全部清掉但不会影响其他业务前缀。要注意xargs -L的数量要和DEL命令的参数数量匹配防止一次命令过长。2.5 批量执行与管道--pipe、-r、-x如果要往Redis里灌几千条数据一条一条敲命令显然不可行。redis-cli支持从文件或管道批量导入这是--pipe的用途redis-cli -h 10.0.1.20 -p 6379 --pipe commands.txt文件里的格式是Redis序列化协议RESP不能像人类敲命令一样随便写。最常见的格式必须是SET key1 value1 SET key2 value2注意这里的空格只能是空格整个文件每一行对应一条命令但命令之间不能有注释也不支持redis-cli交互模式里的那种高亮提示。批量导入时--pipe的效率比逐条执行高十倍甚至几十倍太多核心原因是它把多条命令放在同一个TCP包里发送避免了大量RTT往返。还有一个小而实用的参数-x它表示把标准输入作为命令的最后一个参数。比如我要从文件内容建keycat data.txt | redis-cli -x set config_content这样data.txt的内容会作为一个字符串value存进config_content。这个技巧在处理配置文件、证书文本、长文本对象时特别好用不必手工转义换行。如果要执行多次命令比如连续读取三次某个key的值可以用-r 3redis-cli -r 3 get temp_key它会在三秒内各执行一次配合-i 1可以指定间隔。2.6 内嵌Lua调试--eval 和 DEBUG命令日常运维少有机会写Lua脚本但一旦涉及限流、分布式锁、原子操作--eval就是你调试的最好帮手redis-cli --eval /path/to/script.lua key1 key2 , arg1 arg2这里的格式很容易出错逗号前面是KEYS数组逗号后面是ARGV数组而且逗号两边必须有空格。我在刚开始用的时候因为少了空格脚本一直拿不到参数排查了半天才发现是格式问题。如果在Redis 7.0以上环境做调试可以用redis-cli --ldb --eval进入Lua调试器支持断点、单步、打印变量。调试完记得退出否则连接会一直挂在调试状态。3. hiredis 接入笔记同步API、异步API与内存管理3.1 编译安装与链接先说常见的库版本坑hiredis 是Redis官方维护的C客户端源码在Redis仓库里单独维护也有独立的Git仓库。安装方式一般是编译源码git clone https://github.com/redis/hiredis.git cd hiredis make sudo make install装完后系统里会生成libhiredis.so和头文件hiredis.h、async.h、read.h。编译你自己的程序时链接参数是gcc -o myapp myapp.c -lhiredis这里有一个很常见的坑如果你安装hiredis的时候忘记sudo ldconfig程序运行时会出现error while loading shared libraries: libhiredis.so.1.1.0: cannot open shared object file。解决办法是把库目录加入/etc/ld.so.conf然后执行ldconfig或者在编译时指定-Wl,-rpath,/usr/local/lib。如果你用的是C还有一个注意点hiredis 的头文件大多用C语言写的你需要用extern C包起来。在C文件里#include hiredis/hiredis.h #include hiredis/async.h #include hiredis/adapters/libevent.h新版hiredis的头文件已经做了C兼容的#ifdef __cplusplus extern C处理但老版本没有遇到编译报错 undefined reference 时先检查这个。3.2 同步API的正确姿势看返回码再决定是否为nilhiredis 的同步API最简单三个函数就能跑一个完整流程int main() { redisContext *c redisConnect(127.0.0.1, 6379); if (c NULL || c-err) { if (c) { printf(Connection error: %s\n, c-errstr); redisFree(c); } else { printf(Connection error: cant allocate redis context\n); } return 1; } redisReply *reply redisCommand(c, SET %s %s, foo, bar); if (reply NULL) { printf(Command error: %s\n, c-errstr); redisFree(c); return 1; } freeReplyObject(reply); reply redisCommand(c, GET %s, foo); if (reply ! NULL reply-type REDIS_REPLY_STRING) { printf(Value: %.*s\n, reply-len, reply-str); } freeReplyObject(reply); redisFree(c); return 0; }很多新手写到这里会踩的第一个坑是把redisCommand的返回值和命令执行结果混在一起。实际上redisCommand返回的redisReply*可能为NULL这表示命令连发送都失败了如果命令发送成功但执行出错它返回的是一个REDIS_REPLY_ERROR类型的reply。另一个坑是忘记判断reply-type直接访问reply-str如果执行结果是个整数或者是一个NILreply-str要么为空要么内容不是你要的。所以在生产代码里处理响应必须先看类型switch (reply-type) { case REDIS_REPLY_STRING: // 字符串 case REDIS_REPLY_ARRAY: // 数组 case REDIS_REPLY_INTEGER: // 整数 case REDIS_REPLY_NIL: // key不存在 case REDIS_REPLY_STATUS: // 状态回复 case REDIS_REPLY_ERROR: // 打印错误信息 break; }其中REDIS_REPLY_NIL是最容易忽视的用GET请求一个不存在的key它返回的不是NULL也不是错误而是NIL。如果你直接把它当字符串处理会导致段错误或者取值错乱。3.3 二进制安全的命令构造小心%s的边界hiredis 的redisCommand自带类似printf的格式化功能但它的%s是按C字符串的\0作为结束符的。如果你要存储的内容本身带有二进制数据比如序列化的protobuf字节流里面可能出现\0那用%s就会导致数据被截断。正确的做法是用redisCommandArgv它把命令拆成参数数组还能显式指定每个参数的长度const char *argv[3]; size_t argvlen[3]; argv[0] SET; argvlen[0] 3; argv[1] key; argvlen[1] 3; argv[2] binary_data; argvlen[2] binary_data_len; redisReply *reply redisCommandArgv(c, 3, argv, argvlen);这个API才是二进制安全的处理含有空字节的内容时不会截断。凡是你在项目里要存消息体、图片字节、加密后的数据一律建议用redisCommandArgv而不是redisCommand。3.4 freeReplyObject 为什么要老老实实被调用hiredis 的内存管理很容易被C语言新手忽略每次redisCommand返回的redisReply都是动态分配的必须用freeReplyObject(reply)释放。不释放的话一个循环里跑几千次命令内存就涨上去了这是常见的内存泄漏根因。这里有个更隐蔽的坑如果你对redisReply里的reply-str或reply-element做了指针保存并希望在freeReplyObject之后继续使用那这些指针就成了悬空指针。数组类型的reply其element指针数组里的每个子reply也是同一块内存管理里的整体释放即可不要单独release子元素。3.5 异步API和 EventLoop 的协作方式当你的服务要求高吞吐同步阻塞模型不够用时就要上 hiredis 的异步API。异步API的核心是redisAsyncContext它和同步Context差别很大你发的命令不会立刻得到结果而是通过回调函数来收。一个基于libevent的最小异步流程是这样的void onConnect(const redisAsyncContext *c, int status) { if (status ! REDIS_OK) { printf(connect error: %s\n, c-errstr); return; } printf(connected\n); } void onSet(redisAsyncContext *c, void *reply, void *privdata) { redisReply *r reply; if (r NULL) return; printf(SET result: %s\n, r-str); // 不再用freeReplyObject由异步框架负责释放 } int main() { redisAsyncContext *ac redisAsyncConnect(127.0.0.1, 6379); if (ac-err) { printf(error: %s\n, ac-errstr); return 1; } redisLibeventAttach(ac, event_base); redisAsyncSetConnectCallback(ac, onConnect); redisAsyncCommand(ac, onSet, NULL, SET %s %s, name, hi); event_base_dispatch(event_base); redisAsyncDisconnect(ac); return 0; }异步模式下回调函数里的reply在处理完回调之后由hiredis内部释放所以不要在回调里调用freeReplyObject这是新手最容易和同步API搞混的地方。另外异步Context可能会在连接断开时触发redisAsyncDisconnect和回调的分发顺序问题如果你在回调里又发命令注意组织好命令的时序。3.6 连接复用、超时与断线重连同步API里有个常被忽略的函数是redisConnectWithTimeout它可以在连接时指定超时时间避免由于网络黑洞导致进程卡死struct timeval tv { .tv_sec 2, .tv_usec 0 }; redisContext *c redisConnectWithTimeout(10.0.1.20, 6379, tv);生产环境强烈建议使用带超时的方式因为默认redisConnect在TCP层如果一直得不到响应阻塞时间可能相当长这会让你的服务线程全部挂在连接上。我自己的做法是全局只持有一个或几个连接池每个连接用完不关闭持续复用减少TCP握手和三次握手的开销。一旦检测到c-err非0立即释放旧连接重新创建新连接。断线重连的触发点一般放在每次执行命令后检查reply NULL c-err ! 0。4. redis-benchmark 压测的正确打开方式参数、场景和结果解读4.1 基础参数逐个拆解为什么默认就是个大坑redis-benchmark是Redis安装包自带的压测工具很多人第一次用就直接敲命令redis-benchmark -h 127.0.0.1 -p 6379这确实能跑但结果基本不能直接用于生产判断。为什么因为默认参数是50个并发连接、100000个总请求、每个连接一次发一个命令、测试的固定命令列表包括SET/GET/INCR/LPUSH/LPOP/SADD等。它测的是在50个并发连接下Redis处理混合小value命令的能力这对容量规划参考价值有限。真正要测出数据需要先明确你的业务模型是读多写少是大value还是小value是顺序key还是随机key是单条GET还是带管道批量然后针对性地调整参数。最常用的参数组合参数作用示例-c并发连接数-c 200-n总请求数-n 1000000-dvalue大小字节-d 512-r随机key范围-r 1000000-P管道批量条数-P 16-t只测指定命令-t set,get,lpush--threads压测端多线程--threads 4-q只输出最终QPS-q其中-r是很多人忽略的它决定随机key的范围。如果不指定redis-benchmark默认所有测试命令使用的key都是key加上一个数字而且数字范围很小。打开-r 1000000之后key的分布才更像真实业务能够测试哈希表在分散key下的真实性能。4.2 如何设计一次能用的压测场景我第一次给公司做的压测方案花了半天确认业务模型。Redis在那个系统里承担两个职责一个是作为热点数据的缓存读多写少value大小平均500字节左右另一个是作为消息队列的发布订阅发布命令的量也很大。于是最终我拆成了三个场景场景一缓存读基准模拟500字节value的GET请求key分散在100万范围内redis-benchmark -h 10.0.1.20 -p 6379 -t get -c 100 -n 500000 -d 500 -r 1000000场景二缓存写基准模拟SET请求设置过期策略这个只能靠业务代码实现redis-benchmark不支持带EX参数的SET所以只能测裸SETredis-benchmark -h 10.0.1.20 -p 6379 -t set -c 100 -n 500000 -d 500 -r 1000000场景三管道批量模式模拟服务端在单条连接上一次命令带多条子命令的效果redis-benchmark -h 10.0.1.20 -p 6379 -t set,get -c 50 -n 1000000 -P 16 -r 1000000跑完之后结果里会给出类似这样的数据 SET 1000000 requests completed in 3.34 seconds 50 parallel clients 16 bytes payload keep alive: 1 hostname 10.0.1.20 port: 6379 99.99% 1 milliseconds ... 299401.19 requests per second注意这里requests per second指的是每秒请求总数在管道模式下这个数字会飙升因为它把一次内部批量里的每条子命令也算了进去。如果业务是单条命令一条条发的管道模式得到的QPS会虚高务必清楚自己的业务形态再对照。4.3 结果里的 p99 与吞吐别只看平均值redis-benchmark默认输出的延迟统计格式是百分比分布类似100.00% 1 milliseconds这个的含义是百分位在某个毫秒内的比例。我需要强调Redis 压测输出默认没有 p50、p99 这种常见的百分位指标你需要自己按延迟分布算或者通过--latency和--latency-dist参数看分布图。比如redis-benchmark -h 10.0.1.20 -p 6379 -t get -c 200 -n 1000000 --latency-dist这个参数会在压测结束后输出一个类似gaussian曲线的分布图能直观看到p99在哪里。如果压测机是多核机器压测端默认是单线程发送命令这可能导致压测端自己成为瓶颈Redis还没满CPUQPS却上不去了。此时要加--threadsredis-benchmark -h 10.0.1.20 -p 6379 -t get -c 100 -n 1000000 --threads 4--threads会让压测端每条线程建立自己独立的连接集合这比较接近真实世界里多客户端服务连接Redis的情况。不加它你在高并发场景下得到的QPS很可能是压测线程的上限而不是Redis的真实上限。4.4 压测期间的监控手段避免“盲人摸象”很多人跑redis-benchmark只盯着最后的大数字忽略了压测过程中Redis自身的状态。带宽是否打满CPU是否只在单核上热内存是否出现大量换页这些都会严重影响压测结论。我自己的习惯是开三个终端在压测执行时同步跑终端1监控Redis实时状态redis-cli -h 10.0.1.20 -p 6379 --stat终端2检查CPU占用和缓存命中情况top -b -n 3 | grep redis终端3压测之后的系统指标快照vmstat 1 5如果发现压测过程中redis-cli --stat里的ops/sec和redis-benchmark报告的 QPS 接近说明压测流量真实打到了Redis如果差距巨大说明有网络代理、连接复用或者压测端本地的限流在作祟。4.5 压测常见的错误判断我在实际接触过的案例里最常见的错误是把redis-benchmark测出来的 QPS 当成业务系统的 QPS 上限。这是两回事。业务系统的调用链路里每个请求经过网络、序列化、业务逻辑、可能存在分布式锁和数据库查询Redis只是链路里的一环。redis-benchmark只能证明Redis本身在特定模型下的能力证明不了业务能跑多少。还不止这些redis-benchmark默认测试的命令都是无阻塞、无事务的简单命令。如果你的业务里有BLPOP、WAIT这类可能长时间阻塞的命令redis-benchmark根本没法模拟需要靠精确的脚本回归才能验出来。还有一个坑就是默认压测数据不检查命令返回的正确性。某些情况下Redis实例可能会出现错误或超时但redis-benchmark最终还是把请求数算完只报一个较低QPS和较高延迟。所以你要在压测结果里看一眼错误率。redis-benchmark本身不输出错误码统计但可以通过最后的延迟分布大致判断如果大量请求集中在高延迟段说明可能存在慢命令、网络排队或者定了过大的超时时间。5. 一次排查与压测的组合拳从怀疑到确认最后用一段我自己的实际经历把三件套如何串起来讲清楚。某个新项目上线前服务端程序使用C语言写的Redis刚部署好业务方反馈偶尔会出现查询很慢的现象。我第一反应是打开redis-cli --latency看网络延迟。结果显示平均延迟0.4毫秒左右最大延迟到了200毫秒。这个太不正常了——即使在本地网络毫秒级最大延迟突然跳到200ms明显有阻塞。进一步用redis-cli --intrinsic-latency 50测本机延迟基线最大10毫秒也正常。那问题很可能在Redis进程内部。我接着敲了以下命令查看慢日志redis-cli -h 10.0.1.20 -p 6379 SLOWLOG GET 20结果看到了很多KEYS命令每次执行耗时300-600毫秒。这就是典型的误用了KEYS通配符匹配在key数量大的时候全库扫描阻塞了Redis单线程。业务方本意是找一个前缀匹配的key我在命令行里演示了怎么用SCAN替代并把这段逻辑写进了C服务代码用redisCommandArgv发SCAN命令。替换完后我又用redis-benchmark做了对照压测redis-benchmark -h 10.0.1.20 -p 6379 -t get,set -c 100 -n 500000 -d 256 -r 1000000结果QPS从替换前的19万涨到了29万最大延迟也从200毫秒以上降到了3毫秒以内。这个案例里redis-cli负责定位问题hiredis负责在生产代码里替换错误用法redis-benchmark负责验证优化效果三者缺一不可。顺着这个案例说点经验redis-cli不只是给大家连上去看key用的它的SLOWLOG、INFO commandstats、CONFIG GET *这些命令在排查时特别有用。例如通过redis-cli INFO commandstats你能看到每条命令被调用多少次、总耗时多少、平均耗时多少。那次排查里KEYS命令的耗时统计高得吓人基本是实锤。如果怀疑是内存碎片导致性能劣化跑redis-cli INFO memory重点看mem_fragmentation_ratio和used_memory_rss的关系。这个指标可能受内存分配器影响波动不一定直接证明什么但结合压测对比还是能看到趋势。最后补充一点使用redis-cli和hiredis配合的经验凡是生产上有人误操作出现问题的事件几乎都集中在两种场景——用人肉方式做了本应该由代码做的批量操作以及用默认配置跑完压测就直接上了生产容量。所以你在读这篇文章之后可以先花几分钟检查一下自己的Redis环境里是否存在flushall裸奔、慢日志里有没有KEYS、压测报告里有没有记录--latency-dist和内存快照。这三步做完你的Redis运维水平已经超过了大部分只在能用层面的同行。