Redis六种数据结构
1. 简单动态字符串(Simple Dynamic String),简称SDS
我们知道Redis当中保存的key为字符串类型,value也往往是字符串或者字符串集合。可见字符串是Redis中最常见的一种数据结构。虽然Redis是用C语言实现的,但是Redis并没有使用C语言中的字符串,因为C语言中的字符串(C语言中声明的字符串,本质是字符数组)存在很多问题:比如
1. 需要通过运算获取字符串长度
2. 非二进制安全(不能包含特殊字符,'\n')
3. 不可修改
Redis是C语言实现的,其中SDS是一个结构体,源码如下:(刚开始时len和alloc值相同,后面不一定相等)
uint8_t 表示无符号,整型,占8个bit位,即len的最大值为255,对应数组buf[]最大255个字节,在C语言中,char占一个字节,所以数组最大存放255个字符。
由于SDS是根据字符串长度来获取字符串的,所以不会存在由于遇到特殊字符'\0'而没有读取到完整字符串就停止读取的情况。获取字符串长度只要读取len即可,不需要进行计算。
另外,SDS之所以叫动态字符串,是因为它具备动态扩容的能力。例如一个内容为"hi"的SDS
len:2 | alloc:2 | flags:1 | h | i | \0 |
假如我们要给SDS追加一段字符串",Amy",首先会申请新的内存空间
1. 如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
2. 如果新字符串长度大于1M,则新空间为扩展后字符串长度+1M+1,称为内存预分配;
总结可以得出SDS的-以下几个优点:
1. 获取字符串长度的时间复杂度为O(1)
2. 支持动态扩容
3. 减少内存分配次数
4. 二进制安全
2. 整数数组 IntSet
IntSet是Redis中Set集合的一种实现方式,基于整数数组实现,具有长度可变和有序等特征。
为了方便查找,Redis将IntSet中的所有整数按照升序依次保存在contents数组中。(头部分固定占8个字节)
IntSet升级
现在,假设有一个intset,元素为{5,10,20},采用的编码是INTSET_ENC_INT16,则每个整数占2字节:
我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。
以当前案例来说流程如下:
1 升级编码为INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式及元素个数扩容数组
2 倒序依次将数组中的元素拷贝到扩容后的正确位置
3 将待添加的元素放入数组末尾
4 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4
3. Dict
我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。
Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry,键和值的数据类型大多数都是指针,指向SDS对象)、字典(Dict)
当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用h&sizemask来计算元素应该存储到数组中的哪个索引位置。(size一定是,h&sizemask和h%size效果一样,但是h&sizemask性能更高)。我们存储k1=v1,假设k1的哈希值h=1,则1&3=1,因此k1=v1要存储到数组角标1位置。存储k2=v2,假设哈希值也是h=1,也要存储到角标1位置,采用头插法进行插入。
Dict的扩容
Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低。
Dict在每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容:
◆ 哈希表的LoadFactor>=1,并且服务器没有执行BGSAVE或者BGREWRITEAOF等后台进程;
◆ 哈希表的LoadFactor>5;
Dict的收缩
Dict除了扩容以外,每次删除元素时,也会对负载因子做检查,当LoadFactor<0.1时,会做哈希表收缩:
Dict的rehash
不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash。过程是这样的:
1. 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:
◆ 如果是扩容,则新size为第一个大于等于dict.ht[0].used+1的
◆如果是收缩,则新size为第一个大于等于dict.ht[0].used的(不得小于4)
2. 按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]
3. 设置dict.rehashidx=0,标示开始rehash
4. 将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]
Dict的rehash并不是一次性完成的。试想一下,如果Dict中包含数百万的entry,要在一次rehash完成,极有可能导致主线程阻塞。因此Dict的rehash是分多次、渐进式的完成,因此称为渐进式rehash。
4. 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1] (rehash过程中数据要么在dict.ht[1]中要么在dict.ht[0]中,不会同时在两个表中存在,所以要在两个表中都进行查改删,新增只需在dict.ht[1]中)
5. 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存