基本架构
Redis 是一个基于内存的键值数据库,主要结构如下:
- Server:整个 Redis 实例。
- Database:每个 Redis 实例包含多个数据库(默认16个)。
- Key-Value Store:每个数据库是一个键值对集合,键和值可以是多种数据类型。
1. 数据库结构
Redis 的数据库结构体 redisDb 定义如下:
typedef struct redisDb {
dict *dict; // 主字典,存储所有键值对
dict *expires; // 过期字典,存储键的过期时间
dict *blocking_keys; // 阻塞等待的键
dict *ready_keys; // 已准备好的键
dict *watched_keys; // 被 WATCH 命令监控的键
int id; // 数据库 ID
} redisDb;
- dict:主字典,用来存储所有的键值对。
- expires:过期字典,记录键的过期时间。
- blocking_keys、ready_keys 和 watched_keys:用于管理阻塞操作、通知和事务。
2. 全局数据库数组
Redis 服务器实例 (redisServer) 中包含了一个 redisDb 数组,用于存放所有的数据库:
struct redisServer {
redisDb *db; // 数据库数组
int dbnum; // 数据库数量,默认是 16
...
};
db:指向数据库数组的指针。dbnum:数据库的数量,默认是 16,可以通过配置文件修改。
3. 数据库切换
Redis 支持使用 SELECT 命令切换当前数据库:
void selectCommand(client *c) {
long id;
if (getLongFromObjectOrReply(c, c->argv[1], &id, NULL) != C_OK)
return;
if (id < 0 || id >= server.dbnum) {
addReplyError(c,"invalid DB index");
return;
}
c->db = &server.db[id];
addReply(c,shared.ok);
}
- 参数解析:首先从命令参数中解析出数据库 ID。
- 合法性检查:检查数据库 ID 是否在合法范围内(0 到
server.dbnum - 1)。 - 数据库切换:将客户端的
db指针指向指定的数据库。
4. 命令执行中的数据库访问
在具体的命令实现中,通过客户端上下文访问相应的数据库。例如,设置键值对的 SET 命令:
void setCommand(client *c) {
int flags = OBJ_SET_NO_FLAGS;
robj *key = c->argv[1];
robj *val = c->argv[2];
setKey(c,c->db,key,val);
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
addReply(c,shared.ok);
}
c->db:当前客户端所选择的数据库。setKey:在当前数据库里进行键值设置操作。
5. 数据存储与隔离
每个 redisDb 都有自己独立的字典 (dict),各数据库之间的数据互不影响,实现了逻辑上的隔离:
int dbAdd(redisDb *db, robj *key, robj *val) {
sds copy = sdsdup(key->ptr);
int retval = dictAdd(db->dict, copy, val);
return retval == DICT_OK ? C_OK : C_ERR;
}
- 独立字典:每个数据库有自己的
dict,管理自己的键值对。 - 独立操作:数据库操作都是针对当前数据库的字典进行,不会影响其他数据库。
6. 数据库数量的配置
在 Redis 的配置文件中,可以调整数据库的数量:
# 默认数据库数量为 16
databases 16
启动时,Redis 会根据配置文件初始化数据库数组:
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
for (int j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
server.db[j].blocking_keys = dictCreate(&dbBlockingDictType,NULL);
server.db[j].ready_keys = dictCreate(&dbReadyKeysDictType,NULL);
server.db[j].watched_keys = dictCreate(&dbWatchedKeysDictType,NULL);
server.db[j].id = j;
}
数据库切换与操作
当一个客户端连接到 Redis 服务器时,它会默认连接到数据库 0。客户端可以通过 SELECT 命令切换到其他数据库,这个过程非常轻量且高效。
切换数据库的实现:
void selectCommand(client *c) {
long id;
if (getLongFromObjectOrReply(c, c->argv[1], &id, NULL) != C_OK)
return;
if (id < 0 || id >= server.dbnum) {
addReplyError(c,"invalid DB index");
return;
}
c->db = &server.db[id];
addReply(c,shared.ok);
}
查找操作如何定位到当前数据库,找到对应数据库中的数据
在 Redis 中,查找操作包括多个步骤,其中关键的一步是定位到当前客户端选择的数据库,并在该数据库中执行查找操作。下面将详细解析这一过程的源码实现流程。
1. 客户端请求处理
Redis 接收到客户端的请求后,会首先进行命令解析。每个客户端都有一个上下文(client 结构体),其中包含了当前所选择的数据库指针。
2. 客户端结构体
client 结构体中包含了与客户端相关的所有信息,包括当前选择的数据库:
typedef struct client {
redisDb *db; // 指向当前数据库的指针
int argc; // 命令参数数量
robj **argv; // 命令参数数组
...
} client;
3. 命令分派与处理
当 Redis 接收到一个 GET 请求时,会调用对应的处理函数 getCommand。处理函数会使用客户端上下文中的数据库指针来执行操作。
getCommand 实现:
void getCommand(client *c) {
robj *o;
// 从当前数据库中查找键
o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,OBJ_STRING)) return;
addReplyBulk(c,o);
}
在这里,lookupKeyReadOrReply 函数用于在当前客户端选择的数据库中查找键。如果未找到键,则返回特定的回复信息(如空值)。
4. 查找键的核心函数
lookupKeyReadOrReply 实现:
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
robj *o = lookupKeyRead(c->db,key);
if (!o) addReply(c,reply);
return o;
}
robj *lookupKeyRead(redisDb *db, robj *key) {
return lookupKey(db,key,LOOKUP_NONE);
}
lookupKeyReadOrReply:尝试从当前数据库中读取键值,如果未找到则回复客户端特定信息。lookupKeyRead:直接调用lookupKey在指定数据库中查找键值。
5. 数据库结构体
每个数据库在 Redis 中都由 redisDb 结构体表示:
typedef struct redisDb {
dict *dict; // 主字典,存储所有键值对
dict *expires; // 过期字典,存储键的过期时间
...
int id; // 数据库 ID
} redisDb;
6. 键的查找
lookupKey 实现:
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict, key->ptr);
if (de) {
robj *val = dictGetVal(de);
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
updateLRU(val);
}
return val;
} else {
return NULL;
}
}
dictFind:在数据库的主字典中查找键。dictGetVal:获取字典条目中的值。
7. 字典查找机制
dictFind 实现:
dictEntry *dictFind(dict *d, const void *key) {
if (d->ht[0].used + d->ht[1].used == 0) return NULL;
unsigned int h = dictHashFunction(key);
dictEntry *he;
// 遍历第一个哈希表
he = d->ht[0].table[h & d->ht[0].sizemask];
while (he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
// 如果正在进行rehash,遍历第二个哈希表
if (d->ht[1].table) {
he = d->ht[1].table[h & d->ht[1].sizemask];
while (he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
}
return NULL;
}
- 哈希计算:使用哈希函数计算键的哈希值。
- 桶位置:根据哈希值找到对应的桶位置。
- 链表遍历:在相应的桶中遍历链表,查找匹配的键。
- rehash 处理:如果正在进行 rehash(重新构建哈希表),会同时检查两个哈希表。
8. 返回结果
一旦找到键,lookupKey 函数会返回找到的键值对象 (robj) 给上层调用者。对于 getCommand 来说,如果成功找到键,则将其值返回给客户端。
完整的 getCommand 逻辑:
void getCommand(client *c) {
robj *o;
// 从当前数据库中查找键
o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,OBJ_STRING)) return;
addReplyBulk(c,o);
}
- 查找键:通过
lookupKeyReadOrReply在当前数据库中查找键。 - 类型检查:确保键对应的值是字符串类型。
- 发送回复:将找到的值发送给客户端。
9. 数据库切换
在 Redis 中,每个客户端可以在连接过程中使用 SELECT 命令来切换数据库。例如:
void selectCommand(client *c) {
long id;
if (getLongFromObjectOrReply(c, c->argv[1], &id, NULL) != C_OK)
return;
if (id < 0 || id >= server.dbnum) {
addReplyError(c,"invalid DB index");
return;
}
c->db = &server.db[id];
addReply(c,shared.ok);
}
- 解析数据库 ID:从命令参数中获取数据库 ID。
- 合法性检查:检查数据库 ID 是否在合法范围内。
- 切换数据库:将客户端上下文中的数据库指针指向指定的数据库。
总结
Redis 查找操作的流程包括以下几个关键步骤:
- 客户端请求处理:接收并解析客户端请求。
- 命令分派与处理:根据请求调用相应的命令处理函数。
- 定位当前数据库:每个客户端都有一个指向当前数据库的指针(
client->db)。 - 在数据库中查找键:调用
lookupKey等函数,从数据库的主字典中查找键。 - 返回结果:找到键后,将其值返回给客户端。
思考题1: redis为什么设计16个数据库
Redis 默认配置了 16 个数据库,这一设计选择背后有多方面的考虑:
1. 简单有效的隔离机制
- 逻辑隔离:通过提供多个数据库,用户可以在一个 Redis 实例内实现简单的逻辑数据隔离。
- 轻量级:不同于 PostgreSQL 等数据库系统中的多数据库实例,Redis 的多数据库是非常轻量级的,它们共享同一个进程和资源,只是在逻辑上进行划分。
2. 避免复杂性
- 统一管理:多个数据库共享同一个 Redis 配置和持久化机制,这简化了系统管理和运维。
- 减少配置复杂度:如果每个用户或应用都需要启动一个独立的 Redis 实例,将会导致高昂的资源消耗和复杂的管理任务。16 个数据库的设置在一定程度上解决了这一问题,同时避免了过多配置带来的复杂性。
3. 适应多数使用场景
- 常见需求覆盖:对于大多数应用场景来说,16 个数据库已经足够用来实现数据隔离。例如,不同模块、功能或环境的数据存储。
- 合理限制:通过限制数据库数量,Redis 可以更好地控制资源使用,防止因滥用而造成的性能问题。
4. 操作简单
- 切换便捷:使用
SELECT命令即可轻松切换数据库,无需额外配置和操作。 - 命名空间:用户可以通过不同的数据库 ID 来实现简单的命名空间,避免键冲突。
5. 历史原因与向下兼容
- 早期设计:16 个数据库的设计源自早期版本,同时为了保持向下兼容性,这个默认值一直保留至今。
- 灵活调整:尽管默认是 16 个数据库,用户仍可以通过修改配置文件中的
databases参数来调整数据库数量,满足特定需求。
# redis.conf 配置文件示例
databases 16
案例分析
以下是一些具体案例,说明为什么 16 个数据库在很多情况下是恰当且实用的:
- 开发测试环境:一个团队可能有多个开发和测试环境,可以将不同环境的数据放在不同的数据库中。
- 模块分离:一个大型应用可以将不同模块的数据分散到不同数据库中,以减少代码层面的命名冲突。
- 多租户应用:一些简单的多租户应用可以利用 Redis 的多个数据库来实现不同租户间的数据隔离。
限制与注意事项
尽管 Redis 提供了多达 16 个数据库,但也存在一些限制和需要注意的地方:
- 非独立实例:不同数据库之间没有真正的物理隔离,共享同一个内存池和 CPU 资源,因此一个数据库的高负载可能影响其他数据库。
- 集群模式不支持多数据库:在 Redis Cluster 模式下,只能使用数据库 0。这是因为集群模式主要通过分片(sharding)来管理数据,支持多数据库会增加实现复杂度。