redis-源码带读-05-集群事务与高级特性
Redis 源码带读 第5篇:集群、事务与高级特性
本篇目标
了解Redis的集群架构、事务机制和高级特性。
前置知识
- 阅读过第1-4篇
- 了解分布式系统基本概念
1. Redis Cluster(cluster.c)
1.1 哈希槽(Hash Slot)
Redis Cluster将所有数据划分为16384个哈希槽:
slot = CRC16(key) % 16384
每个节点负责一部分槽。客户端发送命令时,先计算key对应的槽,发给对应节点。
1.2 Cluster数据结构
// cluster.h
typedef struct clusterState {
clusterNode *myself; // 当前节点
clusterNode **slots; // 16384个槽的指针数组
int slots_count; // 槽数量
dict *nodes; // 节点字典(nodeID -> node)
clusterNode *nodes_buckets[CLUSTER_SLOTS/16]; // 槽的位图
...
} clusterState;
typedef struct clusterNode {
char name[CLUSTER_NAMELEN]; // 节点ID(40字节十六进制)
int flags; // 节点状态(master/slave/fail等)
uint64_t configEpoch; // 配置纪元(用于故障转移)
clusterNode *slaveof; // 主节点指针(从节点才有)
unsigned char slots[CLUSTER_SLOTS/8]; // 槽位图
int numslots; // 负责的槽数量
int numslaves; // 从节点数量
clusterNode **slaves; // 从节点数组
...
} clusterNode;
1.3 Gossip协议
节点之间通过Gossip协议交换状态信息:每个节点定期向随机节点发送PING,接收PONG,更新集群状态。
int clusterCommand(client *c) {
if (!strcasecmp(c->argv[1]->ptr, "meet")) {
// CLUSTER MEET <ip> <port>
// 添加新节点到集群
clusterProcessGossipSection(c, hdr);
} else if (!strcasecmp(c->argv[1]->ptr, "nodes")) {
// CLUSTER NODES
// 返回集群中所有节点信息
clusterGenNodesDescription(0);
} else if (!strcasecmp(c->argv[1]->ptr, "info")) {
// CLUSTER INFO
// 返回集群状态信息
clusterInfoCommand(c);
}
...
}
Gossip消息格式:
+-------+-------+-------+-------+-------+
| type | sender| state | slots | nodes |
+-------+-------+-------+-------+-------+
| 1字节 | 40字节| 1字节 | 2KB | 动态 |
+-------+-------+-------+-------+-------+
1.4 MOVED/ASK重定向
- MOVED:key所在的槽已迁移到其他节点,客户端应更新本地路由表
- ASK:槽正在迁移中,临时重定向
// 当key不在当前节点时
void getKeysFromCommand(client *c, ...);
int getKeysFromCommandWithFlags(client *c, ...);
// 检查key是否在当前节点
if (keyslot != myself->slots) {
// 发送MOVED重定向
addReplyError(c, "MOVED <slot> <ip>:<port>");
return;
}
// 槽正在迁移时发送ASK重定向
if (slot_state == CLUSTER_SLOT_IMPORTING) {
addReplyError(c, "ASK <slot> <ip>:<port>");
return;
}
1.5 故障转移
当主节点故障时,其从节点发起选举(Raft-like),获得多数投票后升级为主节点。
// 故障检测
int clusterNodeFailure(clusterNode *node) {
// 检查节点是否超时未响应
if (node->flags & CLUSTER_NODE_PFAIL) {
// 收集足够的PFAIL报告
if (clusterNodeFailureReportsCount(node) >= clusterGetN/AE_FAIL_HELD) {
node->flags &= ~CLUSTER_NODE_PFAIL;
node->flags |= CLUSTER_NODE_FAIL;
// 广播FAIL消息
clusterSendFail(node->name);
}
}
}
// 故障转移
int clusterFailoverReplaceYourMaster(void) {
// 1. 选择新的主节点
clusterNode *newmaster = clusterSelectSlaveOfMaster(myself->slaveof);
// 2. 将从节点升级为主节点
clusterBumpConfigEpoch(newmaster);
newmaster->flags &= ~CLUSTER_NODE_SLAVE;
newmaster->flags |= CLUSTER_NODE_MASTER;
newmaster->slaveof = NULL;
// 3. 更新槽的归属
for (int i = 0; i < CLUSTER_SLOTS; i++) {
if (clusterNodeGetSlot(newmaster, i)) {
server.cluster->slots[i] = newmaster;
}
}
// 4. 广播更新后的配置
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
return C_OK;
}
2. 事务(multi.c)
2.1 MULTI/EXEC
void multiCommand(client *c) {
// 检查是否已经在事务中
if (c->flags & CLIENT_MULTI) {
addReplyError(c, "MULTI calls can not be nested");
return;
}
// 开始事务
c->flags |= CLIENT_MULTI;
c->mstate.count = 0;
c->mstate.cmd_flags = 0;
addReply(c, shared.ok);
}
void execCommand(client *c) {
// 1. 检查是否在事务中
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c, "EXEC without MULTI");
return;
}
// 2. 执行所有入队命令
for (int j = 0; j < c->mstate.count; j++) {
client *cmd_client = c->mstate.commands[j];
// 执行命令
call(cmd_client, CMD_CALL_FULL);
}
// 3. 清理事务状态
c->flags &= ~CLIENT_MULTI;
c->mstate.count = 0;
addReply(c, shared.ok);
}
2.2 WATCH乐观锁
void watchCommand(client *c) {
for (int j = 1; j < c->argc; j++) {
robj *key = c->argv[j];
// 为每个被监控的key创建一个watchedKey
watchedKey *wk = zmalloc(sizeof(watchedKey));
wk->key = key;
wk->db = c->db;
// 将watchedKey添加到key的watched_keys字典
dictAdd(key->watched_keys, wk, NULL);
// 将key添加到客户端的watched_keys列表
listAddNodeTail(c->client->watched_keys, wk);
}
addReply(c, shared.ok);
}
void touchWatchedKey(redisDb *db, robj *key) {
// 当key被修改时,标记所有监控该key的客户端为CLIENT_DIRTY_CAS
list *clients = dictFetchValue(key->watched_keys, NULL);
if (clients) {
listIter li;
listNode *ln;
listRewind(clients, &li);
while ((ln = listNext(&li))) {
client *client = listNodeValue(ln);
client->flags |= CLIENT_DIRTY_CAS;
}
}
}
void execCommand(client *c) {
// 在执行前检查是否有watched key被修改
if (c->flags & CLIENT_DIRTY_CAS) {
// 有key被修改,事务失败
addReply(c, shared.null);
c->flags &= ~(CLIENT_MULTI | CLIENT_DIRTY_CAS);
return;
}
// 执行事务...
}
2.3 重要特性
Redis事务不支持回滚! 如果EXEC执行中某条命令失败,其他命令仍然执行。这是因为Redis追求简单和性能。
事务的限制:
- 不能在事务中使用SUBSCRIBE、WATCH等命令
- 不能根据某个key的值决定后续操作
- 没有真正的回滚机制
Lua脚本的优势:
- 可以在脚本中使用条件逻辑
- 可以读取中间结果,根据结果决定后续操作
- 更灵活,但脚本执行时间有限制(默认5秒)
3. EVAL Lua脚本(eval.c)
3.1 基本用法
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 key value
3.2 执行流程
void evalCommand(client *c) {
// 1. 计算脚本的SHA1哈希
char funcname[41];
sha1hex(funcname, c->argv[1]->ptr, sdslen(c->argv[1]->ptr));
// 2. 检查脚本缓存中是否存在
if (scriptExists(funcname)) {
// 脚本已缓存,直接执行
evalGenericCommand(c, 0);
} else {
// 脚本不存在,加载并缓存
evalLuaScript(c);
}
}
void evalGenericCommand(client *c, int evalsha) {
// 1. 创建Lua脚本执行上下文
luaScript *script = createScript(c->argv[1]->ptr, ...);
// 2. 设置KEYS和ARGV参数
for (int i = 0; i < numkeys; i++) {
lua_pushlstring(lua, keys[i]->ptr, sdslen(keys[i]->ptr));
lua_setglobal(lua, "KEYS");
}
// 3. 执行脚本
int err = lua_pcall(lua, 0, 0, 0);
// 4. 获取返回结果
getReplyFromInterpreter(lua, c);
}
3.3 原子性
脚本执行期间不处理其他命令(类似MULTI/EXEC),但比事务更灵活。
原子性的保证:
- 脚本执行期间,Redis单线程不处理其他命令
- 脚本中的所有命令在一个原子操作中执行
- 脚本可以读取中间结果,根据结果决定后续操作
3.4 SCRIPT FLUSH/CHECKSHA
SCRIPT FLUSH:清空脚本缓存SCRIPT EXISTS sha1:检查脚本是否存在SCRIPT KILL:终止正在执行的脚本
4. 发布订阅(pubsub.c)
4.1 基本命令
| 命令 | 说明 |
|---|---|
SUBSCRIBE channel |
订阅频道 |
UNSUBSCRIBE channel |
取消订阅 |
PUBLISH channel message |
发布消息 |
PSUBSCRIBE pattern |
模式订阅(通配符) |
4.2 实现原理
void subscribeCommand(client *c) {
for (int j = 1; j < c->argc; j++) {
robj *channel = c->argv[j];
// 1. 将频道添加到客户端的订阅列表
listAddNodeTail(c->pubsub_channels, channel);
// 2. 将客户端添加到频道的订阅者列表
dict *channels = server.pubsub_channels;
if (dictFind(channels, channel) == NULL) {
// 创建新的频道字典
list *clients = listCreate();
dictAdd(channels, channel, clients);
}
// 3. 将客户端添加到频道的订阅者列表
list *clients = dictFetchValue(channels, channel);
listAddNodeTail(clients, c);
}
addReply(c, shared.ok);
}
void publishCommand(client *c) {
robj *channel = c->argv[1];
robj *message = c->argv[2];
// 1. 查找频道的订阅者列表
dict *channels = server.pubsub_channels;
list *clients = dictFetchValue(channels, channel);
// 2. 向所有订阅者发送消息
if (clients) {
listIter li;
listNode *ln;
listRewind(clients, &li);
while ((ln = listNext(&li))) {
client *client = listNodeValue(ln);
// 发送消息
addReply(client, shared.message);
addReplyBulk(client, channel);
addReplyBulk(client, message);
}
}
// 3. 返回收到消息的订阅者数量
addReplyLongLong(c, listLength(clients));
}
4.3 Pub/Sub的限制
- 消息不持久化
- 不支持消息回放
- 消息可能丢失(如果订阅者断线)
- 需要此功能请使用Stream
5. Stream(t_stream.c)
5.1 概念
Redis Stream类似Kafka,提供:
- 有序消息流
- 消费者组(Consumer Group)
- 消息持久化
- ACK确认机制
5.2 Stream数据结构
typedef struct stream {
rax *rax; // 消息索引(rax基数树)
uint64_t length; // 消息总数
streamID last_id; // 最后一条消息的ID
streamID first_id; // 第一条消息的ID
streamID max_deleted_entry_id; // 最大已删除消息ID
rax *cgroups; // 消费者组字典
} stream;
typedef struct streamCG {
sds name; // 消费者组名称
streamID last_id; // 最后投递的消息ID
streamID pel; // 待确认消息列表(rax)
rax *consumers; // 消费者字典
} streamCG;
typedef struct streamConsumer {
sds name; // 消费者名称
uint64_t seen_time; // 最后活跃时间
streamCG *group; // 所属消费者组
rax *pel; // 该消费者待确认的消息
} streamConsumer;
5.3 Stream的底层实现
- rax(Radix Tree):压缩前缀树,按消息ID索引
- 每个rax节点存储一个listpack(紧凑列表)
- 消息ID格式:
<毫秒时间戳>-<序列号>
5.4 消费者组
XGROUP CREATE mystream mygroup $ -> 创建消费者组
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
-> 读取新消息
XACK mystream mygroup 1526569495631-0
-> 确认消息已处理
消费者组的实现:
void xreadgroupCommand(client *c) {
// 1. 查找或创建消费者组
streamCG *cg = streamLookupCG(stream, groupname);
// 2. 查找或创建消费者
streamConsumer *consumer = streamLookupConsumer(cg, consumername, 1);
// 3. 读取新消息
if (strcmp(c->argv[4]->ptr, ">") == 0) {
// 读取未投递的新消息
streamIteratorInit(stream, &si, &cg->last_id, 1);
} else {
// 读取待确认的消息
streamIteratorInit(stream, &si, &startid, 0);
}
// 4. 将消息添加到PEL(待确认列表)
streamRaxInsert(&cg->pel, entry_id, ...);
streamRaxInsert(&consumer->pel, entry_id, ...);
// 5. 更新消费者组的last_id
cg->last_id = entry_id;
}
void xackCommand(client *c) {
// 1. 从PEL中移除已确认的消息
streamRaxRemove(&cg->pel, entry_id, NULL);
streamRaxRemove(&consumer->pel, entry_id, NULL);
// 2. 返回确认的消息数量
addReplyLongLong(c, acknowledged);
}
6. 阻塞命令(blocked.c)
6.1 BLPOP/BRPOP
BLPOP key1 key2 timeout
-> 如果所有key都为空:阻塞客户端
-> 当某个key有新元素入队时:唤醒客户端并返回
-> 超时后返回nil
6.2 实现机制
void blpopCommand(client *c) {
// 1. 尝试从所有key中弹出元素
for (int j = 1; j < c->argc - 1; j++) {
robj *val = listTypePop(c->db->dict, c->argv[j]->ptr);
if (val) {
// 找到非空key,直接返回
addReply(c, shared.ok);
return;
}
}
// 2. 所有key都为空,阻塞客户端
c->bpop.timeout = timeout;
c->bpop.type = BLOCKED_LIST;
c->bpop.keys = keys;
// 将客户端从正常客户端列表中移除
listDelNode(server.clients, listSearchKey(server.clients, c));
// 添加到key的blocking_keys字表中
for (int j = 1; j < c->argc - 1; j++) {
dict *blocking_keys = c->db->blocking_keys;
list *clients = dictFetchValue(blocking_keys, c->argv[j]);
listAddNodeTail(clients, c);
}
// 设置超时事件
aeCreateTimeEvent(server.el, timeout, unblockClientOnTimeout, c);
}
void unblockClient(client *c) {
// 1. 从blocking_keys中移除
for (int j = 1; j < c->argc - 1; j++) {
dict *blocking_keys = c->db->blocking_keys;
list *clients = dictFetchValue(blocking_keys, c->argv[j]);
listDelNode(clients, listSearchKey(clients, c));
}
// 2. 添加回正常客户端列表
listAddNodeTail(server.clients, c);
// 3. 清除阻塞状态
c->flags &= ~CLIENT_BLOCKED;
c->bpop.type = BLOCKED_NONE;
}
6.3 阻塞命令列表
| 命令 | 说明 |
|---|---|
BLPOP/BRPOP |
列表阻塞弹出 |
BZPOP/BZPOPMIN |
有序集合阻塞弹出 |
XREAD BLOCK |
Stream阻塞读取 |
WAIT |
等待复制完成 |
6.4 阻塞命令的唤醒机制
void signalBlockedClientsAwoken(redisDb *db, robj *key) {
// 查找在该key上阻塞的客户端
dict *blocking_keys = db->blocking_keys;
list *clients = dictFetchValue(blocking_keys, key);
if (clients) {
listIter li;
listNode *ln;
listRewind(clients, &li);
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 检查是否有数据可弹出
if (c->bpop.type == BLOCKED_LIST) {
robj *val = listTypePop(db->dict, key);
if (val) {
// 有数据,唤醒客户端
addReply(c, shared.ok);
addReplyBulk(c, key);
addReplyBulk(c, val);
unblockClient(c);
}
}
}
}
}
7. 本篇小结
| 概念 | 要点 |
|---|---|
| Cluster | 16384哈希槽 + Gossip + MOVED重定向 |
| 事务 | MULTI/EXEC + WATCH,不支持回滚 |
| Lua脚本 | EVAL原子执行,比事务更灵活 |
| Pub/Sub | 发布订阅,不持久化 |
| Stream | Kafka-like消息流,消费者组 |
| 阻塞命令 | 客户端挂起直到有数据或超时 |
思考题
- 为什么Redis Cluster只用16384个槽而不是65536?
- Redis事务和数据库事务的本质区别是什么?
- EVAL脚本和MULTI/EXEC在原子性上有什么异同?
- Stream和Pub/Sub各自适合什么场景?
思考题解答
1. 为什么Redis Cluster只支持16384个slot而不是65536?
原因:
-
心跳包大小:每个节点需要向其他节点发送心跳包,包含自己的slot信息。16384个slot只需要2KB(16384/8)的位图,而65536个slot需要8KB。心跳包更小,网络开销更低。
-
性能考虑:slot数量越多,查找和路由的开销越大。16384个slot对于大多数场景足够,且性能更好。
-
历史原因:16384是2^14,是一个合理的选择,既能支持足够多的节点,又能保持较小的位图。
-
节点数量限制:理论上最多支持16384个主节点,但实际中很少有超过1000个节点的集群。
2. Redis事务对数据库的修改顺序有什么特点?
特点:
-
串行执行:MULTI/EXEC之间的所有命令在一个事务中原子执行,不会被其他客户端打断。
-
命令排队:所有命令先入队,然后一次性执行。执行过程中不会被其他命令插入。
-
无回滚:如果某个命令失败,其他命令仍然会执行。Redis不支持事务回滚。
-
命令有限制:不能在事务中使用SUBSCRIBE、WATCH等命令。
-
Lua脚本:EVAL脚本中的命令也是串行执行,但可以在脚本中使用事务命令。
3. EVAL脚本和MULTI/EXEC的原子性有什么区别?
MULTI/EXEC:
- 命令排队后一次性执行
- 不会读取中间结果
- 不能使用条件逻辑(如根据某个key的值决定后续操作)
EVAL脚本:
- 命令在Lua脚本中执行,可以使用条件逻辑
- 可以读取中间结果,根据结果决定后续操作
- 更灵活,但脚本执行时间有限制(默认5秒)
关键区别:
- MULTI/EXEC:命令串行执行,但不能根据中间结果做决策
- EVAL:可以读取中间结果,支持复杂逻辑
- 两者都是原子性的,但EVAL更灵活
4. Stream和Pub/Sub分别适合什么场景?
Pub/Sub适合:
- 实时通知:如在线状态变更、新消息通知
- 广播消息:一条消息需要发送给多个订阅者
- 低延迟要求:消息立即发送,不保存历史
Stream适合:
- 消息队列:需要持久化和回放的消息
- 消费者组:多个消费者共同消费一组消息
- 消息确认:需要确认消息被正确处理
- 消息历史:需要保留消息历史,支持回溯
对比:
| 特性 | Pub/Sub | Stream |
|---|---|---|
| 持久化 | 不持久化 | 持久化 |
| 消费者组 | 不支持 | 支持 |
| 消息确认 | 不支持 | 支持 |
| 消息回放 | 不支持 | 支持 |
| 延迟 | 极低 | 较低 |
| 内存占用 | 低 | 较高 |
建议:如果需要消息持久化和消费者组,使用Stream;如果只需要实时广播,使用Pub/Sub。

浙公网安备 33010602011771号