链表
1.链表提供了高效的节点重排能力,已经顺序性的节点访问方式,还有灵活地增删能力
2.结构
//链表节点
struct listNode{
listNode* prev;
listNode* next;
void* value;
}
//链表
struct list{
listNode* head;
listNode* tail;
long len;
}
如图所示:
字典
字典在Redis应用非常广泛,可以说整个Redis都是基于字典来实现的。例如Redis的数据库就是使用字典作为底层实现,对数据库的增删改查都是转换为对字典的操作;其次字典还是哈希表的实现之一。其实现由哈希表、哈希节点、字典实现:
//哈希表
struct dictht{
dictEntry **table;//哈希表数组
long size;//表大小
long sizeMask;//哈希表大小掩码,等于size-1,用于计算索引
long used;//已有节点数量
}
//哈希节点
struct dictEntry{
void *key;//键
void *value;//值
dictEntry* next;//下个哈希节点
}
//字典
struct dict{
dictType* type;
dictht ht[2];
int rehashidx;//rehash索引,为-1时rehash停止
}
其中,字典结构中包含了两个哈希表,一般情况下只会用到ht[0]表,而在rehash过程中还会使用ht[1]表,而reashidx用于标记rehash的进度。
哈希算法
Redis采用MurmurHash2算法来计算键的哈希值,同时使用按位与和sizeMask计算键所在的位置,其伪代码如下:
hash = MurmurHash2(key);
index = hash & ht.sizeMask;
同时Redis采用链地址法来解决键的冲突,如果存在keyA和keyB计算处理来的index一样,则将这两个键存在同个表位置,用一个next指针连接起来形成链表。
rehash
随着操作的进行,哈希表保存的数据会逐渐增加或者减少,这个时候原先的哈希表的负载就没有维持在一个合理的范围,就需要系统对哈希表进行调整。
1.为字典的ht[1]进行空间分配,该表的大小计算方式如下:
1.1 进行扩展操作,则ht[1]的大小为大于等于ht[0].used*2的第一个2^n
1.2 进行缩小操作,则ht[1]的大小为大于等于ht[0].used/2的第一个2^n
2.将保存在ht[0]的键值对重新hash到ht[1]上面
3.迁移完ht[0]的数据后,清理ht[1]
发起时机
1 服务器没有执行BGSAVE 或者BGREWRITEAOF,并且哈希表的负载因子大于等于1
2.服务器没有执行BGSAVE 或者BGREWRITEAOF,并且哈希表的负载因子大于等于5
3.服务器没有执行BGSAVE 或者BGREWRITEAOF,并且哈希表的负载因子小于0.1
负载因子=used/size
渐进式rehash
由于Redis是单进程程序,所以任何一个耗时长的指令都会阻塞后序指令的操作。所以考虑到减少哈希表重新映射减少对系统的阻塞,Redis采用了渐进式rehash的方法来将ht[0]的键值对迁移到ht[1]
操作步骤
1.分配ht[1]空间
2.初始化rehashidx,设为0,表示rehashidx开始
3.每次进行字典操作的时候,都会将rehashidx位置的键值对迁移到ht[1],完成后rehashidx数值加1
4.随着操作的进行,最后将ht[0]中的所有数据都rehash到ht[1]表中,释放ht[0]表,将rehashidx置为-1
注意:在hash过程中,字典会持有两个哈希表,这就表示每次操作都需要检查两个表,如:
1.每次查找、更新、删除,都先在ht[0]中操作,找不到则在ht[1]中操作
2.每次新增值都会直接保存到ht[1]表中,避免重复操作