咱们以前已经讲过了Redis的数组列表(List),但其实Redis
中最经常使用的数据结构是字典(hash)
,能够说,Redis
总体的设计都是基于字典的,这不只仅体如今咱们存取数据都是经过键值对的方式,还在于其余的复合数据结构set/zset
也都是基于hash
来设计的。php
字典在任何语言中都是很是基础和常见的数据结构,在Java中它是HashMap
,在PHP中它是Array
,在JS中它是Object
,它更常见的是通用的数据传输格式JSON
。
字典是一种可变容器型数据结构,能够存储任意类型的数据,它经过哈希表来存储数据和访问,哈希表是其实现原理。java
hashTable
哈希表哈希表又称为散列表,它根据键(key)
来直接访问指定存储位置的数据,而肯定要访问的指定存储位置是经过散列函数对key
值进行压缩生成摘要,生成固定长度的随机字母和数字的字符串,建立散列值,但在hashTable
中,咱们会使用纯数字。优秀的散列函数会尽可能均匀的分布数据,避免散列冲突。python
哈希表的主要目的是加快查找指定key的速度,不管hashTable
中有多少元素,它的查找效率均为O(1)
,至关于无需遍历,直接定位到元素。算法
如图所示,哈希表是数组+链表的二维数据结构,数组是第一维,链表是第二维。数组中的每一个元素称为槽或者桶,存储着链表的第一个元素的指针。在上图中咱们一共有11个槽,如今咱们有a-h共8个key
须要存储,我么会使用哈希函数对每一个key值生成纯数字的摘要,而后将数字对11取模,这样就可以肯定该key值应该放在哪一个槽中。当多个key值的摘要取模后相等时,就会使用链表进行串联依次存储key值,这种状况称为散列冲突,也叫碰撞。segmentfault
这样,当咱们须要取某个key时,只须要对这个key进行从新hash获得摘要,而后取模就能知道它在哪一个槽里了,而后经过链表的遍历依次匹配,就能获得指定的key,进而取得value。数组
这种哈希表遍历的时候是无序的,由于常规的遍历方式是从槽0遍历到槽10,当当前槽存在元素链表时,再按照顺序依次进行遍历,这样咱们的遍历顺序就跟存储的时候不一样,所以说是无序的。浏览器
对于JS的对象来讲,咱们也说遍历的顺序是没法保证的,但若是元素的集合已是肯定的,那么遍历的顺序应该是一致的呀。这里涉及到的问题就是槽的数量可能不一样,所以顺序是彻底不一样的,在不一样浏览器引擎的设计中,初始化设置的槽数不一样,扩缩容的时机和空间数也是不一样的,所以没法保证。对于Redis也是这样。缓存
咱们前面说到字典的查找效率是O(1)
的,这是创建在字典可以充分hash的前提下,也就是一维数组要足够用保证二维链表不会过长,不然查找效率会下降到O(lgn)
,甚至在只有一个槽时下降到O(n)
。数据结构
所以咱们会在散列冲突较多时对字典进行扩容,但扩容是以牺牲空间为代价提升效率的,Redis
做为内存占用型的缓存系统,能够说内存很是宝贵,所以咱们须要在槽空洞过多时进行缩容。函数
扩容条件:hashTable
中元素的个数等于一维数组长度时,会对数组长度进行两倍的扩容。不过若是系统正在作bgsave
(后台刷新内存数据到磁盘中)时,会延迟扩容时机。但当元素达到一维数组长度的5倍时,就会强制执行扩容。
缩容条件:元素个数小于一维数组长度的10%
。
rehash
从新哈希当扩缩容时,咱们须要申请新的一维数组,并对全部元素进行从新哈希和挂载元素链表。因为Redis是单线程的,所以咱们为了避免阻塞服务的正常运转,咱们采用渐进式rehash
的策略。
也就是全部的字典结构内部首层是一个数组,数组的两个元素分别指向一个哈希表,正常状况下只有一个哈希表,而在迁移过程当中会同时保留新旧两个哈希表,元素有可能存在于两个表中的任意一个,所以会同时尝试从两个哈希表中查找数据。当数据搬迁完成后,老的哈希表就会被自动删除。
渐进式hash不是一次性将字典的内容搬完,而是经过每一个执行命令时搬运一部分,同时定时任务搬运一部分,最终用时间来换取执行效率。
对于hashTable
来讲,哈希函数相当重要,由于好的哈希函数能够将哈希表的key值打散的比较均匀,这样高随机性的元素分布也可以提高总体的查找效率。Redis使用的哈希函数是siphash
。
若是哈希函数打散的效果不好,或者有模式能够遵循,那么就会存在hash攻击,攻击者利用模式的偏向性经过大量产生数据,将这些数据尽量挂载在同一个链表上,这种hash不均匀会致使查找的性能急剧降低同时浪费大量的内存空间,进而拖垮Redis总体的性能。
set
集合set和字典很是相似,其内部实现就是上述的hashTable的特殊实现,与字典不一样的地方有两点:
NULL
。zset
有序集合zSet是Redis很是有特点的数据结构,它是基于Set并提供排序的有序集合。其中最为重要的特色就是支持经过score的权重来指定权重。
zadd code 9.0 "java" zadd code 8.0 "python" zadd code 8.5 "php" zrange code 0 -1 1) "python" 2) "php" 3) "java"
此时,全部的value都变成了score
。而这种支持经过score
来进行排序的则是经过另外一个特殊的数据结构:跳跃列表。
skiplist
跳跃列表Redis的zset数据结构是一个复合结构,经过一个相似于set的hashTable来实现value和score的对应关系,也支持set的快速读写和去重的功能。同时经过skiplist来支持按照score排序的功能。
上图中每一列表明一个元素,从左到右score值愈来愈大,最左侧的kv head
表明起始位置,score值为MIN_VALUE
。最底层用双向链表串联,用于反向遍历,元素的二层以上会有指向下一个同层高元素的单向指针。
如上图所示,当咱们须要根据score值插入紫色kv节点时,咱们首先从kv-head
的最高层进行启动,判断指针的下个元素的score值是否小于新元素的score值,若是小于,则继续向前遍历,不然从kv-head降一层,从新比较判断。
经过这种对比,咱们能够仅仅比较6次就找到合适的新元素位置,这在大量数据的时候性能提高效果很是明显。
从上面的算法能够看出每一个元素的层高对该算法的执行效率影响很是明显,若是层高所有一致,效率就会变成O(n)
,从头遍历的感受。虽然为了不这种状况,skiplist
除了考虑score
值外,还考虑对value
值进行字符串对比并进行排序。可是层高算法依然很是重要。
每一个元素的层高经过随机算法分配层高,Redis层高总共64层,每层的晋升率为25%
,所以1层每一个元素是100%,二层是25%,三层就是1/16,四层就是1/64的几率。这样是为了减小层高,可以减小向下遍历的次数,同时可以承载更多的元素也不下降效率。
skipList
会记录最高的层高,并将kv-head
的高度置为这个层高。
查找元素的方法和上述新增元素的方法是一致的。
经过上述的查找方式找到元素后,直接删除元素,并更新先后的指针便可。若是最高层数变化了,须要更新下maxLevel
参数。
当更新元素时,其实就是改变了score值,这时Redis会直接先删除,而后插入。这样在某些场景下效率较低,如score值改变并不影响排序。
咱们能够获取指定排名段的元素列表,也能够得到指定元素的排名,这个rank
实际上是经过skiplist的span
属性获得的。
咱们在每一个forward
单项指针上都增长了span(跨度)属性,代表从上一个元素到下一个元素中间通过了多少个元素,所以,当咱们沿着上面的方法查找时,只须要将通过的全部指针的span值进行累加,就知道指定元素的排名是多少了。