前沿
Redis的底层数据结构大概分为简单字符串、字典、压缩列表、类似于LinkedList链表、跳跃表这几种,这边文章我们主要聊聊字典底层数据结构。
正文
字典结构(dict)是Redis服务器中出现最为频繁的复合型数据结构,用到地方很多,例如hash结构,如果数据少且小的时候会使用到字典,反之使用压缩链表,整个Redis所有key和value组成一个全局的字典、带过期时间的key集合、zset集合汇总存储value和score直接映射关系等等,字段结构其实类似于我们Java中的Map结构,下面我们看看他的底层的结构:
结构体
struct RedisDb { dict* dict; // all keys key=>value dict* expires; // all expired keys key=>long(timestamp) ... } struct zset { dict *dict; // all values value=>score zskiplist *zsl; } |
dict内部结构
从上图可以看出,他是由类似于Java中数组+链表组成
下面看字典的扩容方式
如上图可以看出,dict结构内部包含俩个hashtable,通常只有一个是有值的。但是在扩容缩融的时候,需要分配一个新的hashtable,然后进行渐进式搬迁,搬迁结束后,旧的hashtable被删除。
而hashTable的结构类似于hashMap结构,内部由数组+链表组成,通过分桶的方式解决hash冲突,数组中关联链表第一个元素的指针。具体结构如下图所示
struct dict { ... dictht ht[2]; } |
而dictht结构如下:
struct dictht { dictEntry** table; // 二维数组结构 long size; // 第一维数组的长度 long used; // hash 表中的已经存在的元素个数 ... } struct dictEntry { void* key; void* val; dictEntry* next; // 链接下一个 entry } |
渐进式rehash
由于大字典的扩容比较耗时间,要将旧的链表下的数据重新挂到新的链表下,是O(n)级别的操作,所以Redis采用渐进式的rehash方式搬迁。
扩容分类
Redis的扩容分为2种,一种是客户端触发,另一种是定时任务中对字典进行主动搬迁。
客户端触发搬迁过程:
首先判断是否需要扩容,若是,则小步搬迁;然后判断是否正在搬迁,是则将元素挂到新的分组下。
定时任务主动搬迁过程:也是类似的情况
查找过程
func get(key) { let index = hash_func(key) % size; let entry = table[index]; while(entry != NULL) { if entry.key == target { return entry.value; } entry = entry.next; } } |
将key映射成一个整数,该整数对大小取余,得到桶的下标,然后在该桶下面遍历获取与key相同的对应的value,最后将该value返回,大家有没有发现其实与hashMap的的get操作操作类似。
hash函数
hashtable性能完全取决于hash函数的质量,如果hash函数可以将key打散的比较均匀,从而防止热key的产生,那么该hash函数就比较好,而hash函数默认是siphash算法,该算法即使在key很小的情况下,能够产生随机性特别好的输出。
hash攻击
hash攻击指的是hash函数在存在偏向性,黑客利用这个特点对服务器进行攻击,在这种情况下输入会导致hash的第二维链表长度极为不均匀,很有可能所有的元素都集中在个别的链表下,导致查找效率特别的低下,从而导致服务器就会被测底拖垮。
扩容条件
正常情况下,当hash表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的大小是原数组的2倍,但如果Redis正在做bgsave,为了减少内存页的过多分离,Redis尽量不扩容(dict_can_resize),但如果hash表元素的个数已经达到数组大小的5倍(dict_force_resize_ratio参数可以设置),这时就要强制扩容。
缩容条件
缩容的条件是元素的个数小于数组长度的10%,但缩容不会考虑bgsave是否在进行。
set结构
Redis 里面 set 的结构底层实现也是字典,只不过所有的 value 都是 NULL,其它的特性和字典一模一样。