Redis属于哪种数据库
很多人认为Redis是内存中间件,对于这个观点,我其实不太认同。实际上,Redis是一种基于内存的高效数据存储系统,它不仅仅是中间件,而是一个具备丰富功能的内存数据库。从使用角度来看,Redis采用了内存作为主要存储介质,并通过定期的持久化机制(RDB和AOF)将数据持久化到磁盘,以保证数据的安全和高效恢复。因此,从功能和使用场景来看,Redis可以被视为内存数据库,它提供了对非关系型数据的快速操作,并在性能上表现突出。
接下来,我们将深入讨论Redis如何设计和实现其作为非关系型内存数据库的架构。
详解redis数据库的设计与实现
redis服务端对于数据库的管理
Redis在服务端通过抽象出redisServer对象来管理多个数据库。用户与Redis的交互操作需要先与服务器建立连接,Redis会为每个连接分配相应的数据库实例。Redis使用redisServer中的db指针来管理所有的数据库,在服务器初始化时,Redis会根据配置的参数(如dbnum,默认为16)为db指针分配一块连续的内存空间来存储数据库对象:
对应的初始化的代码我们可以在redis.c的initServer函数看到这段调用,用户在进行数据库切换的时候,可以通过select指令切换要操作的数据库:
void initServer(void) {
// 初始化服务器
// 基于dbnum分配一块连续的内存空间给db指针,每个DB都是一块连续内存空间
server.db = zmalloc(sizeof(redisDb) * server.dbnum);}
在数据库切换方面,Redis提供了select命令来允许用户在不同的数据库间切换。例如,select 0会选择db0,而select 1则切换到db1。通过这种方式,Redis实现了多个数据库实例的独立管理。
数据库的数据结构
Redis使用redisDb结构体封装每个数据库,其核心是一个字典(dict),用于存储非关系型的键值对。字典通过哈希算法进行管理,以实现接近O(1)的增删改查操作。值得注意的是,哈希表可能会遇到哈希冲突,这种情况下Redis采用拉链法来解决冲突。尽管查询时间复杂度可能在极端情况下达到O(n),但在大多数场景下,Redis能够保持O(1)级别的性能。
对应的我们给出redis.h下的redisDb 的数据结构,可以看到其最核心字段就是作为数据库的dict指针:
typedef struct redisDb {
// 作为内存数据库
dict *dict; /* The keyspace for this Database */
// 数据库 ID
int id; /* Database ID */
// 其他字段
// ......
} redisDb;
字典本质上使用了两个哈希表ht[2]进行存储,平时主要使用ht[0],而ht[1]在渐进式哈希(rehash)期间用于数据扩容和迁移。
ht:hast table 哈希表
上面的字典结构如下
dict *dict;
typedef struct dict {
// 哈希表数组,每个索引地址都存储的是哈希表
dictht ht[2];
// 其他字段
// ......
} dict;
客户端对于数据库的分配和使用
当客户端连接到Redis时,Redis会为每个客户端分配一个默认的数据库指针,指向db0(即第一个数据库)。这一操作在createClient函数中通过调用selectDb函数完成,将db0分配给客户端的db指针:
对应的代码段我们可以在初始化客户端的核心代码段createClient方法看到这一操作,其通过selectDb将db0指针交给c->db :
redisClient *createClient(int fd) {
redisClient *c = zmalloc(sizeof(redisClient));
// 初始化操作
selectDb(c, 0);
// 返回客户端对象
return c;
}
// 默认情况下, 将客户端的db指针指向db[0]
int selectDb(redisClient *c, int id) {
if (id < 0 || id >= server.dbnum)
return REDIS_ERR;
c->db = &server.db[id];
return REDIS_OK;
}
通过这种机制,每个客户端可以独立操作不同的数据库,并且Redis提供了灵活的数据库切换机制,以满足多样化的业务需求。
数据库高效的增删改查实现
由于Redis的数据库存储基于哈希表,因此其数据操作通常能够实现接近O(1)的时间复杂度。以下是Redis通过哈希表实现的查询、插入和删除操作的核心逻辑:
对应的给出redis哈希表查询、插入、删除的核心逻辑,都是哈希定位再操作:
// 查询
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
unsigned int h, idx, table;
// 计算哈希值
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
// 通过哈希值定位索引
idx = h & d->ht[table].sizemask;
// 到表中查询并比对,如果一致则返回
he = d->ht[table].table[idx];
while (he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
}
return NULL;
}
// 插入
dictEntry *dictAddRaw(dict *d, void *key)
{
// 计算索引
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
// 分配内存空间
entry = zmalloc(sizeof(*entry));
// 存到对应的索引位置下
entry->next = ht->table[index];
return entry;
}
// 删除
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
dictEntry *he, *prevHe = NULL;
unsigned int h, idx, table;
// 计算哈希值
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
// 计算索引
idx = h & d->ht[table].sizemask;
// 获取对应元素
he = d->ht[table].table[idx];
while (he) {
// 比对如果一致,则从链表中移除掉
if (dictCompareKeys(d, key, he->key)) {
// 如果有前驱节点,则让前驱挂当前节点的后继
if (prevHe)
prevHe->next = he->next;
else
// 反之直接让当前索引指向下一个元素
d->ht[table].table[idx] = he->next;
// 释放内存空间
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(he);
d->ht[table].used--;
return DICT_OK;
}
prevHe = he;
he = he->next;
}
}
return DICT_ERR; /* not found */
}
这些操作通过哈希定位和链表操作实现了高效的数据操作,从而保证了Redis在高并发场景下的优异性能。
总结
Redis作为一种高效的内存数据库,利用了哈希表的高效结构,并通过渐进式哈希、内存管理等机制保证了在大规模数据操作下的高性能。
参考文章
https://redis.io/docs/latest/develop/architecture/
https://redis.io/docs/latest/
https://redis.io/
加入我们的微信群,与我们一起探讨数据库技术,以及SQL Server、 MySQL、PostgreSQL、MongoDB 的相关话题。
微信群仅供学习交流使用,没有任何广告或商业活动。