访问 redis
访问 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_verbose | false | [DEBUG] 打印每一个 redis 请求/响应 | src/brpc/policy/redis_protocol.cpp |
| redis_verbose_crlf2space | false | [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=482200 线程下的峰值 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)
评论
登录后参与评论
KnowForge