ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

brpc Redis 客户端完全指南:基于 bthread 的高性能 Redis 访问方案

2026/9/14 3:10:18 拓冰建站 浏览量
brpc Redis 客户端完全指南:基于 bthread 的高性能 Redis 访问方案 brpc Redis 客户端完全指南基于 bthread 的高性能 Redis 访问方案【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. brpc means better RPC.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpcbrpc 在 C 框架层面直接实现了 Redis 协议PROTOCOL_REDIS使得业务代码可以用统一的Channel/Controller编程模型访问 Redis天然获得线程安全、同步/异步/半同步调用、并行访问ParallelChannel、备份请求、超时控制、链路追踪与内置服务等 brpc 全家桶能力。本文以 docs/en/redis_client.md 为主线结合 src/brpc/redis.h、src/brpc/redis_reply.h、src/brpc/redis_cluster.h 等源码与 example/redis_c 示例系统讲解如何用 brpc 访问单机 Redis、带鉴权的 Redis、以及原生 Redis Cluster并深入剖析其性能优势与底层实现原理。读完本文你将能够独立完成 Redis 客户端的接入、命令构造、回复解析、集群路由与调试排障。为什么用 brpc 访问 Redis与官方客户端 hiredis 相比brpc 的 Redis 客户端具备以下差异化优势线程安全brpc::Channel本身是线程安全的所有线程可以共享同一个 Channel无需为每个线程单独建立客户端连接。调用模型丰富支持同步、异步、半同步访问可以通过 ParallelChannel 等组合方式声明式地定义访问模式。连接类型可配支持 brpc 的全部连接类型并继承超时、备份请求、取消、链路追踪、内置服务等 brpc 通用能力。连接复用进程内所有 brpc 客户端共享到同一 Redis 服务器的单一连接多线程并发访问同一 Redis 时效率更高回复内容无论多复杂都按块block分配内存并实现了短字符串优化SSO进一步提速。解析复杂度有保证与 HTTP 类似brpc 保证 Redis 回复解析的最坏时间复杂度是 O(N) 而非 O(N²)N 为回复字节数。当回复包含大型数组时这一点尤为关键。可观测性开启-redis_verbose可打印所有 Redis 请求与回复内容仅用于调试。从源码看这些能力的基础是 RedisRequest 将多条命令以管道方式序列化进一个butil::IOBufRedis 回复解析则由 RedisReply::ConsumePartialIOBuf 增量完成——它保证数据不足时缓冲区不变可反复重试解析从而把最坏复杂度维持在 O(N)。请求一台 Redis 服务器创建 ChannelRedis 是 brpc 的内置协议之一访问方式与访问其他 RPC 服务完全一致先创建brpc::Channel并设置protocol brpc::PROTOCOL_REDIS再调用Init指向 Redis 地址默认端口 6379#include brpc/redis.h #include brpc/channel.h brpc::ChannelOptions options; options.protocol brpc::PROTOCOL_REDIS; brpc::Channel redis_channel; if (redis_channel.Init(0.0.0.0:6379, options) ! 0) { // 6379 是 redis-server 的默认端口 LOG(ERROR) Fail to init channel to redis-server; return -1; }之后通过CallMethod发送RedisRequest并接收RedisResponsedone参数传nullptr即为同步调用传入回调对象即为异步调用。执行 SET 与 INCRstd::string my_key my_key_1; int my_number 1; ... // 执行 SET my_key my_number brpc::RedisRequest set_request; brpc::RedisResponse response; brpc::Controller cntl; set_request.AddCommand(SET %s %d, my_key.c_str(), my_number); redis_channel.CallMethod(nullptr, cntl, set_request, response, nullptr/*done*/); if (cntl.Failed()) { LOG(ERROR) Fail to access redis-server; return -1; } // 通过 response.reply(i) 获取第 i 条回复 if (response.reply(0).is_error()) { LOG(ERROR) Fail to set; return -1; } // 回复有多种打印方式 LOG(INFO) response.reply(0).c_str() // OK response.reply(0) // OK response; // OK ... // 执行 INCR my_key brpc::RedisRequest incr_request; incr_request.AddCommand(INCR %s, my_key.c_str()); response.Clear(); cntl.Reset(); redis_channel.CallMethod(nullptr, cntl, incr_request, response, nullptr/*done*/); if (cntl.Failed()) { LOG(ERROR) Fail to access redis-server; return -1; } if (response.reply(0).is_error()) { LOG(ERROR) Fail to incr; return -1; } // 回复有多种打印方式 LOG(INFO) response.reply(0).integer() // 2 response.reply(0) // (integer) 2 response; // (integer) 2批量执行Pipelinebrpc 允许在同一个RedisRequest中通过多次AddCommand放入多条命令一次 RPC 发送到服务器这正是 Redis 官方推荐的 Pipeline 用法。例如批量执行incr/decrbrpc::RedisRequest request; brpc::RedisResponse response; brpc::Controller cntl; request.AddCommand(INCR counter1); request.AddCommand(DECR counter1); request.AddCommand(INCRBY counter1 10); request.AddCommand(DECRBY counter1 20); redis_channel.CallMethod(nullptr, cntl, request, response, nullptr/*done*/); if (cntl.Failed()) { LOG(ERROR) Fail to access redis-server; return -1; } CHECK_EQ(4, response.reply_size()); for (int i 0; i 4; i) { CHECK(response.reply(i).is_integer()); CHECK_EQ(brpc::REDIS_REPLY_INTEGER, response.reply(i).type()); } CHECK_EQ(1, response.reply(0).integer()); CHECK_EQ(0, response.reply(1).integer()); CHECK_EQ(10, response.reply(2).integer()); CHECK_EQ(-10, response.reply(3).integer());注意在 hiredis 中若请求包含 N 条命令需要调用redisGetReplyN 次才能取完所有回复而 brpc 的RedisResponse已包含全部 N 条回复直接用reply(i)即可按位置对应获取。带鉴权访问 Redis当 Redis 开启了requirepass时需要为 Channel 配置认证器。创建brpc::policy::RedisAuthenticator并赋给ChannelOptions::authbrpc::ChannelOptions options; brpc::policy::RedisAuthenticator* auth new brpc::policy::RedisAuthenticator(my_password); options.auth auth;从 redis_authenticator.h 的源码可以看到RedisAuthenticator的构造函数签名是RedisAuthenticator(const std::string passwd, int db -1)第二个参数为数据库索引默认-1表示不切换数据库。GenerateCredential 的实现会根据参数依次生成AUTH passwd与当db 0时SELECT db两条命令在连接建立阶段自动完成鉴权业务代码无需关心握手细节。因此带库索引的用法为new brpc::policy::RedisAuthenticator(my_password, 0)。RedisRequest命令构造RedisRequest 可通过AddCommand*系列方法追加多条命令成功返回true失败返回false并打印调用栈callsite backtracebool AddCommand(const char* fmt, ...); bool AddCommandV(const char* fmt, va_list args); bool AddCommandByComponents(const butil::StringPiece* components, size_t n);格式化规则格式化说明符与 hiredis 兼容%b对应二进制数据指针 长度其余与printf类似。brpc 还做了一些改进例如被单引号或双引号包裹的内容会被识别为一个字段无论其中是否含有空格AddCommand(Set a key with space a value with space as well)上述调用会把a value with space as well设置到键a key with space而在 hiredis 中必须写成redisvCommand(..., SET% s% s, a key with space, a value with space as well)。⚠️安全警告AddCommand和AddCommandV的fmt参数如果设置错误可能导致程序崩溃或数据泄露请谨慎使用绝不要将受用户输入影响的内容作为fmt参数用户输入应作为参数传入而不是拼进格式串。AddCommandByComponentsAddCommandByComponents与 hiredis 的redisCommandArgv类似把命令的每个部分放在数组中指定天然规避了AddCommand/AddCommandV中常见的转义问题。如果使用AddCommand/AddCommandV时遇到Unmatched quote或invalid format等错误应改用本方法例如butil::StringPiece components[] { set, key, value }; request.AddCommandByComponents(components, arraysize(components));失败语义与复用一旦某次AddCommand*失败后续的AddCommand*和CallMethod也会失败因此一般无需检查AddCommand*的返回值——RPC 本身会直接失败。用command_size()获取已成功加入的命令数对应源码中的_ncommand见 redis.h。复用RedisRequest对象前必须先调用Clear()。RedisResponse回复解析RedisResponse 可能包含一条或多条 RedisReply。用reply_size()获取回复总数用reply(i)获取第 i 条回复从 0 计数。只要 RPC 成功response.reply_size()应当等于request.command_size()前提是 Redis 服务器行为正常——Redis 协议保证回复与命令按相同顺序一一对应位置对应关系。RedisReply 的六种类型RedisReply的类型由 redis_reply.h 中的RedisReplyType枚举定义枚举值对应 Redis 文档判断方法取值方法说明REDIS_REPLY_NILNULLis_nil()—表示值不存在REDIS_REPLY_STATUSSimple Stringis_string()c_str()/data()通常表示操作状态如SET返回的OKREDIS_REPLY_STRINGBulk Stringis_string()c_str()/data()大多数返回值含incr的返回值属于此类型REDIS_REPLY_ERRORErroris_error()error_message()操作失败时的错误消息REDIS_REPLY_INTEGERIntegeris_integer()integer()64 位有符号整数REDIS_REPLY_ARRAYArrayis_array()size()取大小、[i]取子回复回复数组可嵌套注REDIS_REPLY_STATUS与REDIS_REPLY_STRING共用is_string()见 redis_reply.h 的实现。嵌套数组的访问如果一个响应包含三条回复——一个整数、一个字符串和一个含 2 个元素的数组可分别用response.reply(0).integer()、response.reply(1).c_str()以及response.reply(2)[0]、response.reply(2)[1]取值。如果类型不匹配会打印调用栈并返回未定义值整数返回 0、字符串返回空串见 redis_reply.h 与c_str()/data()的实现。内存归属与复用所有回复的内存都归RedisResponse所有response 销毁时所有 reply 一并销毁用户无需手动释放。复用RedisResponse对象前必须先调用Clear()。data()返回butil::StringPiece可避免拷贝若需std::string调用.data().as_string()会分配内存。含\0的字符串用c_str()无法完整打印应使用data()。请求 Redis 集群方案一一致性哈希 命名服务创建一个使用一致性哈希负载均衡算法c_md5或c_murmurhash的Channel挂载到命名服务下即可访问 Redis 集群。注意约束每个RedisRequest只能包含一条命令或所有命令都使用相同 key。当前实现会把单次请求内的多条命令始终发往同一台服务器如果这些 key 分布在不同的服务器上结果必然错误。此时必须把请求拆成多条、每条一条命令。另一个常见选择是部署 twemproxy 代理方案客户端像访问单机一样访问集群但需要额外部署代理节点且增加一跳延迟。方案二RedisClusterChannel原生 Redis Cluster对于原生 Redis Cluster基于 slot 的路由、MOVED/ASK 重定向、从CLUSTER SLOTS/CLUSTER NODES刷新拓扑brpc 提供brpc::RedisClusterChannel#include brpc/redis_cluster.h brpc::RedisClusterChannel channel; brpc::RedisClusterChannelOptions options; options.max_redirect 5; if (channel.Init(127.0.0.1:7000,127.0.0.1:7001, options) ! 0) { LOG(ERROR) Fail to init redis cluster channel; }Init的第一个参数是逗号分隔的 seed 节点列表可传入多个引导节点。RedisClusterChannel支持同步/异步CallMethodMOVED/ASK 自动重定向与重试周期性拓扑刷新。多 key 命令支持MGET/MSET/DEL/EXISTS/UNLINK/EVAL/EVALSHAMULTI/EXEC目前不支持按设计会返回错误回复。从 redis_cluster.h 的源码结构可以看到其内部实现PickEndpointForKey/HashSlot/ExtractHashtag负责 slot 计算与 key 的 hashtag 提取RefreshTopologyFromEndpoint/FetchAndParseClusterSlots/FetchAndParseClusterNodes负责拓扑获取SendToEndpoint/ParseRedirectReply处理 MOVED/ASK 重定向拓扑数据存放于DoublyBufferedData中实现无锁读。RedisClusterChannel 选项RedisClusterChannelOptions见 redis_cluster.h的常用字段选项默认含义说明max_redirect每条命令的最大重定向次数超过后报错refresh_interval_s周期性拓扑刷新间隔单位秒topology_refresh_timeout_ms拓扑命令CLUSTER SLOTS/CLUSTER NODES超时单位毫秒channel_options普通 brpc Channel 选项作用于每个 Redis 节点的内部 Channelenable_periodic_refresh是否开启周期性刷新若应用自行控制刷新可关闭RedisClusterChannel 示例example/redis_c/redis_cluster_client.cpp 演示了从多个 seed 节点引导bootstrapMOVED/ASK 自动重定向与重试从CLUSTER SLOTS刷新拓扑失败时回退到CLUSTER NODES使用同一个 Channel 进行同步 Pipeline 与异步调用。编译运行cd example/redis_c make redis_cluster_client ./redis_cluster_client \ --seeds127.0.0.1:7000,127.0.0.1:7001 \ --max_redirect5 \ --timeout_ms1000对应的 gflags 定义在示例文件头部redis_cluster_client.cpp--seeds、--key_prefix、--timeout_ms、--rpc_max_retry、--max_redirect、--refresh_interval_s、--topology_refresh_timeout_ms、--disable_periodic_refresh。示例中同步 Pipeline 一次性set两个 key 再mget异步调用则通过bthread::CountdownEvent配合Done回调等待完成。注意点MGET/MSET/DEL/EXISTS/UNLINK按 key 逐个执行后在请求顺序上合并结果EVAL/EVALSHA要求声明的所有 key 落在同一个 slotMULTI/EXEC按设计返回错误回复。调试redis_verbose 开关开启-redis_verbose会打印所有 Redis 请求与回复的内容仅用于调试切勿用于线上服务。同时可开启-redis_verbose_crlf2space把调试日志中的CRLF\r\n替换为空格提升可读性。名称默认值描述定义位置redis_verbosefalse[DEBUG] 打印每条 redis 请求/回复src/brpc/policy/redis_protocol.cppredis_verbose_crlf2spacefalse[DEBUG] 将 \r\n 显示为空格src/brpc/redis.cpp性能数据与原理以下为文档记录的基准测试Redis 版本 2.6.14客户端与 redis-server 位于同一台机器分别用 1、50、200 个 bthread 同步发送请求延迟单位为微秒。注意这是文档记载的历史测试数据反映的是 brpc 单连接合并发送的设计优势具体数值会随硬件、版本与网络环境变化。单条命令场景单连接$ ./client -use_bthread -thread_num 1 TRACE: 02-13 19:42:04: * 0 client.cpp:180] Accessing redis server at qps18668 latency50 TRACE: 02-13 19:42:05: * 0 client.cpp:180] Accessing redis server at qps17043 latency52 TRACE: 02-13 19:42:06: * 0 client.cpp:180] Accessing redis server at qps16520 latency54 $ ./client -use_bthread -thread_num 50 TRACE: 02-13 19:42:54: * 0 client.cpp:180] Accessing redis server at qps301212 latency164 TRACE: 02-13 19:42:55: * 0 client.cpp:180] Accessing redis server at qps301203 latency164 TRACE: 02-13 19:42:56: * 0 client.cpp:180] Accessing redis server at qps302158 latency164 $ ./client -use_bthread -thread_num 200 TRACE: 02-13 19:43:48: * 0 client.cpp:180] Accessing redis server at qps411669 latency483 TRACE: 02-13 19:43:49: * 0 client.cpp:180] Accessing redis server at qps411679 latency483 TRACE: 02-13 19:43:50: * 0 client.cpp:180] Accessing redis server at qps412583 latency482200 线程时峰值 QPS 远高于 hiredisbrpc 默认对同一 redis-server 使用单连接多线程的请求以 wait-free 方式合并发送使 redis-server 批量收到请求从而获得远高于 hiredis 的 QPS。后续使用连接池pooled的测试 QPS 明显更低就是佐证。批量场景每个请求 10 条命令$ ./client -use_bthread -thread_num 1 -batch 10 TRACE: 02-13 19:46:45: * 0 client.cpp:180] Accessing redis server at qps15880 latency59 TRACE: 02-13 19:46:46: * 0 client.cpp:180] Accessing redis server at qps16945 latency57 TRACE: 02-13 19:46:47: * 0 client.cpp:180] Accessing redis server at qps16728 latency57 $ ./client -use_bthread -thread_num 50 -batch 10 TRACE: 02-13 19:47:14: * 0 client.cpp:180] Accessing redis server at qps38082 latency1307 TRACE: 02-13 19:47:15: * 0 client.cpp:180] Accessing redis server at qps38267 latency1304 TRACE: 02-13 19:47:16: * 0 client.cpp:180] Accessing redis server at qps38070 latency1305 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 16878 gejun 20 0 48136 2436 1004 R 93.8 0.0 12:48.56 redis-server // thread_num50 $ ./client -use_bthread -thread_num 200 -batch 10 TRACE: 02-13 19:49:09: * 0 client.cpp:180] Accessing redis server at qps29053 latency6875 TRACE: 02-13 19:49:10: * 0 client.cpp:180] Accessing redis server at qps29163 latency6855 TRACE: 02-13 19:49:11: * 0 client.cpp:180] Accessing redis server at qps29271 latency6838 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 16878 gejun 20 0 48136 2508 1004 R 99.9 0.0 13:36.59 redis-server // thread_num200注意redis-server 每秒处理的命令数是 QPS 乘以 10约为 40 万。当 thread_num 达到 50 或更高时redis-server 的 CPU 使用率已到达上限——redis-server 是单线程 reactor 模型单核即为其性能天花板。连接池对照场景50 个 bthreadpooled 连接$ ./client -use_bthread -connection_type pooled TRACE: 02-13 18:07:40: * 0 client.cpp:180] Accessing redis server at qps75986 latency654 TRACE: 02-13 18:07:41: * 0 client.cpp:180] Accessing redis server at qps75562 latency655 TRACE: 02-13 18:07:42: * 0 client.cpp:180] Accessing redis server at qps75238 latency657 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 16878 gejun 20 0 48136 2520 1004 R 99.9 0.0 9:52.33 redis-server与单连接相比 QPS 大幅下降且 redis-server 达到 CPU 上限原因是连接池场景下 redis-server 每条连接同一时刻只能读一个请求IO 操作成本显著增加。这也是 hiredis 客户端的性能上限所在。命令行工具 redis_cliexample/redis_c/redis_cli.cpp 是一个模拟官方 redis-cli 交互风格、基于 brpc 实现的命令行工具用于演示 brpc 与 Redis 的对话能力。当使用 brpc 客户端从 redis-server 得到意外结果时可以用它进行交互式排障。其 gflags 包括--server默认127.0.0.1:6379、--connection_type可选single/pooled/short、--timeout_ms默认 1000与--max_retry默认 3。像官方 CLI 一样redis_cli command直接执行单条命令也可用-server指定服务器地址$ ./redis_cli __ _ __ / /_ ____ _(_)___/ /_ __ _________ _____ / __ \/ __ / / __ / / / /_____/ ___/ __ \/ ___/ / /_/ / /_/ / / /_/ / /_/ /_____/ / / /_/ / /__ /_.___/\__,_/_/\__,_/\__,_/ /_/ / .___/\___/ /_/ This command-line tool mimics the look-n-feel of official redis-cli, as a demostration of brpcs capability of talking to redis server. The output and behavior is not exactly same with the official one. redis 127.0.0.1:6379 mset key1 foo key2 bar key3 17 OK redis 127.0.0.1:6379 mget key1 key2 key3 [foo, bar, 17] redis 127.0.0.1:6379 incrby key3 10 (integer) 27 redis 127.0.0.1:6379 client setname brpc-cli OK redis 127.0.0.1:6379 client getname brpc-cli该工具的输出与行为与官方 redis-cli 并不完全一致。对于原生 Redis Cluster可参考 example/redis_c/redis_cluster_client.cpp。进一步探索example/redis_c/redis_cli.cpp交互式 CLI 完整实现example/redis_c/redis_cluster_client.cppRedis Cluster 客户端完整示例example/redis_c/redis_press.cpp压测工具用于复现本文的性能测试example/redis_c/redis_server.cpp基于 brpc 服务端实现 Redis 服务的示例对应 redis.h 中的RedisService/RedisCommandHandler连接类型、超时与重试等通用选项见 client.md组合 Channel 见 combo_channel.md。【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. brpc means better RPC.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考