1.链表
Redis链表的实现较为简单,主要结构为链表节点和链表头节点,链表节点(adlist.h/listNode)结构主要由以下几部分组成:
- 前置节点prev:链表节点类型的指针,指向前一个节点
- 后置节点next:同前置节点类型相同,指向后一个节点
- value:代表这个节点的值
链表头节点主要是为了更好地操作这个链表,主要有以下部分:
- 头节点head,链表节点类型指针,指向这个链表表头
- 尾节点tail,同头节点类型一样,指向表尾
- len:链表长度 unsigned long类型
- dup函数:节点值复制函数
- free函数:节点释放函数
- match函数:节点值对比函数
2.字典
2.1 字典结构
字典结构主要由三个部分构成,字典结构,hash表结构,hash表节点结构;
字典主要结构(dict.h/dict)如下:
- dictType *type:类型特定函数,dictType结构体主要存储了一些为hash表服务的函数(如计算hash值,复制键,复制值,销毁键,销毁值,对比键的函数)
- void *privatedata:私有数据,保存需要传递给类型特定函数的可选参数。这个属性和第一个属性主要是为了为多态服务。
- hash表数组:包含两个hash表,平常使用ht[0],rehash时使用ht[1]
- int rehashIdx:指示rehash的进度,若目前没有进行rehash则置为-1
hash表结构(dict.h/dict.ht)主要如下:
- dictEntry **table:hash表数组,里面包含多个hash表节点结构
- unsigned long size:hash表大小
- unsigned long sizemask:索引掩码,永远等于size-1,详情见hash算法一节
- unsigned long used :hash表已有节点数量
hash表(dictEntry)节点结构:
- void *key; 键
- union{
void *val;
uint64_t;
int64_t s64;
} v; 值,可以为一个指针,也可以为 uint64_t,int64_t的值 - struct dictEntry *next; 指向下一个节点
2.2 hash算法
Redis的hash算法和java的HashMap类似,主要分为两步
- 根据hash函数计算出key的hash值
- 用hash值和hash表的sizemask属性想与(&),计算出索引值
这里解释一下为什么采用与操作可以计算索引值:因为sizemask的值为size-1,而size的大小我们通过源码可以看出初始化一个hash表他的大小为4,而扩容时又保证了他的size永远为2的n次幂,因此sizemask的二进制值永远为01…11(如初始化时为011),所以可以用与操作来计算索引值。
2.3 解决键冲突
Redis的hash表解决键冲突的方法和java的HashMap一样采用拉链法(链地址法),即相同索引的键值对会放在一个链表上,不同的时java的HashMap会把新的对象放到链表末端(对应的访问时也会访问链表末尾),而Redis为了实现访问速度更快,因此把新节点放到了链表的头部(Redis链表有指向头结点的指针)
2.4 rehash(Hash表的扩展与收缩)
首先是hash表扩展和收缩的时间:主要与负载因子(used/size)有关。
当服务器没有执行BGSAVE命令和BGREWRITEAOF命令,负载因子大于1时进行扩展
当服务器执行BGSAVE命令和BGREWRITEAOF命令,负载因子大于5时扩展
BGSAVE命令和BGREWRITEAOF命令需要创建子进程,由于有很多系统都采用写时复制的方法与Redis交互,因此存在子进程时会尽量避免扩展操作,以减少对内存的读写。
另一方面,当负载因子小于0.1时,Redis进行收缩操作。
Hash表的扩展和收缩主要通过rehash来执行,rehash的步骤:
- 为字典的ht[1]分配空间:如果执行扩展操作,那么分配第一个大于等于ht[0].used * 2 的2的n次幂
如果执行的是收缩操作,则分配第一个大于等于ht[0].used的2的n次幂 - 将ht[0]上的键值对rehash到ht[1]上。(采用渐进式rehash的方法)
- 当ht[0]上面的所有值均处理完毕后,将ht[1]赋给ht[0],在为ht[1]分配一个新的Hash表
2.5渐进式rehash
渐进式rehash主要是为了避免一次将ht[0]上面的数据rehash到ht[1]上导致的服务器停止服务(Redis为单线程)。所以采取渐进式的方式,逐渐的将ht[0]上的数据rehash到ht[1]上。主要步骤如下:
- 为ht[1]分配空间,此时字典同时持有ht[0]和ht[1]两个hash表。
- 再字典中维持一个索引计数器变量rehashidx,并将其设为0,代表rehash开始
- 再reahsh期间,每次对字典添加,删除,查找或者更新操作时,均会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],并对rehashidx增一。(遇到rehashidx上值为null会跳过)
- 当ht[0]上的所有键值对都完成rehash之后,将rehashidx的值置为-1。代表rehash结束。
在rehash期间,所有的增加操作均会增加到ht[1]上,查找时会先在ht[0]上查找,查不到的话会查找ht[1]