看了一些关于redis 的相关文章,例如redis为什么这么快、redis的原理、redis的数据结构这些,但其只是从整体的结构来说明,并没有梳理源码的具体流程,但我不是很喜欢一些黑盒的东西,所以我们这一篇就通过跑redis的源码,来追踪redis源码中的一些数据结构。这篇文章的源码是基于redis 3.0版本,同时源码是直接从github上面clone下来的别人已经处理好的redis代码(windows平台),地址为:https://github.com/htw0056/redis-3.0-annotated-cmake-in-clion。感谢这位前辈。
首先我们启动服务端,然后再启动客户端,通过客户端输入命令我们来跟踪其的主要结构,下面就正式开始。
我们通过客户端来设置值,然后来追踪其的执行。
一、redisClient关联内容
其首先是从redis.c
的main还是开始的,然后通过在main
函数中调用aeMain(aeEventLoop *eventLoop)
来处理事件(redis使用的是IO多路复用epoll
)。当然这里以及之后会进行各种内存、数据结构的初始化等。我们就不具体分析这种了(对于c语言我也只是能看懂大概的内容,具体细节也不是很明白)。但我们通过源码debug还是能明白大体的结构的。
1、整体执行流程
/*
* 事件处理器的主循环
*/
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
下面我们就直接到processCommand
方法,来看其对于输入命令的具体执行。
int processCommand(redisClient *c) {
.............
// 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作
if (server.maxmemory) {
// 如果内存已超过限制,那么尝试通过删除过期键来释放内存
int retval = freeMemoryIfNeeded();
// 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
// 并且前面的内存释放失败的话
// 那么向客户端返回内存错误
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
flagTransaction(c);
addReply(c, shared.oomerr);
return REDIS_OK;
}
}
........
/* Don't accept write commands if this is a read only slave. But
* accept write commands if this is our master. */
// 如果这个服务器是一个只读 slave 的话,那么拒绝执行写命令
if (server.masterhost && server.repl_slave_ro &&
!(c->flags & REDIS_MASTER) &&
c->cmd->flags & REDIS_CMD_WRITE)
{
addReply(c, shared.roslaveerr);
return REDIS_OK;
}
............
// 执行命令
call(c,REDIS_CALL_FULL);
..........
return REDIS_OK;
}
这里我们省略了很多内容,值保留了部分内容。
2、结构体内容
1)、redisClient结构体
首先我们来看下redisClient
结构体,这个就是redis的客户端链接命令处理:
typedef struct redisClient {
// 套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
.........
} redisClient;
这里是直接有redisDB
,然后dictid
是表示当前使用的是哪个redisDB
,例如当前就是使用的0
号库。然后在redisDb
中,就有真正存放添加的数据了。
2)、redisDb结构体
typedef struct redisDb {
// 数据库键空间,保存着数据库中的所有键值对
dict *dict; /* The keyspace for this DB */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires; /* Timeout of keys with a timeout set */
......
// 数据库号码
int id; /* Database ID */
// 数据库的键的平均 TTL ,统计信息
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
这个就是redis数据库
的结构体了,类似于mysql的数据库的概念,不过其的数据库名称就是id
标识,表示是第几号库。
3)、dict结构体
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
这个就是字典表。然后数据就是存在两张hash表中ht[2]
中**(redis不管要存入的是哪种类型例如string
、list
、hash
这些,都是放在这个hash表中**、(然后这里就是文章上说的,redis为什么这么快的第二个答案了(因为使用hash结构,其的查找会很快,第一个快的原因我认为是使用的内存),同时要注意这些类型存在redis内部又是其他具体设置的结构体、例如LINKEDLIST
常规链表、ZIPLIST
压缩列表、SKIPLIST
跳表),这个我们后面再来看。
然后这里dictht ht[2]
之所以是两张表,是用来扩容使用的,在渐进式hash扩容期间其是会使用两张表的,然后在ht[0]
中获取不到,就会去ht[1]
中找**(关于渐进式hash我们后面也会说明)、(这个也可以是速度快的第三个原因)**。然后这里的rehashidx
就用来标识当前是不是在渐进式hash扩容期间。
4)、dictht &dictEntry 结构体
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
这个就是hash表数组(客户端的设置获取命令最终都要落到table
来。),然后used
表示当前包含的节点,而dictEntry
就是对应的实体名称。
可以看到目前我们的key是有30个。
5)、整体数据介绍
然后我们再来看下redisClient
关联的db数据:
可以看到其当前使用的个数是30
个,同时其也要扩容了。但现在还没有扩容rehashidx
是-1
。
3、processCommand执行步骤
1)、查询检查命令
这里就是检查查看有没有我们当前输入的命令,如果没有就给出提示返回unknown command
。
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (!c->cmd) {
// 没找到指定的命令
flagTransaction(c);
addReplyErrorFormat(c,"unknown command '%s'",
(char*)c->argv[0]->ptr);
return REDIS_OK;
}
struct redisCommand *lookupCommand(sds name) {
return dictFetchValue(server.commands, name);
}
void *dictFetchValue(dict *d, const void *key) {
dictEntry *he;
// T = O(1)
he = dictFind(d,key);
return he ? dictGetVal(he) : NULL;
}
我们目前是set
命令。
2)、server.commands填充逻辑
首这个字典的参数化就是在populateCommandTable
方法遍历添加的。
void populateCommandTable(void) {
int j;
// 命令的数量
int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);
for (j = 0; j < numcommands; j++) {
// 指定命令
struct redisCommand *c = redisCommandTable+j;
// 取出字符串 FLAG
char *f = c->sflags;
...........
// 将命令关联到命令表
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
.........
}
}
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
{"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
{"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
{"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
{"strlen",strlenCommand,2,"r",0,NULL,1,1,1,0,0},
{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
{"exists",existsCommand,2,"r",0,NULL,1,1,1,0,0},
{"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
{"getbit",getbitCommand,3,"r",0,NULL,1,1,1,0,0},
{"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
{"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
{"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
{"incr",incrCommand,2,"wm",0,NULL,1,1,1,0,0},
{"decr",decrCommand,2,"wm",0,NULL,1,1,1,0,0},
{"mget",mgetCommand,-2,"r",0,NULL,1,-1,1,0,0},
{"rpush",rpushCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"lpush",lpushCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"rpushx",rpushxCommand,3,"wm",0,NULL,1,1,1,0,0},
{"lpushx",lpushxCommand,3,"wm",0,NULL,1,1,1,0,0},
............
{"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0}
};
这里面就是所有的redis命令。
3)、字典添加的逻辑(dictAddRaw)
int dictAdd(dict *d, void *key, void *val)
{
// 尝试添加键到字典,并返回包含了这个键的新哈希节点
// T = O(N)
dictEntry *entry = dictAddRaw(d,key);
// 键已存在,添加失败
if (!entry) return DICT_ERR;
// 键不存在,设置节点的值
// T = O(1)
dictSetVal(d, entry, val);
// 添加成功
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
// 如果条件允许的话,进行单步 rehash
// T = O(1)
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
// 计算键在哈希表中的索引值
// 如果值为 -1 ,那么表示键已经存在
// T = O(N)
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;
// T = O(1)
/* Allocate the memory and store the new entry */
// 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
// 否则,将新键添加到 0 号哈希表
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
// 为新节点分配空间
entry = zmalloc(sizeof(*entry));
// 将新节点插入到链表表头
entry->next = ht->table[index];
ht->table[index] = entry;
// 更新哈希表已使用节点数量
ht->used++;
/* Set the hash entry fields. */
// 设置新节点的键
// T = O(1)
dictSetKey(d, entry, key);
return entry;
}
这里具体有3步:首先看需不需要单步渐进hash赋值,如果需要(扩容的时候才需要),就渐进hash(其的步长是1(也就是一次只一定hash表的一个位置的节点链表到另一张表)),然后创建分配dictEntry
,再计算其的hash表index
使用头插法将其添加hash表中。这个是命令的字典表。当我们的设置字典表也是类似这个逻辑。
4)、内存清除
/* Handle the maxmemory directive.
*
* First we try to free some memory if possible (if there are volatile
* keys in the dataset). If there are not the only thing we can do
* is returning an error. */
// 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作
if (server.maxmemory) {
// 如果内存已超过限制,那么尝试通过删除过期键来释放内存
int retval = freeMemoryIfNeeded();
// 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
// 并且前面的内存释放失败的话
// 那么向客户端返回内存错误
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
flagTransaction(c);
addReply(c, shared.oomerr);
return REDIS_OK;
}
}
在正式执行命令设置内容之前,我们需要检查是不是已经达到最大内存了,如果到了,我们就需要根据内存淘汰策略其起来一些值了:
int freeMemoryIfNeeded(void) {
size_t mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves);
............
// 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回
if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
return REDIS_ERR; /* We need to free memory, but policy forbids. */
/* Compute how much memory we need to free. */
// 计算需要释放多少字节的内存
mem_tofree = mem_used - server.maxmemory;
// 初始化已释放内存的字节数为 0
mem_freed = 0;
// 根据 maxmemory 策略,
// 遍历字典,释放内存并记录被释放内存的字节数
while (mem_freed < mem_tofree) {
int j, k, keys_freed = 0;
// 遍历所有字典
for (j = 0; j < server.dbnum; j++) {
long bestval = 0; /* just to prevent warning */
sds bestkey = NULL;
dictEntry *de;
redisDb *db = server.db+j;
dict *dict;
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{
// 如果策略是 allkeys-lru 或者 allkeys-random
// 那么淘汰的目标为所有数据库键
dict = server.db[j].dict;
} else {
// 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl
// 那么淘汰的目标为带过期时间的数据库键
dict = server.db[j].expires;
}
// 跳过空字典
if (dictSize(dict) == 0) continue;
/* volatile-random and allkeys-random policy */
// 如果使用的是随机策略,那么从目标字典中随机选出键
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
{
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
}
/* volatile-lru and allkeys-lru policy */
// 如果使用的是 LRU 策略,
// 那么从一集 sample 键中选出 IDLE 时间最长的那个键
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
............
}
/* volatile-ttl */
// 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键
else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
for (k = 0; k < server.maxmemory_samples; k++) {
.........
}
}
/* Finally remove the selected key. */
// 删除被选中的键
if (bestkey) {
......
dbDelete(db,keyobj);
.......
}
}
if (!keys_freed) return REDIS_ERR; /* nothing to free... */
}
return REDIS_OK;
}
这个首先是遍历所有的db
,然后根据淘汰策略,如果是以所有key
(ALLKEYS
)为目标其是直接server.db[j].dict
,如过不是(就是过期key)server.db[j].expires
,然后再是具体的策略处理了:
/* Redis maxmemory strategies */
#define REDIS_MAXMEMORY_VOLATILE_LRU 0
#define REDIS_MAXMEMORY_VOLATILE_TTL 1
#define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
#define REDIS_MAXMEMORY_ALLKEYS_LRU 3
#define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
#define REDIS_MAXMEMORY_NO_EVICTION 5
#define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION
看以看到默认的策略就是REDIS_MAXMEMORY_NO_EVICTION
,永不淘汰,然后直接就会直接返回REDIS_ERR
。
5)、执行具体的命令调用(例如set)
再之后,就是正式调用了。
void call(redisClient *c, int flags) {
// start 记录命令开始执行的时间
long long dirty, start, duration;
// 记录命令开始执行前的 FLAG
int client_old_flags = c->flags;
...........
// 保留旧 dirty 计数器值
dirty = server.dirty;
// 计算命令开始执行的时间
start = ustime();
// 执行实现函数
c->cmd->proc(c);
// 计算命令执行耗费的时间
duration = ustime()-start;
// 计算命令执行之后的 dirty 值
dirty = server.dirty-dirty;
.........
server.stat_numcommands++;
}
这里正在调永的是c->cmd->proc(c);
,这个是与你具体的命令相关的,当前是set
命令其就会到:
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(redisClient *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = REDIS_SET_NO_FLAGS;
.........
// 尝试对值对象进行编码
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
void setGenericCommand(redisClient *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
..........
// 将键值关联到数据库
setKey(c->db,key,val);
// 将数据库设为脏
server.dirty++;
// 为键设置过期时间
if (expire) setExpire(c->db,key,mstime()+milliseconds);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",key,c->db->id);
.......
// 设置成功,向客户端发送回复
// 回复的内容由 ok_reply 决定
addReply(c, ok_reply ? ok_reply : shared.ok);
}
这里先是通过setKey(c->db,key,val)
将其设置到hash表中(获取redisClient对应的db
),然后将server.dirty
自增(用于rdb同步)。
// 检查是否有某个保存条件已经满足了
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
REDIS_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == REDIS_OK))
{
redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
// 执行 BGSAVE
rdbSaveBackground(server.rdb_filename);
break;
}
例如这里就是判断修改时间间隔,以及在这个时间内进行了多少次修改,条件满足的话,就通过rdbSaveBackground(server.rdb_filename)
来解析rdb
快照备份。
然后下面我们就来具体说明下关于命令的hash表字典的设置(setKey(c->db,key,val)
)、获取,以及rehash 的处理。
二、具体的命令设值 setKey(c->db,key,val)
我们当前的命令是set t5 v4
void setKey(redisDb *db, robj *key, robj *val) {
// 添加或覆写数据库中的键值对
if (lookupKeyWrite(db,key) == NULL) {
dbAdd(db,key,val);
} else {
dbOverwrite(db,key,val);
}
......
// 移除键的过期时间
removeExpire(db,key);
// 发送键修改通知
signalModifiedKey(db,key);
}
这里实现判断有没有这个key,没有就添加设置,有的话就取覆盖。
1、lookupKeyWrite & lookupKey
robj *lookupKeyWrite(redisDb *db, robj *key) {
// 删除过期键
expireIfNeeded(db,key);
// 查找并返回 key 的值对象
return lookupKey(db,key);
}
robj *lookupKey(redisDb *db, robj *key) {
// 查找键空间
dictEntry *de = dictFind(db->dict,key->ptr);
// 节点存在
if (de) {
// 取出值
robj *val = dictGetVal(de);
.........
// 返回值
return val;
} else {
// 节点不存在
return NULL;
}
}
这里就是通过key
使用dictFind
方法来查找对应的dictEntry
,然后通过dictGetVal
来获取value
。
dictGetVal(he)
#define dictGetVal(he) ((he)->v.val)
2、dictFind
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
unsigned int h, idx, table;
......
// 如果条件允许的话,进行单步 rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
// 计算键的哈希值
h = dictHashKey(d, key);
// 在字典的哈希表中查找这个键
// T = O(1)
for (table = 0; table <= 1; table++) {
// 计算索引值
idx = h & d->ht[table].sizemask;
// 遍历给定索引上的链表的所有节点,查找 key
he = d->ht[table].table[idx];
// T = O(1)
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
// 如果程序遍历完 0 号哈希表,仍然没找到指定的键的节点
// 那么程序会检查字典是否在进行 rehash ,
// 然后才决定是直接返回 NULL ,还是继续查找 1 号哈希表
if (!dictIsRehashing(d)) return NULL;
}
// 进行到这里时,说明两个哈希表都没找到
return NULL;
}
这里的逻辑就是先通过dictIsRehashing(d)
判断当前是不是还在进行rehash
,如果是的话就通过_dictRehashStep(d)
来进行渐进rehash
。
完成后就通过h
找到两张ht
表对应槽位的元素,遍历这个链表,找到了就返回完成本次搜索。
3、单步渐进hash
1)、dictIsRehashing(ht)
#define dictIsRehashing(ht) ((ht)->rehashidx != -1)
首先是通过rehashidx != -1
判断其是不是在扩容,如果为-1
表示其没有扩容,如果为正
,表示其在扩容,同时这个值是顺序自增的,没进行一次单步rehash
就+1
。一直到ht[0]
的used
为0,就是rehash完成,都转移到了ht[1]
。然后进行rehash
:
2)、_dictRehashStep(dict *d) & dictRehash(dict *d, int n)
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {
........
// 进行 N 步迁移
// T = O(N)
while(n--) {
dictEntry *de, *nextde;
/* Check if we already rehashed the whole table... */
// 如果 0 号哈希表为空,那么表示 rehash 执行完毕
// T = O(1)
if (d->ht[0].used == 0) {
// 释放 0 号哈希表
zfree(d->ht[0].table);
// 将原来的 1 号哈希表设置为新的 0 号哈希表
d->ht[0] = d->ht[1];
// 重置旧的 1 号哈希表
_dictReset(&d->ht[1]);
// 关闭 rehash 标识
d->rehashidx = -1;
// 返回 0 ,向调用者表示 rehash 已经完成
return 0;
}
// 略过数组中为空的索引,找到下一个非空索引
while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
// 指向该索引的链表表头节点
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
// 将链表中的所有节点迁移到新哈希表
// T = O(1)
while(de) {
unsigned int h;
// 保存下个节点的指针
nextde = de->next;
/* Get the index in the new hash table */
// 计算新哈希表的哈希值,以及节点插入的索引位置
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 插入节点到新哈希表
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
// 更新计数器
d->ht[0].used--;
d->ht[1].used++;
// 继续处理下个节点
de = nextde;
}
// 将刚迁移完的哈希表索引的指针设为空
d->ht[0].table[d->rehashidx] = NULL;
// 更新 rehash 索引
d->rehashidx++;
}
return 1;
}
这个就是rehash的代码,还是很清晰的:
这个入参n
,就是步长,就是一次转移几个hash表的槽位,以及与下面的d->rehashidx++
是对应的。所谓渐进式rehash,就是分多长完成rehash
数据转移,同时在转移过程中ht[0]
与ht[1]
是同时使用的,如果0
表查询不到,就会去1
表查。
然后这里实现是判断ht[0].used == 0
表正在使用的表示不是为0
,如果为0,就表示已经都转移到ht[1]
,就表示已经完成了整个rehash
转移,就将现在的ht[1]
设置为ht[0]
,然后再d->rehashidx = -1;
表示已经完成了rehash扩容。
再之后,就是将ht[0]
对应槽位的链表(while(de)
、de = nextde;
)都转移到ht[1]
中,同时d->ht[0].used--
、d->ht[1].used++
。
并且入参n
多大,就进行几次while
槽位转移。
3)、rehash步长方法的调用时机
那一般时候调用呢?
1、目前我们就是在通过key查到的时候调的:dictFind(dict *d, const void *key)
2、在进行字典表数据的添加的时候:dictAddRaw(dict *d, void *key)
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
// 如果条件允许的话,进行单步 rehash
// T = O(1)
if (dictIsRehashing(d)) _dictRehashStep(d);
........
3、在随机获取key的时候:dictGetRandomKey(dict *d)
dictEntry *dictGetRandomKey(dict *d)
{
dictEntry *he, *orighe;
unsigned int h;
int listlen, listele;
// 字典为空
if (dictSize(d) == 0) return NULL;
// 进行单步 rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
4、在进行删除的时候:dictGenericDelete(dict *d, const void *key, int nofree)
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
unsigned int h, idx;
dictEntry *he, *prevHe;
int table;
// 字典(的哈希表)为空
if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */
// 进行单步 rehash ,T = O(1)
if (dictIsRehashing(d)) _dictRehashStep(d);
5、还有一个就是在后台处理的:dictRehashMilliseconds(dict *d, int ms)
int dictRehashMilliseconds(dict *d, int ms) {
// 记录开始时间
long long start = timeInMilliseconds();
int rehashes = 0;
while(dictRehash(d,100)) {
rehashes += 100;
// 如果时间已过,跳出
if (timeInMilliseconds()-start > ms) break;
}
return rehashes;
}
可以看到这个方法的步长是100
。不像前面的_dictRehashStep
默认为1
。
4、dbAdd(redisDb *db, robj *key, robj *val)
这个就是添加键值到db中:
void dbAdd(redisDb *db, robj *key, robj *val) {
// 复制键名
sds copy = sdsdup(key->ptr);
// 尝试添加键值对
int retval = dictAdd(db->dict, copy, val);
......
}
int dictAdd(dict *d, void *key, void *val)
{
// 尝试添加键到字典,并返回包含了这个键的新哈希节点
// T = O(N)
dictEntry *entry = dictAddRaw(d,key);
// 键已存在,添加失败
if (!entry) return DICT_ERR;
// 键不存在,设置节点的值
// T = O(1)
dictSetVal(d, entry, val);
// 添加成功
return DICT_OK;
}
这个就是添加的逻辑,不过这个dictAddRaw(d,key)
方法我们通过,这里就不再赘述了。我们这里可以关注到另一种数据结构,就是sds
。这个是redis设计用来取代字符数组,可以看到目前我们的key就是用这个结构表示的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZoVvMCP-1626492884755)(F:\MarkDown-File\中间件源码\Redis源码解读(1)-基本的数据结构.assets\image-20210717093615247.png)]
三、redis中的一些其他数据结构
/* Object types */
// 对象类型
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
// 对象编码
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
redis有上面的五种面向用户的类型,当其内部是用的REDIS_ENCODING_XXX
这些类型处理的。
1、sdshdr(sds)
struct sdshdr {
int len;
int free;
char buf[];
};
这个sdshdr
是redis设计用来存放字符串的,这个结构其使用buf[]
来存放字符数组、len
表示占用的空间、free
表示还有多少没有使用。
目前我们的key
为t5
,然后当前sdshdr
的长度为2
,因为都占满了,所以free
为0
2、zskiplist 跳跃表
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
这个就是跳跃表的结构。首先是zskiplistNode
,也就是跳跃表的节点
结构体:
1)、zskiplistNode
score
表示分值(跳跃表是用来给zset
类型使用的,但要注意,zset
类型不一定使用这个skiplist
,数量少的时候可能是使用的压缩列表)。
robj *obj
,表示key对应的值描叙,redis中,我们前面提到的dictEntry
其的值(不管是用的哪种数据类型),都是会构建一个robj
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
然后这里的type:4
就是表示的对象类型,就是我们前面提到的REDIS_ENCODING_XXX
这些,然后里面达到一定的条件就会进行一些转换。*ptr
表示指向真正的类型。
然后这个结构体主要是有level[]
,这个也就是具体的跳远表的内容。主要有两个参数forward
表示指向后面的指针、span
表示向后面跳了多少个节点,也就是跨度为多少。
上面这个就是跳跃表的简单模型(这里只是为了说明的一个简单模型),这个例如说,当我们要查找5
节点,我们就不用从首节点开始往下遍历,注解就知道第一层然后直接跳到4
节点,往下。同样要找10
的话,也可以直接找到第二层注解到8
节点。感觉有点树结构的味道了。
2)、zskiplist
zskiplist
就是描叙跳跃表,保持头结点、尾结点、保持的元素个数等。
3、encoding编码类型说明
例如我们现在使用hset hs1 k1 v1
来设置,在c->cmd->proc(c);
这里就会调用hsetCommand(redisClient *c)
、(t_hash.c
文件),而我们前面的set t5 v4
是调用的setCommand(redisClient *c)
、(t_string.c
文件)。
void hsetCommand(redisClient *c) {
int update;
robj *o;
// 取出或新创建哈希对象
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 如果需要的话,转换哈希对象的编码
hashTypeTryConversion(o,c->argv,2,3);
// 编码 field 和 value 对象以节约空间
hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);
// 设置 field 和 value 到 hash
update = hashTypeSet(o,c->argv[2],c->argv[3]);
..........
// 将服务器设为脏
server.dirty++;
}
然后这里的hashTypeLookupWriteOrCreate
方法就是创建robj
对象:
robj *hashTypeLookupWriteOrCreate(redisClient *c, robj *key) {
robj *o = lookupKeyWrite(c->db,key);
// 对象不存在,创建新的
if (o == NULL) {
o = createHashObject();
dbAdd(c->db,key,o);
// 对象存在,检查类型
}..........
// 返回对象
return o;
}
robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(REDIS_HASH, zl);
o->encoding = REDIS_ENCODING_ZIPLIST;
return o;
}
然后我们可以看到现在其的初始化也是设置的o->encoding
为REDIS_ENCODING_ZIPLIST
。
然后下面的hashTypeTryConversion
我们可以看到其达到一定的条件其就会转换为REDIS_ENCODING_HT
也就是hash表。
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
// 如果对象不是 ziplist 编码,那么直接返回
if (o->encoding != REDIS_ENCODING_ZIPLIST) return;
// 检查所有输入对象,看它们的字符串值是否超过了指定长度
for (i = start; i <= end; i++) {
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
// 将对象的编码转换成 REDIS_ENCODING_HT
hashTypeConvert(o, REDIS_ENCODING_HT);
break;
}
}
}
4、压缩列表
/*
空白 ziplist 示例图
area |<---- ziplist header ---->|<-- end -->|
size 4 bytes 4 bytes 2 bytes 1 byte
+---------+--------+-------+-----------+
component | zlbytes | zltail | zllen | zlend |
| | | | |
value | 1011 | 1010 | 0 | 1111 1111 |
+---------+--------+-------+-----------+
^
|
ZIPLIST_ENTRY_HEAD
&
address ZIPLIST_ENTRY_TAIL
&
ZIPLIST_ENTRY_END
非空 ziplist 示例图
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte
+---------+--------+-------+--------+--------+--------+--------+-------+
component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
+---------+--------+-------+--------+--------+--------+--------+-------+
^ ^ ^
address | | |
ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END
|
ZIPLIST_ENTRY_TAIL
typedef struct zlentry {
// prevrawlen :前置节点的长度
// prevrawlensize :编码 prevrawlen 所需的字节大小
unsigned int prevrawlensize, prevrawlen;
// len :当前节点值的长度
// lensize :编码 len 所需的字节大小
unsigned int lensize, len;
// 当前节点 header 的大小
// 等于 prevrawlensize + lensize
unsigned int headersize;
// 当前节点值所使用的编码类型
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
这个就是压缩列表的具体节点entry1
、entry2
这些结构体,我们可以其不同于一般的链表,是通过前后指针,压缩列表是连续的数组结构。当他与一般的数组与不同例如int[]
这种,这种只能存储一种确定的类型,二压缩列表是能存储多种的。
所以由这个我们就能找到,如果要存储不同的长度内容话,其首先要记录下自己所占的长度lensize, len
。
然后这里也有encoding
,这个我们前面提过,就是达到一定条件可能就会解析数据结构的转换。
然后我们要遍历的话,我们由于不是链表有前后指针,所以我们就需要知道前面节点的长度才能获取prevrawlensize, prevrawlen
。以上就是这个压缩列表的一些基本内容了。