访问 redis

qianmoQqianmoQ· 更新于 2026-10-05· 阅读 25 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

访问 redis

学习如何访问 redis。

redis 是近年最受欢迎的缓存服务之一。与 memcached 相比,它为用户提供了更多的数据结构和操作,加快了开发速度。为了更方便地访问 redis 服务器,并充分发挥 bthread 的并发能力,brpc 直接支持 redis 协议。示例请参见 example/redis_c++。

与 hiredis(redis 官方客户端)相比的优势:

  • 线程安全。无需为每个线程分别创建客户端。
  • 支持同步、异步、半同步等访问方式。支持 ParallelChannel 等以声明式方式定义访问模式。
  • 支持多种连接类型。支持超时、备份请求、取消、追踪、内建服务以及 brpc 提供的其他特性。
  • 一个进程中的所有 brpc 客户端共享与同一个 redis-server 的单条连接,当多个线程同时访问同一个 redis-server 时效率更高(参见性能)。无论回复的复杂程度如何,内存都按块分配,并实现了短字符串优化(SSO)以进一步提升性能。

与 http 类似,brpc 保证解析 redis 回复的时间复杂度在最坏情况下为 O(N) 而非 O(N^2),其中 N 为回复的字节数。当回复包含大型数组时,这一点非常重要。

打开 -redis_verbose 可以打印所有 redis 请求和响应的内容,仅用于调试。

请求 redis 服务器

创建一个 Channel 用于访问 redis:

#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 is the default port for redis-server
   LOG(ERROR) << "Fail to init channel to redis-server";
   return -1;
}
...

执行 SET,然后 INCR:

std::string my_key = "my_key_1";
int my_number = 1;
...
// Execute "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(NULL, &cntl, &set_request, &response, NULL/*done*/);
if (cntl.Failed()) {
    LOG(ERROR) << "Fail to access redis-server";
    return -1;
}
// Get a reply by calling response.reply(i)
if (response.reply(0).is_error()) {
    LOG(ERROR) << "Fail to set";
    return -1;
}
// A reply is printable in multiple ways
LOG(INFO) << response.reply(0).c_str()  // OK
          << response.reply(0)          // OK
          << response;                  // OK
...

// Execute "INCR <my_key>"
brpc::RedisRequest incr_request;
incr_request.AddCommand("INCR %s", my_key.c_str());
response.Clear();
cntl.Reset();
redis_channel.CallMethod(NULL, &cntl, &incr_request, &response, NULL/*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;
}
// A reply is printable in multiple ways
LOG(INFO) << response.reply(0).integer()  // 2
          << response.reply(0)            // (integer) 2
          << response;                    // (integer) 2

批量执行 incr 和 decr:

brpc::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(NULL, &cntl, &request, &response, NULL/*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());

RedisRequest

一个 RedisRequest 可以通过调用 AddCommand* 包含多个命令,调用成功时返回 true,否则返回 false。出错时同样会打印调用点的回溯信息。

bool AddCommand(const char* fmt, ...);
bool AddCommandV(const char* fmt, va_list args);
bool AddCommandByComponents(const butil::StringPiece* components, size_t n);

格式与 hiredis 兼容,即 %b 对应二进制数据(指针 + 长度),其余与 printf 中的类似。同时还做了一些改进,例如用单引号或双引号括起来的内容,无论其中是否含有空格,都会被识别为一个字段。例如,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");

AddCommandByComponents 与 hiredis 中的 redisCommandArgv 类似。用户在数组中指定命令的各个部分,从而避免了在 AddCommand 和 AddCommandV 中经常出现的转义问题。如果在使用 AddCommand 和 AddCommandV 时遇到 “Unmatched quote” 或 “invalid format” 之类的错误,请尝试这种方法。

如果 AddCommand* 失败,后续的 AddCommand* 和 CallMethod 也会失败。通常无需检查 AddCommand* 的返回值,因为 RPC 反正会直接失败。

使用 command_size() 获取成功添加的命令数量。

在重新使用 RedisRequest 对象之前,请先调用 Clear()。

RedisResponse

一个 RedisResponse 可以包含一个或多个 RedisReply。使用 reply_size() 获取回复总数,使用 reply(i) 获取第 i 个回复(从 0 开始计数)的引用。注意,在 hiredis 中,如果请求包含 N 条命令,就必须调用 redisGetReply N 次才能得到各个回复;而在 brpc 中则没有必要,因为 RedisResponse 已经包含了这 N 个回复,可以通过 reply(i) 访问。只要 RPC 成功,response.reply_size() 就应该等于 request.command_size(),除非 redis-server 存在缺陷。redis 正常工作的前提是回复与命令按相同顺序一一对应(位置对应)。

每个 RedisReply 可能是:

  • REDIS_REPLY_NIL:redis 中的 NULL,表示该值不存在。可用 is_nil() 判断。
  • REDIS_REPLY_STATUS:redis 文档中称为 Simple String,通常用作操作的状态,例如 SET 返回的 OK。可用 is_string() 判断(与 REDIS_REPLY_STRING 使用同一个函数)。使用 c_str() 或 data() 获取其值。
  • REDIS_REPLY_STRING:redis 文档中称为 Bulk String。大多数返回值都是这种类型,包括 incr 返回的值。可用 is_string() 判断,使用 c_str() 或 data() 获取其值。
  • REDIS_REPLY_ERROR:操作失败时的错误消息。可用 is_error() 判断,使用 error_message() 获取该消息。
  • REDIS_REPLY_INTEGER:64 位有符号整数。可用 is_integer() 判断,使用 integer() 获取其值。
  • REDIS_REPLY_ARRAY:回复组成的数组。可用 is_array() 判断,使用 size() 获取数组大小,使用 [i] 获取对应子回复的引用。

如果一个响应包含三个回复:一个整数、一个字符串和一个包含 2 个元素的数组,则可以分别使用 response.reply(0).integer()、response.reply(1).c_str() 以及 repsonse.reply(2)[0]、repsonse.reply(2)[1] 来获取它们的值。如果类型不正确,会打印出调用点的回溯信息,并返回一个未定义的值。

所有回复的所有权归 RedisResponse 所有。当响应被销毁时,所有回复也会随之销毁。

在重新使用 RedisRespones 对象之前,请先调用 Clear()。

请求 redis 集群

使用一致性哈希作为负载均衡算法(c_md5 或 c_murmurhash)创建一个 Channel,以访问挂载在命名服务下的 redis 集群。注意,每个 RedisRequest 中应只包含一个命令,或者所有命令具有相同的 key。在当前实现中,单个请求中的多个命令始终被发送到同一台服务器。如果 key 分布在不同的服务器上,结果一定是错误的。在这种情况下,你必须将请求拆分为多个请求,每个请求包含一个命令。

另一种选择是使用通用的 twemproxy 解决方案,该方案使客户端访问集群就像访问单台服务器一样,不过该方案需要部署代理并增加了额外的延迟。

调试

开启 -redis_verbose 可以打印所有 redis 请求和响应的内容。注意,此选项仅应用于调试,而非线上服务。

开启 -redis_verbose_crlf2space 可以在调试日志中用空格替换 CRLF(\r\n),以提高可读性。

名称值描述定义位置
redis_verbosefalse[DEBUG] 打印每一个 redis 请求/响应src/brpc/policy/redis_protocol.cpp
redis_verbose_crlf2spacefalse[DEBUG] 将 \r\n 显示为空格src/brpc/redis.cpp

性能

redis 版本:2.6.14

启动一个客户端,使用 1、50、200 个 bthreads 同步向同一台机器上的 redis-server 发送请求。延迟以微秒为单位。

$ ./client -use_bthread -thread_num 1
TRACE: 02-13 19:42:04:   * 0 client.cpp:180] Accessing redis server at qps=18668 latency=50
TRACE: 02-13 19:42:05:   * 0 client.cpp:180] Accessing redis server at qps=17043 latency=52
TRACE: 02-13 19:42:06:   * 0 client.cpp:180] Accessing redis server at qps=16520 latency=54

$ ./client -use_bthread -thread_num 50
TRACE: 02-13 19:42:54:   * 0 client.cpp:180] Accessing redis server at qps=301212 latency=164
TRACE: 02-13 19:42:55:   * 0 client.cpp:180] Accessing redis server at qps=301203 latency=164
TRACE: 02-13 19:42:56:   * 0 client.cpp:180] Accessing redis server at qps=302158 latency=164

$ ./client -use_bthread -thread_num 200
TRACE: 02-13 19:43:48:   * 0 client.cpp:180] Accessing redis server at qps=411669 latency=483
TRACE: 02-13 19:43:49:   * 0 client.cpp:180] Accessing redis server at qps=411679 latency=483
TRACE: 02-13 19:43:50:   * 0 client.cpp:180] Accessing redis server at qps=412583 latency=482

200 线程下的峰值 QPS 远高于 hiredis,因为 brpc 默认使用单个连接访问 redis-server,多个线程的请求以无等待的方式](https://brpc.apache.org/docs/rpc-in-depth/io#sending-messages)进行合并,使 redis-server 能够批量接收请求,从而达到高得多的 QPS。下面使用连接池的测试中较低的 QPS 也印证了这一点。

启动一个客户端,使用 1、50、200 个 bthread 同步向同一台机器上的 redis-server 批量发送请求(每请求 10 条命令),延迟单位为微秒。

$ ./client -use_bthread -thread_num 1 -batch 10
TRACE: 02-13 19:46:45:   * 0 client.cpp:180] Accessing redis server at qps=15880 latency=59
TRACE: 02-13 19:46:46:   * 0 client.cpp:180] Accessing redis server at qps=16945 latency=57
TRACE: 02-13 19:46:47:   * 0 client.cpp:180] Accessing redis server at qps=16728 latency=57

$ ./client -use_bthread -thread_num 50 -batch 10
TRACE: 02-13 19:47:14:   * 0 client.cpp:180] Accessing redis server at qps=38082 latency=1307
TRACE: 02-13 19:47:15:   * 0 client.cpp:180] Accessing redis server at qps=38267 latency=1304
TRACE: 02-13 19:47:16:   * 0 client.cpp:180] Accessing redis server at qps=38070 latency=1305

  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_num=50

$ ./client -use_bthread -thread_num 200 -batch 10
TRACE: 02-13 19:49:09:   * 0 client.cpp:180] Accessing redis server at qps=29053 latency=6875
TRACE: 02-13 19:49:10:   * 0 client.cpp:180] Accessing redis server at qps=29163 latency=6855
TRACE: 02-13 19:49:11:   * 0 client.cpp:180] Accessing redis server at qps=29271 latency=6838

  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_num=200

注意,redis-server 每秒处理的命令数是 QPS 的 10 倍,约为 400K。当 thread_num 等于 50 或更高时,redis-server 的 CPU 使用率达到上限。注意 redis-server 是一个 单线程 reactor,它最多只能利用一个核心。

现在启动一个客户端,通过池化连接,使用 50 个 bthread 向同一台机器上的 redis-server 同步发送请求。

$ ./client -use_bthread -connection_type pooled
TRACE: 02-13 18:07:40:   * 0 client.cpp:180] Accessing redis server at qps=75986 latency=654
TRACE: 02-13 18:07:41:   * 0 client.cpp:180] Accessing redis server at qps=75562 latency=655
TRACE: 02-13 18:07:42:   * 0 client.cpp:180] Accessing redis server at qps=75238 latency=657

  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 客户端的峰值性能。

命令行接口

example/redis_c++/redis_cli 是一个与官方 CLI 类似的命令行工具,用于演示 brpc 与 redis 服务器通信的能力。当你使用 brpc 客户端访问 redis-server 得到非预期结果时,也可以使用该工具进行交互式调试。

与官方 CLI 一样,redis_cli <command> 直接执行命令,并且可以指定 -server,即 redis-server 的地址。

$ ./redis_cli
     __          _     __
    / /_  ____ _(_)___/ /_  __      _________  _____
   / __ \/ __ `/ / __  / / / /_____/ ___/ __ \/ ___/
  / /_/ / /_/ / / /_/ / /_/ /_____/ /  / /_/ / /__
 /_.___/\__,_/_/\__,_/\__,_/     /_/  / .___/\___/
                                     /_/
This command-line tool mimics the look-n-feel of official redis-cli, as a
demostration of brpc's 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"

最后修改于 2022 年 5 月 17 日:更新 brpc 用户页面 (devlive-community/knowforge#71) (a31ce10d3)

评论

登录后参与评论

正在加载评论…