Redis的底层数据结构
redis快的原因
-
内存操作
-
优秀的数据结构
redis数据类型和底层数据结构
底层数据结构
-
简单动态字符串
-
双向链表
-
压缩列表
-
哈希表
-
跳表
-
整数数组
数据类型和底层数据结构映射关系
键和值的组织结构
全局哈希表:保存了所有键值对的映射关系
一个哈希表,其实就是一个数组,数组每个元素称为一个哈希桶 entry, entry 中存储的是 key 和 value 的指针,如果出现哈希冲突通过拉链法解决,也就是 entry 中多一个 next 指针,指向下一个在此位置的 entry .
问题点
哈希表冲突及rehash可能带来的操作阻塞
rehash操作
原因
哈希冲突:两个key的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。
redis解决办法:拉链法
某些哈希冲突链过长,会导致对应哈希桶元素查找时间耗时变长,故有了rehash机制。
rehash实现
rehash操作:增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始,刚插入数据默认使用哈希表1,此时哈希表2未分配空间。随着数据增多,redis开始执行rehash,分为三步:
-
给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍;
-
把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
-
释放哈希表 1 的空间。
用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。
渐进式rehash
原因:"哈希表1中的数据重新映射并拷贝到哈希表2"操作涉及大量数据拷贝,一次性迁移会造成redis线程阻塞,无法服务其它请求。
拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
数据类型的操作效率
redis常见五大数据类型
string/list/hash/set/zset
- string
全局哈希表找到桶即可,时间复杂度O(1),对应底层数据结构简单动态字符串
-
其余集合元素
集合元素的效率:
-
与底层数据结构有关;如通过哈希表要比链表效率更高。
-
与操作类型有关;如,读写单个数据要比批量查询效率更高。
** list
双向链表 压缩列表
** hash
哈希表 压缩列表
** set
整数数组 哈希表
** zset
跳表 压缩列表
-
底层数据结构效率影响
压缩列表
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。
zlbytes:列表长度
zltail:列表尾偏移量
zllen:列表entry个数
zlend:表示列表结束
要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,只能逐个查找,复杂度就是 O(N) 。
跳表
有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。
时间复杂度:同二分查找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