Redis的底层数据结构

redis快的原因

  • 内存操作

  • 优秀的数据结构

redis数据类型和底层数据结构

底层数据结构

  • 简单动态字符串

  • 双向链表

  • 压缩列表

  • 哈希表

  • 跳表

  • 整数数组

数据类型和底层数据结构映射关系

image

键和值的组织结构

全局哈希表:保存了所有键值对的映射关系

image

一个哈希表,其实就是一个数组,数组每个元素称为一个哈希桶 entry, entry 中存储的是 key 和 value 的指针,如果出现哈希冲突通过拉链法解决,也就是 entry 中多一个 next 指针,指向下一个在此位置的 entry .

问题点

哈希表冲突及rehash可能带来的操作阻塞

rehash操作

原因

哈希冲突:两个key的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

redis解决办法:拉链法

image

某些哈希冲突链过长,会导致对应哈希桶元素查找时间耗时变长,故有了rehash机制。

rehash实现

rehash操作:增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始,刚插入数据默认使用哈希表1,此时哈希表2未分配空间。随着数据增多,redis开始执行rehash,分为三步:

  1. 给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍;

  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;

  3. 释放哈希表 1 的空间。

用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。

渐进式rehash

原因:"哈希表1中的数据重新映射并拷贝到哈希表2"操作涉及大量数据拷贝,一次性迁移会造成redis线程阻塞,无法服务其它请求。

拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。

image

把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

数据类型的操作效率

redis常见五大数据类型
string/list/hash/set/zset

  • string

全局哈希表找到桶即可,时间复杂度O(1),对应底层数据结构简单动态字符串

  • 其余集合元素

    集合元素的效率:

    1. 与底层数据结构有关;如通过哈希表要比链表效率更高。

    2. 与操作类型有关;如,读写单个数据要比批量查询效率更高。

    ** list

    双向链表 压缩列表

    ** hash

    哈希表 压缩列表

    ** set

    整数数组 哈希表

    ** zset

    跳表 压缩列表

底层数据结构效率影响

image

压缩列表

image

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。

zlbytes:列表长度

zltail:列表尾偏移量

zllen:列表entry个数

zlend:表示列表结束

要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,只能逐个查找,复杂度就是 O(N) 。

跳表

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。

跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

image

时间复杂度:同二分查找O(log(N)), N为数据量。

操作类型效率影响

  • 单元素操作

指每一种集合类型对单个数据实现的增删改查操作。

单个设置为O(1),批量设置为O(n), 比如hmset。

  • 范围操作

指集合类型中的遍历操作,可以返回集合中的所有数据。

复杂度一般是 O(N),比较耗时,我们应该尽量避免。

如果需求合适,可以利用scan系列操作渐进式遍历,每次只返回有限数据,避免redis阻塞。

  • 统计操作

指集合类型对集合中所有元素个数的记录。

这类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计。

  • 例外情况

指某些数据结构的特殊记录。
例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。

整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

缓存行:一般来说一个缓存行的大小在32kb-256kb之间,而通常的缓存行大小则为64kb(工业得出的最优解)

缓存行 (Cache Line) 便是 CPU Cache 中的最小单位,CPU Cache 由若干缓存行组成,一个缓存行的大小通常是 64 字节(这取决于 CPU),并且它有效地引用主内存中的一块地址。
你正在遍历一个长度为 16 的 long 数组 data[16],原始数据自然存在于主内存中,访问过程描述如下:

访问 data[0],CPU core 尝试访问 CPU Cache,未命中。

尝试访问主内存,操作系统一次访问的单位是一个 Cache Line 的大小 — 64 字节,这意味着:既从主内存中获取到了 data[0] 的值,同时将 data[0] ~ data[7] 加入到了 CPU Cache 之中,for free~

访问 data[1]~data[7],CPU core 尝试访问 CPU Cache,命中直接返回。
访问 data[8],CPU core 尝试访问 CPU Cache,未命中。尝试访问主内存。重复步骤 2

posted @ 2023-03-11 16:23  kiper  阅读(189)  评论(0编辑  收藏  举报