Redis-常见数据结构

1. 前言

Redis是一个Key-Value的存储系统,使用ANSI C语言编写。 key的类型是字符串。value的数据类型8种数据类型。

  • Redis的数据结构有5种常见数据结构
1. string字符串类型
2. list列表类型
3. hash散列类型
4. set集合类型
5. zset有序集合
复制代码
  • 不常见的数据类型
1. bitmap位图类型
2. geo地理位置
3. stream类型
复制代码

它们有什么特点和运用场景。Redis对外暴露的数据结构,用于API的操作,而组成它们的底层基础数据结构又是什么呢?

image.png

2. String数据结构

Redis的String能表达3种值的类型:字符串、整数、浮点数。Redis的字符串是动态字符串。

2.1 什么是二进制安全

在C语言中,用“\0”表示字符串的结束,如果字符串本身就有“\0”字符,字符串就会被截断,即非二进制安全。如果通过某种机制,当有“\0”时,也不会被截断,那么就是二进制安全

Redis3.2版本之前,设计了如下结构:

struct sds {
	int len; // buf中已占用字节数
	int free; // buf中剩余可用字节数
	char buf[]; // 数据空间
}
复制代码

由于有len的存在,读写字符串时不依赖"\0"终止符,保证了二进制安全。

2.2 动态字符串SDS

SDS是"simple dynamic string"的缩写,redis中所有场景中出现的字符串,基本都是由SDS来实现的。

一、动态字符串的数据结构

在Redis3.2中,SDS的定义如下:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;         // 当前已使用的字节数(不包括'\0')
    uint8_t alloc;       // 记录当前字节数组总共分配的字节数量(不包括'\0')
    unsigned char flags; // 标记当前字节数组的属性,是sdshdr8还是sdshdr16等
    char buf[];          // 字节数组,用于保存字符串,包括结尾空白字符'\0'
};
复制代码
  • len:记录当前已使用的字节数(不包括'\0'),获取SDS长度的复杂度为O(1)
  • alloc:记录当前字节数组总共分配的字节数量(不包括'\0')
  • flags:标记当前字节数组的属性,是sdshdr8还是sdshdr16等
  • buf:字节数组,用于保存字符串,包括结尾空白字符'\0'

针对不同的string长度,使用不同类型保存lenalloc,是Redis节省内存的一种优化。

二、动态字符串扩容机制

  • 空间预分配
为减少修改字符串带来的内存重分配次数,采用了“一次管够”的策略。

1. 当修改之后动态字符长度小于1MB,则加倍现有的空间。

2. 超过1M,扩容时一次只会多扩1M的空间。
复制代码
  • 惰性释放空间
2. 为避免缩短字符串时候的内存重分配操作,动态字符在数据减少时,并不立刻释放空间。

1. 释放内存的过程中修改len和free字段,并不释放实际占用内存。
复制代码

3. List数据结构

考虑链表的附加空间相对太高,对于64位操作系统来说,prev 和 next 指针就要占去 16 个字节 ;并且链表的每个节点都单独分配内存碎片化程度较高

在redis3.2之后List对象在底层使用quicklist(快速列表)来存储,相对链表实现来说,quicklist可以大大节省prev 和 next 指针的占用空间。

1. quicklist是由一个个ziplist组成的链表
2. 每个ziplist连续地存放多个list元素,ziplist中的每个entry块存放一个list元素。
复制代码

image.png

3.1 ziplist的数据结构

struct ziplist<T>{
    int32 zlbytes;            //压缩列表占用字节数
    int32 zltail_offset;      //最后一个元素距离起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength;           //元素个数
    T[] entries;              //元素内容
    int8 zlend;               //结束位 0xFF
}
复制代码

ziplist结构是列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的,由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存

ziplist结构

image.png

entity结构

image.png

元素遍历

  • 先找到列表尾部元素

末尾 = 起始位 + zltail_offset(到尾部偏移量)

  • 逐个遍历

Pn = Pn-1 + previous_entry_length (前一个元素的长度)

4. Hash数据结构

哈希表略微有点复杂。哈希表的制作方法一般有两种,一种是:开放寻址法,一种是拉链法。redis的哈希表的制作使用的是拉链法.

4.1 Hash结构

image.png

分为两部分:左边橘黄色部分和右边蓝色部分:

1. 左边橘黄色: dict字典记录类型相关、hash表 、rehash索引等

2. 右边蓝色: ht是hash表数组,有两个hash表。
一般字典只使ht[0]哈希表,ht[1]哈希表会在对ht[0]哈希表进行rehash(重哈希)的时候使用。
当哈希表的键值对数量超过负载数量过多的时候,会将键值对迁移到ht[1]上。
复制代码

4.2 扩容 / 收缩 / 渐进式rehash

Redis中的哈希表通过load_factor(负载因子)来决定是否扩容或者缩容。

load_factor = ht[0].used / ht[0].size

一、扩容

  • 没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于1。
  • 正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于等于5。

第一个大于等于ht[0].used * 2的2^n(2的n次方幂)。扩容时会重新进行hash值计算key的位置,然后h[0]的的数据 和 h[1]互换。

是因为bgsave或bgrewriteaof命令会fork子进程来遍历内存,若内存无变更则与父进程共享内存页,修改内存数据时需将内存页拷贝一份出来进行修改(这就是传说中的写时复制),修改内存数据不但在拷贝内存页时会消耗CPU资源,而且会消耗更多的内存空间。而扩容时需要将旧哈希表元素rehash到新哈希表,从而造成内存数据变更,因此要尽量避免扩容。

二缩容

  • 加载因子小于0.1时,程序自动开始对哈希表进行收缩操作

第一个大于等于ht[0].used的2^n(2的n次方幂)。缩容时会重新进行hash值计算key的位置,然后h[0]的的数据 和 h[1]互换。

三、渐进式rehash

哈希表在扩容和缩容时都需要rehash,由于Redis是单线程处理的,如果字典中的元素太多,一次性执行rehash操作会造成卡顿,因此Redis采用渐进式rehash的策略,在查找、插入、删除操作时,对其进行迁移。如果一直没有被访问,Redis会在定时任务中对其执行rehash。

渐进式rehash每次将重哈希至少向前推进n步(除非不到n步整个重哈希就结束了),每一步都将ht[0]上某一个bucket(即一个dictEntry链表)上的每一个dictEntry移动到ht[1]上,它在ht[1]上的新位置根据ht[1]的sizemask进行重新计算。rehashidx记录了当前尚未迁移(有待迁移)的ht[0]的bucket位置。

5. Set数据结构

  • 集合元素全为能用long声明的整数值,并且元素个素不大于512个(可通过参数set-max-intset-entries配置)时,集合数据通过intset来存储,intset中的元素按从小到大的顺序排列。

  • 其它情况下,集合数据一律使用dict(字典)来存储,字典的键为集合元素的value,字典的值为null。

5.1 insert数据结构

整数集合是集合键的底层实现方式之一。

image.png

6. Zset数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层实现使用了两个数据结构:

1. 第一个是hash
2. 第二个是跳跃列表
复制代码

6.1 跳跃列表的数据结构

image.png

跳跃列表就是类似于层级制,可以通过快速的几次查询找到对应元素。

1. 最下面一层所有的元素都会串起来。

2. 然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。

3. 然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。
复制代码

想想你老家在世界地图中的位置:亚洲-->中国->安徽省->安庆市->枞阳县->汤沟镇->田间村->xxxx号,也是这样一个类似的结构.

7. 链接

-juejin.cn/post/696436…

-juejin.cn/post/684490…

Guess you like

Origin juejin.im/post/7054963156359053349