Redis中的数据结构及其底层

String:

底层实现:int 和 SDS(简单动态字符串)

常用场景:存储对象、计数场景(点赞、转发、库存数量)、分布式锁、共享session信息

String内部三种编码格式:

int:如果是整数值且能用long表示,会将其存在ptr属性并将类型设置为int

embstr:如果是字符串且长度<=32字节,用sds保存,将类型设置为embstr

raw:如果是字符串且长度>32字节,用sds保存,将类型设置为raw

注:embstr与raw界限:redis2.+是32 redis3.0-4.0是39 redis5.0是44

embstr与raw区别:

embstr是分配一块内存区域保存redisObject和SDS,raw分配两块

embst实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令

SDS(动态字符串):

结构:

  • len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。

  • alloc,分配给字符数组的空间长度。在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足,会自动将 SDS 的空间扩展然后才执行修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen,如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB

  • flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。保存大小不同的字符串,节省内容空间

  • buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。

SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
  • SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

List:

底层:双向链表+压缩列表->quicklist

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;

应用场景:List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。

压缩列表:

表头结构:

  • zlbytes,记录整个压缩列表占用对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)

节点结构:

  • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
  • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
  • data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;

压缩列表的缺点:

每个节点存储有前一个节点的长度、当前节点数据存放的数据的类型数据

  • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
  • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

所以,若前一个节点恰好由253字节变为254字节,那么可能会触发连锁更新,连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

双向链表:

链表结构:头指针、尾指针、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数

节点结构:链表头指针 head、链表尾节点 tail、节点值

quicklist:

quicklist 就是「双向链表 + 压缩列表」组合:一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表

通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能

Hash:

Hash 是一个键值对(key - value)集合

内部实现:压缩列表+hash表->listpack+hash表

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。

应用场景:缓存对象、购物车

hash表:

能以 O(1) 的复杂度快速查询数据。将 key 通过 Hash 函数的计算,就能定位数据在表中的位置,因为哈希表实际上是数组,所以可以通过索引值快速查询到数据。

可能会发生哈希冲突

采用链式法解决哈希冲突,但是链表变长了以后,查询效率会下降,所以采用rehash进行扩容

listpack:

listpack中每个节点存放的数据:

  • encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
  • data,实际存放的数据;
  • len,encoding+data的总长度;

可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。

Set:

无序并唯一的键值集合,可以进行交集、并集、差集的运算(Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞

底层实现:整数集合+hash表

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;如果不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

使用场景:点赞、共同关注、抽奖

整数集合:

  • encoding 属性值有三种: INTSET_ENC_INT16、 INTSET_ENC_INT32、INTSET_ENC_INT64,对应不同的数组类型int16_t 类型的数组、 int32_t、 int64_t;

整数集合的特点:在升级时,不会新建一个数组,而是扩容现有的数组,比如说都是int_16类型的元素,加入一个32的,那么数组就会扩容为int_32类型的

Zset:

Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值)

底层实现:压缩列表+跳表 -> listpack+跳表 其实也有一个哈希表,只是用来常数复杂度获取元素权重

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;如果不满足,Redis 会使用跳表作为 Zset 类型的底层数据结构;

应用场景:排行榜、排序场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值