除了多线程,KeyDB在Redis的基础上发展除了一些其他特性
Active Replication
Redis集群模式下做横向扩展时,增加从节点来分摊读压力,从节点一般是只读的,可以配置从节点可写,但会造成主从数据不一致。KeyDB支持从节点可以,并且把从节点写入的操作同步给主节点,从节点和主节点是双向同步的。
双向同步需要解决循环同步的问题,即:主收到客户端写请求,把写请求同步给从,从又再次把该写请求同步给主,这时候主是不能执行这个写请求的,为了解决这个问题,KeyDB为每个server在启动时生成一个唯一的64位uuid,在建立同步连接时,会告知对方自己的uuid,同时会告知对面是否具备active replica能力。
当server收到客户端请求时,会将命令改写,加上命令的uuid、dictid、mvcctamp:
void replicationFeedSlavesCore(list *slaves, int dictid, robj **argv, int argc) {
...
char szDbNum[128];
int cchDbNum = 0;
if (!fSendRaw)
cchDbNum = writeProtoNum(szDbNum, sizeof(szDbNum), dictid);
char szMvcc[128];
int cchMvcc = 0;
incrementMvccTstamp(); // Always increment MVCC tstamp so we're consistent with active and normal replication
if (!fSendRaw)
cchMvcc = writeProtoNum(szMvcc, sizeof(szMvcc), getMvccTstamp());
//size_t cchlen = multilen+3;
struct redisCommand *cmd = lookupCommand(szFromObj(argv[0]));
sds buf = catCommandForAofAndActiveReplication(sdsempty(), cmd, argv, argc);
size_t cchlen = sdslen(buf);
// The code below used to be: snprintf(proto, sizeof(proto), "*5\r\n$7\r\nRREPLAY\r\n$%d\r\n%s\r\n$%lld\r\n", (int)strlen(uuid), uuid, cchbuf);
// but that was much too slow
static const char *protoRREPLAY = "*5\r\n$7\r\nRREPLAY\r\n$36\r\n00000000-0000-0000-0000-000000000000\r\n$";
char proto[1024];
int cchProto = 0;
if (!fSendRaw)
{
char uuid[37];
uuid_unparse(cserver.uuid, uuid);
cchProto = strlen(protoRREPLAY);
memcpy(proto, protoRREPLAY, strlen(protoRREPLAY));
memcpy(proto + 22, uuid, 36); // Note UUID_STR_LEN includes the \0 trailing byte which we don't want
cchProto += ll2string(proto + cchProto, sizeof(proto)-cchProto, cchlen);
memcpy(proto + cchProto, "\r\n", 3);
cchProto += 2;
}
// 复制流中加入uuid
feedReplicationBacklog(proto, cchProto);
// 复制流中加入命令
feedReplicationBacklog(buf, sdslen(buf));
const char *crlf = "\r\n";
// 复制流中加入dbid、mvcctamp
feedReplicationBacklog(crlf, 2);
feedReplicationBacklog(szDbNum, cchDbNum);
feedReplicationBacklog(szMvcc, cchMvcc);
sdsfree(buf);
}
从收到master的复制流之后,判断是否是master,如果是master,则不会继续发送给master:
void replicationFeedSlavesFromMasterStream(){
...
while((ln = listNext(&li))) {
client *slave = (client*)ln->value;
std::lock_guard<decltype(slave->lock)> ulock(slave->lock);
if (FSameHost(slave, server.master))
continue; // Active Active case, don't feed back
}
...
}
多主
Redis中从节点只能有一个主节点,KeyDB将可以有多个主节点,这样,多个主节点的数据就可以同步给一个从节点。
这一特性一般与active replica一起配合使用,可以灵活的构建出多种部署方式。比如:
1、异地多活,多个地域都向一个中心地域同步数据,中心地域的数据用来做最终的备份。
2、避免客户端的跨地域延迟,一个地域的客户端可以就近读到另一个地域的数据。
但是这两个特性在集群模式下会有数据一致性问题,所以这两个特性不能在集群模式下使用,这就限制了集群规模。
集群模式
KeyDB的集群模式与Redis基本相同,在KeyDB的官方文档(https://docs.keydb.dev/docs/cluster-spec)中详细介绍了集群模式的设计思路和注意事项,这也是之前在阅读Redis代码时没有注意到的,下面列几点:
- 为了使得key落在同一个分片中,可以使用hash tag
- 异步主从复制以及集群模式下的failover,使得Redis是有可能丢数据的,以下集群模式下丢数据的两种场景:
a、master收到并提交客户端请求后,客户端收到数据更新成功,但是master在向slave发送复制流时宕机,并且在master重启之前,已经failover成功(基本不会发生,因为master和slave一般同机房,网络传输一般比master与客户端快)
b、failover成功之后,旧的master重启,还没有转换为slave之前,客户端的路由还是旧的,就往旧的master上写数据,数据也会丢失(基本不会发生,master重启之后,在与集群中的大多数节点取得通信之前,会拒写,但是如果客户端与少部分节点一个网络隔离,就极有可能丢失数据) - 从节点迁移算法,该算法保证了集群的主节点都尽可能的只有有一个从节点,防止单节点失效问题,算法如下:如果一个master没有从节点,那么就从集群中所有master节点中选择从节点最多的节点,从中选择一个从节点,挂到之前的master节点上。
- configEpoch冲突解决,在failover成功或者slot迁移之后,节点都会自增自己的configEpoch,来保证在此时,自己的configEpoch是集群中最大的,但是在进行这两个操作的时候,有可能出现configEpoch冲突,这样当冲突的节点同时宣布负责某个slot时,其他节点就不知道如何处理了,所以遇到configEpoch冲突时,节点自身再次自增configEpoch
ps:最后吐槽下:可能是由于多人开发,并且c和c++混合,KeyDB的代码可读性真的很差,代码风格不统一,新feature的注释很少,大量的全局变量。。。