Redis中常用的数据结构
- 字符串String
- 列表List
- 哈希Hash
- 集合Set
- 有序集合Sort Set
数据结构底层实现
压缩列表
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。
压缩列表中会保存4个特殊字段:
zlbytes
字段,位于压缩列表的表头,表示列表长度
zltail
字段,位于压缩列表的表头,表示列表尾的偏移量
zllen
字段,位于压缩列表的表头,表示列表中元素的个数
zlend
字段,位于压缩列表的表尾,表示列表结束
相比于数组,压缩列表减少了不必要的内存,普通数组每个元素的大小相同,统一按照最大的元素大小来分配内存,而压缩列表则化整为零,压缩掉没有用到的空间来使空间变得紧凑,通过offset来控制元素访问;相比链表来说则是少了随机内存的碎片,压缩列表使用的是一块连续的内存空间
跳表
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
集合操作的复杂度
单元素操作是基础
范围操作非常耗时
统计操作通常高效
例外情况只有几个
- 单元素操作:指每一种集合类型对单个数据实现的增删改查操作
- 范围操作:指集合类型中的遍历操作,可以返回集合中的所有数据,这类操作一般的时间复杂度是O(N)
- 统计操作:指集合类型对集合中所有元素个数的记录
- 例外情况:指某些数据结构的特殊记录,比如压缩列表和双向链表都会记录表头和表尾的偏移量,这样在表头或者表尾添加删除元素的时间复杂度也就是O(1)了
Redis中的键和值
为了实现从键到值的快速访问,Redis中使用了一个哈希表来存储所有的键值对。
在Redis的全局哈希表中,在哈希桶里存储的是指向值的指针,而不是具体的值。
哈希冲突
Redis使用链地址法来解决全局哈希表的哈希冲突问题。
随着元素数量越来越多,哈希冲突的可能性也会越来越大,每次都需要遍历哈希桶对应的链表,所以需要Rehash操作。
Rehash操作
Redis会对全局哈希表去做rehash操作,也就是增加现有的哈希桶的数量,让逐渐增多的元素可以在更多的桶里分散保存,减少单个桶里的元素数据,从而减少单个桶中的元素数量
Redis会维护两个全局哈希表(表1和表2),一开始插入数据的时候,只会往表1中进行插入操作,也就是只会给表1分配内存空间,此时表2是没有分配内存空间的,当需要进行rehash操作的时候,会有如下操作:
- 给表2分配内存空间,一般是表1此时内存空间的2倍
- 把表1的数据拷贝到表2中
- 清空表1,释放表1的内存空间
为了防止在拷贝数据时导致Redis线程阻塞,服务不可用,Redis采用渐进式rehash。
渐进式rehash
对Rehash的第二步的数据拷贝环节进行优化,不是一次性拷贝所有的数据,而是分批次进行拷贝。
渐进式rehash分为两个部分:
- 在拷贝数据的过程中,Redis客户端仍然在处理请求,此时每处理一个请求,就将该请求涉及到的key进行拷贝操作,从表1拷贝到表2(一次只搬迁一个桶的数据)
- Redis会有一个定时任务,每隔一段时间就去拷贝一部分的key到表2(一次至少搬迁100个桶的数据)
在进行rehash操作的时候,删除、查找、更新操作会同时在两个哈希表上进行
- 当查找一个key的时候,会先在表1中进行查找,找不到再到表2去找
- 新添加的元素只会添加到表2,不会再添加到表1,这样就可以保证表1的数据只减不增
触发rehash操作的时机:
- 缩容操作: Redis 定时任务 serverCron 会在每个周期内检查 bucket 的使用情况。当存放 key 的数量和总 bucket 数的比例小于 HASHTABLE_MIN_FILL(10%),触发缩容 Rehash 操作
- 扩容操作:每次新增的时候回去检查当前存放的key与bucket的比例,超过了 dict_force_resize_ratio(5)就会触发扩容操作