前言
Redis使用的是单reactor网络模型,也就是io多路复用+非阻塞io的异步处理流程(注册事件,在事件循环callback处理事件)。我们可以将每个连接抽象看成一个pipe,哪个pipe中的数据先满就先处理。注意,单reactor指的是acceptor只有一个,而工作线程在6.0版本之前只有一个,也就是单reactor单线程,6.0版本之后就是单reactor多线程。下图为6.0版本之后的reactor模型。但之后的讨论我们基于6.0版本之前,也就是单reactor单线程。
pipeline
Redis的Pipeline是一种客户端的批处理技术,它允许客户端一次性发送多个命令给Redis服务器,然后服务器端再依次执行这些命令,并返回结果。这种技术可以显著减少网络延迟,提高操作效率。
使用Pipeline之前,每次执行命令都需要客户端与服务器之间进行一次完整的网络往返(Round Trip Time,RTT),这在执行大量命令时会导致显著的性能问题。Pipeline通过将多个命令打包在一起发送,然后服务器端集中处理后返回结果,从而减少了网络往返的次数,提高了执行效率。Pipeline的工作流程大致如下:
- 客户端创建一个Pipeline对象,并向其中添加需要执行的命令。
- 客户端将所有命令一次性发送到Redis服务器。
- Redis服务器接收到命令后,依次执行这些命令,并将每个命令的结果存储起来。
- 客户端等待所有命令执行完成后,从服务器获取结果并按照命令发送的顺序进行处理。
需要注意的是,虽然Pipeline可以提高性能,但它也有一些限制和注意事项:
- Pipeline中的命令不具备原子性,即如果其中一个命令失败,不会影响到其他命令的执行。
- 使用Pipeline时,应该避免一次性发送过多命令,以免造成服务器处理阻塞,影响其他客户端的请求。
- 在Redis集群环境中使用Pipeline时,需要确保所有命令都在同一个节点上执行,避免跨节点操作导致的性能问题。
cpp
#include <cpp_redis/cpp_redis>
#include <iostream>
int main() {
// 连接到 Redis 服务器
cpp_redis::client client;
// 尝试连接到 Redis
client.connect("127.0.0.1", 6379, [](const std::string& host, std::size_t port, cpp_redis::connect_state status) {
if (status == cpp_redis::connect_state::ok) {
std::cout << "Successfully connected to redis instance at " << host << ":" << port << std::endl;
} else {
std::cerr << "Failed to connect to redis at " << host << ":" << port << std::endl;
}
});
// 等待连接确认
std::this_thread::sleep_for(std::chrono::seconds(1));
// 开启 Pipeline
auto sync_commit = client.sync_commit();
// 获取当前键的值
sync_commit.get("zg", [](const cpp_redis::reply& reply) {
if (reply.is_error()) {
std::cerr << "Error: " << reply.error() << std::endl;
} else {
// 将值翻倍
std::string value = std::to_string(std::stoll(reply.as_string()) * 2);
// 设置新的值
cpp_redis::client::commit("zg", value);
}
});
// 执行 Pipeline 并等待回复
std::vector<cpp_redis::reply> replies = sync_commit.exec();
// 检查回复并输出结果
for (const auto& reply : replies) {
if (reply.is_error()) {
std::cerr << "Error: " << reply.error() << std::endl;
} else {
std::cout << "Reply: " << reply.as_string() << std::endl;
}
}
return 0;
}
Redis事务
事务概念可以参考我写的MySQL事务原理,从Redis的角度简单来说就是用户定义的一系列操作被视为一个完整的逻辑处理工作单元,这些单元要么全部执行,要么全部不执行。Redis是非关系型数据库,它是不使用SQL语句的,所以那些数据库操作就是命令。 在多线程操作的情况下就需要考虑事务,因为单线程的命令本身就是一条条执行没有干扰的,多线程会有交叉操作,这种情况可以参考我写的原子操作与锁。所以事务就是用来解决并发场景下的冲突。
涉及到事务我们就要了解ACID,同样可以参考MySQL事务原理。ACID分别指的是原子性、一致性、隔离性、持久性。MySQL中有完整性约束保持数据类型的一致性,而Redis作为一个基于内存的非关系型数据库,它提供的是最终一致性模型。在 Redis 事务中,所有命令都会被序列化并按顺序执行,但与 MySQL 不同的是,如果事务中的某个命令执行失败,Redis 的事务并不会回滚,而是继续执行后续命令。Redis 事务不是完全符合 ACID 属性的。它只保证原子性和隔离性,但它们不保证持久性(Durability),因为 Redis 的持久化机制(如 RDB 或 AOF)与事务机制是分开的。而且,由于 Redis 是单线程的,它不需要处理多个并发事务之间的复杂交互,这简化了它的事务模型。
Lua
WATCH--监视一个或多个键,如果在执行 EXEC 之前,这些键被其他命令修改,则事务会被丢弃。
MULTI--开始一个新的事务,将后续的命令放入事务队列中。
--命令
EXEC--执行事务队列中的所有命令。如果事务成功,它会返回一个数组,其中包含每个命令的回复。如果事务因为之前的错误而被丢弃,它会返回 null。
DISCARD--取消事务,清空事务队列,并退出事务状态。
cpp
WATCH key
MULTI
INCR counter
SET key "value"
HSET hashfield field "value"
DISCARD # 假设在这个时候,我们决定取消事务
其实在实际上我们使用并非使用这些命令,而是使用lua脚本,因为乐观锁watch失败需要重试,操作起来复杂度就高。Redis中嵌入了一个lua虚拟机,所以可以通过lua操作redis的数据,lua脚本执行命令是一个语句(EVAL script numkeys key [key ... ] arg [arg ... ])作为一个数据包执行,所以其他语句根本插不进去,这样就天生具备原子性 。我们通常先使用script load将lua脚本发送至Redis,Redis会将lua脚本使用hash算法生成40bit字符串sha1,而后我们使用evalsha sha1 来执行脚本,通过hash简化命令,使数据包更小,减少带宽压力。