Boost.Redis 是一个基于 Redis 构建的高级客户端库,它构建于 Boost.Asio 之上,并实现了 Redis 协议 RESP3。使用 Boost.Redis 的要求是
要使用该库,必须包含
在您的应用程序中不超过一个源文件中。要使用 cmake 构建示例和测试,请运行
更多详情请参见 https://github.com/boostorg/cmake。
以下代码使用一个短连接来 ping Redis 服务器
async_run
和 async_exec
函数扮演的角色是
async_exec
:执行请求中包含的命令,并将各个响应存储在 resp
对象中。可以从代码中的多个位置并发调用。async_run
:解析、连接、ssl 握手、resp3 握手、健康检查、重新连接和协调底层读取和写入操作(以及其他事项)。Redis 服务器还可以向客户端发送各种推送,其中一些是
连接类通过 boost::redis::connection::async_receive
函数支持服务器推送,该函数可以在用于执行命令的同一连接中调用。下面的协程展示了如何使用它
Redis 请求由一个或多个命令组成(在 Redis 文档中,它们被称为 管道)。例如
向 Redis 发送请求是通过已经声明的 boost::redis::connection::async_exec
执行的。请求内部的 boost::redis::request::config
对象决定了 boost::redis::connection
在某些情况下如何处理请求。建议读者仔细阅读它。
Boost.Redis 使用以下策略来处理 Redis 响应
boost::redis::request
用于命令数量不动态的请求。boost::redis::generic_response
用于大小动态的情况。例如,以下请求有三个命令
因此,它的响应也将包含三个元素,可以在以下响应对象中读取
响应的行为类似于元组,并且必须具有与请求命令数相同的元素(以下例外情况)。还需要每个元组元素都能够存储对其引用的命令的响应,否则将发生错误。要忽略请求中各个命令的响应,请使用标签 boost::redis::ignore_t
,例如
下表提供了某些 Redis 命令返回的 resp3 类型
命令 | RESP3 类型 | 文档 |
---|---|---|
lpush | 数字 | https://redis.ac.cn/commands/lpush |
lrange | 数组 | https://redis.ac.cn/commands/lrange |
set | 简单字符串、null 或 blob 字符串 | https://redis.ac.cn/commands/set |
get | Blob 字符串 | https://redis.ac.cn/commands/get |
smembers | 集合 | https://redis.ac.cn/commands/smembers |
hgetall | 映射 | https://redis.ac.cn/commands/hgetall |
要将这些 RESP3 类型映射到 C++ 数据结构,请使用下表
RESP3 类型 | 可能的 C++ 类型 | 类型 |
---|---|---|
简单字符串 | std::string | 简单 |
简单错误 | std::string | 简单 |
Blob 字符串 | std::string , std::vector | 简单 |
Blob 错误 | std::string , std::vector | 简单 |
数字 | long long , int , std::size_t , std::string | 简单 |
双精度浮点数 | double , std::string | 简单 |
Null | std::optional<T> | 简单 |
数组 | std::vector , std::list , std::array , std::deque | 聚合 |
映射 | std::vector , std::map , std::unordered_map | 聚合 |
集合 | std::vector , std::set , std::unordered_set | 聚合 |
推送 | std::vector , std::map , std::unordered_map | 聚合 |
例如,对以下请求的响应
可以在下面的响应对象中读取
然后,要执行请求并读取响应,请使用如下所示的 async_exec
如果目的是完全忽略响应,请使用 ignore
包含嵌套聚合或异构数据类型的响应将在后面的“一般情况”中进行特殊处理。截至撰写本文时,并非所有 RESP3 类型都被 Redis 服务器使用,这意味着在实践中,用户将关注 RESP3 规范的简化子集。
没有响应的命令,例如
"SUBSCRIBE"
"PSUBSCRIBE"
"UNSUBSCRIBE"
不应包含在响应元组中。例如,以下请求
必须在响应对象 response<std::string, std::string>
中读取。
应用程序访问 Redis 服务器中不存在或已过期的键是很常见的,要处理这些用例,请使用 std::optional
包装类型,如下所示
其他一切保持不变。
要读取事务的响应,我们必须首先观察到 Redis 会将事务命令排队,并将它们的各个响应作为数组的元素发送,该数组本身就是对 EXEC
命令的响应。例如,要读取对此请求的响应
使用以下响应类型
有关完整示例,请参见 cpp20_containers.cpp。
在某些情况下,Redis 命令的响应不适合上述模型,一些示例是
set
)。期望 int
并接收 blob 字符串会导致错误。response
中读取。为了处理这些情况,Boost.Redis 提供了 boost::redis::resp3::node
类型抽象,它是响应中最通用形式的元素,无论是简单的 RESP3 类型还是聚合的元素。它的定义如下
对 Redis 命令的任何响应都可以在 boost::redis::generic_response
中接收。该向量可以看作是响应树的预序视图。使用它与使用其他类型没有区别
例如,假设我们要使用 HGETALL
从 Redis 检索哈希数据结构,一些选项是
boost::redis::generic_response
:始终有效。std::vector<std::string>
:高效且扁平,所有元素均为字符串。std::map<std::string, std::string>
:如果您需要将数据作为 std::map
,则高效。std::map<U, V>
:如果您存储序列化数据,则高效。避免临时变量,并且 U
和 V
需要 boost_redis_from_bulk
。除了上述内容外,用户还可以使用容器的无序版本。相同的推理适用于集合,例如 SMEMBERS
和一般的其他数据结构。
Boost.Redis 通过以下自定义点支持用户定义类型的序列化
这些函数通过 ADL 访问,因此用户必须将它们导入全局命名空间。在“示例”部分,读者可以找到展示如何使用 json 和 protobuf 进行序列化的示例。
以下示例展示了如何使用到目前为止讨论的功能
async_run
并对 async_exec
执行同步调用。某些异步示例中使用的 main 函数已分解到 main.cpp 文件中。
本文档对我使用不同语言和不同 Redis 客户端实现的 TCP 回声服务器的性能进行了基准测试。选择回声服务器的主要动机是
我还对实现施加了一些约束
要重现这些结果,请在一个终端中运行一个回声服务器程序,并在另一个终端中运行 echo-server-client。
首先,我测试了一个纯 TCP 回声服务器,即直接将消息发送给客户端而不与 Redis 交互的服务器。结果如下所示
测试是在本地主机上使用 1000 个并发 TCP 连接执行的,在我的机器上平均延迟为 0.07 毫秒。在更高延迟的网络上,库之间的差异预计会减少。
基准测试中使用的代码可以在以下位置找到
这与上述回声服务器类似,但消息由 Redis 而不是回声服务器本身回显,回声服务器充当客户端和 Redis 服务器之间的代理。结果如下所示
测试是在平均延迟为 35 毫秒的网络上执行的,否则它使用与上一个示例相同数量的 TCP 连接。
正如读者所见,Libuv 和 Rust 测试未在图中描述,原因是
基准测试中使用的代码可以在以下位置找到
Redis 客户端必须支持自动管道才能具有竞争力的性能。有关本文档的更新,请关注 https://github.com/boostorg/redis。
我开始编写 Boost.Redis 的主要原因是为了拥有一个与 Asio 异步模型兼容的客户端。随着我的进展,我还可以解决我认为其他库中的弱点。由于时间限制,我无法与 官方 列表中列出的每个客户端进行详细比较,相反,我将重点关注 github 上星数最多的最受欢迎的 C++ 客户端,即
在我们开始之前,重要的是要提及 redis-plus-plus 不支持的一些内容
其余要点将单独讨论。让我们首先看看发送命令、管道和事务是什么样的
此 API 的一些问题是
根据文档,redis-plus-plus 中的管道具有以下特征
注意:默认情况下,创建 Pipeline 对象并不便宜,因为它会创建一个新连接。
这显然是 API 的一个缺点,因为管道应该是默认的通信方式,而不是例外,为每个管道付出如此高的代价会对性能造成严重影响。事务也遭受着非常相同的问题。
注意:创建 Transaction 对象并不便宜,因为它会创建一个新连接。
在 Boost.Redis 中,发送一个命令、管道或事务之间没有区别,因为请求与 IO 对象是解耦的。
redis-plus-plus 还支持异步接口,但是,事务和订阅者的异步支持仍在进行中。
异步接口依赖于第三方事件库,到目前为止,仅支持 libuv。
redis-plus-plus 中的异步代码如下所示
正如读者所见,异步接口基于 future,这也以性能不佳而闻名。然而,这种异步设计最大的问题是,它使得正确编写异步程序成为不可能,因为它在发送的每个命令上启动一个异步操作,而不是将消息排队并在可以发送时触发写入。也不清楚如何使用这种设计来实现管道(如果可以实现的话)。
高级 页面记录了所有公共类型。
感谢帮助塑造 Boost.Redis 的人员
AUTH
和 HELLO
命令如何相互影响。async_exec
的调用应失败的场景。还要非常感谢所有参与 Boost 评审的个人
评审可以在这里找到:https://lists.boost.org/Archives/boost/2023/01/date.php。来自评审管理器的 ACCEPT 线程可以在这里找到:https://lists.boost.org/Archives/boost/2023/01/253944.php。
std::optional
,例如 response<std::optional<std::vector<std::string>>>
。但在某些情况下,例如 MGET 命令,向量中的每个元素都可能不存在,现在可以将响应指定为 response<std::optional<std::vector<std::optional<std::string>>>>
。deferred
作为连接默认完成令牌。async_exec
重载,允许传递响应适配器。这使得可以直接在自定义数据结构中接收 Redis 响应,从而避免不必要的数据复制。感谢 Ruben Perez (@anarthal) 实现了此功能。wait_for_one_error
而不是 wait_for_all
,提高了对断开连接的反应时间。函数 connection::async_run
也已更改为在从服务器接收到该错误时向用户返回 EOF。这是一个重大更改。disabled
更改为 debug
。config::username
的默认值设置为 "default"
。这使得在 Redis 中使用 requirepass
配置更加简单。std::size_t
而不是 std::uint64_t
来修复窄化转换。现在的代码依赖于 std::from_chars
,如果平台上 std::size_t
的大小为 32,则当接收到的值大于 32 时,返回错误。async_receive
重载。用户现在应该首先调用 set_receive_response
以避免不断地和不必要地设置相同的 response。std::function
来类型擦除 response 适配器。此更改不应以任何方式影响用户,但允许连接内部结构的重要简化。这导致了巨大的性能改进。get_usage()
,它返回连接使用信息,例如写入、接收的字节数等。asio::channel
进行通信,因此可以缓冲,从而避免阻塞套接字读取循环。批量读取也通过 channel.try_send
支持,缓冲的消息可以使用 connection::receive
同步消费。已添加函数 boost::redis::cancel_one
以简化处理同一 generic_response
中包含的多个服务器推送。重要提示:当 connection::async_receive
恢复时,这些更改可能会导致响应中出现多个推送。因此,用户在调用 resp.clear()
时必须小心:要么确保所有消息都已处理,要么只使用 consume_one
。boost::redis::config::database_index
,以便可以在启动运行命令之前选择数据库,例如在自动重新连接之后。boost::redis
中。to_bulk
和 from_bulk
名称对于 ADL 自定义点来说太通用了。它们获得了前缀 boost_redis_
。boost::redis::resp3::request
移动到 boost::redis::request
。boost::redis::response
,应该使用它来代替 std::tuple
。boost::redis::generic_response
,应该使用它来代替 std::vector<resp3::node<std::string>>
。redis::ignore
重命名为 redis::ignore_t
。async_exec
以接收 redis::response
而不是适配器,即,用户应该直接传递 resp
而不是传递 adapt(resp)
。boost::redis::adapter::result
以存储对命令的响应,包括可能的 resp3 错误,而不会丢失错误诊断部分。要访问值,现在使用 std::get<N>(resp).value()
而不是 std::get<N>(resp)
。request::coalesce
变得不必要并被删除。我可以使用这些更改测量到显著的性能提升。boost::redis::connection::async_run
将自动解析、连接、重新连接并执行健康检查。retry_on_connection_lost
重命名为 cancel_if_unresponded
。(v1.4.1)boost::string_view
、Boost.Variant2 和 Boost.Spirit 的依赖。HELLO
命令。如果不膨胀连接类,则无法正确实现此功能。现在,用户有责任发送 HELLO。包含它的请求优先于其他请求,并将移动到队列的前面,请参阅 aedis::request::config
aedis::connection::async_run
中删除。用户现在必须手动执行此步骤。此更改的原因是内置它们无法提供 boost 用户所需的足够灵活性。aedis::connection
现在使用 typedef 到 net::ip::tcp::socket
,aedis::ssl::connection
到 net::ssl::stream<net::ip::tcp::socket>
。需要使用其他流类型的用户现在必须专门化 aedis::basic_connection
。aedis::adapt
现在支持使用 std::tie
创建的元组。aedis::ignore
现在是 std::ignore
类型的别名。aedis::connection
类中使用的内部队列提供分配器支持。async_run
的行为,以便在收到 asio::error::eof 时完成并成功。这使得使用 awaitable 运算符编写组合操作更容易。aedis::request
中添加分配器支持 (来自 Klemens Morgenstern 的贡献)。aedis::request::push_range2
重命名为 push_range
。后缀 2 用于消除歧义。Klemens 使用 SFINAE 修复了它。fail_on_connection_lost
重命名为 aedis::request::config::cancel_on_connection_lost
。现在,只有当 async_run
完成时,它才会导致连接被取消。aedis::request::config::cancel_if_not_connected
,如果 async_exec
在连接建立之前被调用,这将导致请求被取消。aedis::request::config::retry
,如果设置为 true,则会导致请求在发送到 Redis 后,但在 async_run
完成后仍然未响应时,不会被取消。它提供了一种避免重复执行命令的方法。aedis::connection::async_run
重载。aedis::adapt()
与 std::vector<aedis::resp3::node<T>>
一起使用的行为。接收 RESP3 简单错误、blob 错误或 null 不会导致错误,但将被视为正常响应。用户有责任检查向量中的内容。connection::cancel(operation::exec)
中的错误。现在,此调用将仅取消未写入的请求。aedis::connection::async_exec
实现每个操作的隐式取消支持。以下调用将 co_await (conn.async_exec(...) || timer.async_wait(...))
取消请求,只要它尚未写入。aedis::connection::async_run
完成签名更改为 f(error_code)
。过去就是这样,第二个参数没有帮助。operation::receive_push
重命名为 aedis::operation::receive
。aedis::connection::config
中删除 coalesce_requests
,它现在成为请求属性,请参阅 aedis::request::config::coalesce
。aedis::connection::config
中删除 max_read_size
。最大读取大小现在可以指定为 aedis::adapt()
函数的参数。aedis::sync
类,有关如何执行同步和线程安全调用的信息,请参阅 intro_sync.cpp。这在 Boost. 1.80 中是可能的,因为它需要 boost::asio::deferred
。boost::optional
移动到 std::optional
。这是迁移到 C++17 的一部分。aedis::connection::async_run
重载的行为,以便在连接丢失时始终返回错误。aedis::connection::timeouts::resp3_handshake_timeout
。这是用于发送 HELLO
命令的超时。aedis::endpoint
,除了主机和端口之外,用户还可以选择性地提供用户名、密码和预期的服务器角色 (请参阅 aedis::error::unexpected_server_role
)。aedis::connection::async_run
检查 hello 命令中接收到的服务器角色是否等于 aedis::endpoint
中指定的预期服务器角色。要跳过此检查,请将 role 变量留空。aedis::connection
中删除重新连接功能。在简单的重新连接策略中这是可能的,但在更复杂的场景中会使类膨胀,例如,使用 sentinel、身份验证和 TLS。这在单独的协程中实现起来很简单。因此,enum event
和 async_receive_event
也已从类中删除。connection::async_receive_push
中的错误,该错误阻止传递除 adapt(std::vector<node>)
之外的任何响应适配器。aedis::adapt()
的行为,该行为导致 RESP3 错误被忽略。其结果之一是,在需要身份验证的服务器中,connection::async_run
不会失败退出。connection::async_run
的行为,该行为会导致在 connection::async_exec
中发生错误时完成并成功。aedis::sync
,它在线程安全和同步 API 中包装 aedis::connection
。sync.hpp
中的所有自由函数现在都是 aedis::sync
的成员函数。aedis::connection::async_receive_event
拆分为两个函数,一个用于接收事件,另一个用于服务器端推送,请参阅 aedis::connection::async_receive_push
。aedis::adapter::adapt
和 aedis::adapt
之间的冲突。connection::operation
枚举以替换 cancel_*
成员函数,使用单个 cancel 函数,该函数获取应取消的操作作为参数。connection
对象具有未发送命令的状态重新连接时出现的错误。在某些情况下,可能导致 async_exec
永远无法完成。adapt()
函数的文档。experimental::exec
和 receive_event
函数,以提供跨线程执行请求的线程安全和同步方式。有关示例,请参阅 intro_sync.cpp
和 subscriber_sync.cpp
。connection::async_read_push
已重命名为 async_receive_event
。connection::async_receive_event
现在用于向用户传达内部事件,例如 resolve、connect、push 等。有关示例,请参阅 cpp20_subscriber.cpp 和 connection::event
。aedis
目录已移动到 include
以使其看起来更像 Boost 库。用户现在应该在编译器标志中将 -I/aedis-path
替换为 -I/aedis-path/include
。AUTH
和 HELLO
命令现在自动发送。此更改对于实现重新连接是必要的。AUTH
中使用的用户名和密码应由用户在 connection::config
上提供。connection::enable_reconnect
。connection::async_run(host, port)
重载中的错误,该错误导致重新连接时崩溃。any_io_executor
。connection::async_run
退出时,connection::async_receiver_event
不再被取消。此更改使用户代码更简单。connection::async_exec
已被删除。请使用其他 connection::async_run
重载。connection::async_run
中的主机和端口参数已移动到 connection::config
,以更好地支持身份验证和故障转移。chat_room
示例中的许多简化。echo_server
示例中添加重新连接协程。(v0.1.2)make_parallel_group
修正 client::async_wait_for_data
以启动操作。(v0.1.2)