Redis基本数据结构的底层实现(总结版)

string 字符串

字符串对象的实现有3种编码:INT、RAW和EMBSTR。

  • 当字符串对象保存的值为整数值,且在long类型表示范围内,那么使用INT编码。此时,整数值会被保存到字符串对象中的ptr属性。
  • 当字符串对象保存的值为字符串值,且占用字节大于44字节,那么使用RAW编码。此时,使用SDS来保存字符串值。
  • 当字符串对象保存的值为字符串值,且占用字节小于等于44字节,那么使用EMBSTR编码。此时,也是使用SDS来保存字符串值。不同的是,对于EMBSTR编码来说,SDS和redisObject对象共用一块连续的内存空间。

为什么string决定使用RAW编码或EMBSTR编码,是以44字节为分界?

当字符串值最大占用字节为44字节,加上SDS其他部分占用的字节以及redisObject占用的字节,那么整个string对象最大占用字节为64字节。

redis的空间是以2的n次方来进行分配的,也就是说,这样可以减少内存碎片的产生。

EMBSTR编码和RAW编码都是使用SDS来保存字符串值,那么它们之间有什么区别?

  1. 对于RAW编码来说,创建字符串对象需要进行两次内存分配。而EMBSTR编码,只需要执行一次内存分配即可。
  2. 对于RAW编码来说,字符串对象的释放需要调用两次内存释放函数。而EMBSTR编码,只需要调用一次。
  3. EMBSTR编码通过将redisObject和SDS放到一块连续的内存空间,可以减少内存碎片的产生。

字符串编码之间的转换是怎么样的?

INT编码和EMBSTR编码在满足条件的情况下,都可以转换成RAW编码。

  • 当字符串对象进行修改操作使得保存的字符串值不再是整数值。那么redis就会将INT编码转换成RAW编码。
  • 对于EMBSTR编码来说,redis并没有为它编写对应的修改程序。也就是EMBSTR编码本质上来说是只读,那么当对采用EMBSTR编码的字符串对象,redis会先将EMBSTR编码转换成RAW编码,再执行修改命令。所以,只要EMBSTR编码的字符串对象进行了修改,无论如何都会变成RAW编码的字符串对象。

hash 哈希

哈希使用压缩列表ziplist或字典dict来作为底层实现。

  • 若哈希使用ziplist作为底层实现。当需要插入一个元素时,元素的键会被作为压缩列表一个节点插入队尾,然后再将元素的值作为压缩列表一个节点插入到队尾。
  • 若哈希使用字典dict作为底层实现。哈希对象的每一个键值对都使用一个字典键值对来进行保存。

哈希什么时候使用ziplist或dict来作为底层实现?

当哈希使用ziplist来作为底层实现时,对哈希的操作都需要遍历压缩列表才能够找到对应的键值对。因此,ziplist只会在哈希内的元素不多并且元素占用字节数不大的情况下才会使用。否则,使用dict来作为底层实现。

list 列表

在redis 3.2以前,list使用ziplist或linkedlist来作为底层实现。

在redis 3.2以后,list统一使用快速列表quicklist来作为底层实现。

那么在redis 3.2以前,列表什么时候使用ziplist或linkedlist来作为底层实现?

当列表使用ziplist作为底层实现时,对元素的查找需要遍历整个列表,效率很低。因此,当元素占用字节数较小且元素不多的情况下才会使用ziplist。否则,使用linkedlist。

set 集合

set集合使用intset或dict来作为底层实现。

  • 当set使用intset来作为底层实现时,集合每个元素都存储到intset中。
  • 当set使用dict来作为底层实现时,集合的每个元素都作为字典的键来进行存储,对应的值设置为NULL。

set什么时候使用intset或dict来作为底层实现?

当set存储元素都是整数值且元素不多时,才使用intset来作为底层实现。否则使用dict。

inset内存储的元素都是有序的,为什么set要存储元素不多时才使用intset来作为底层实现?

intset内存储的元素都是有序的,这样对intset进行查找时,通过二分法也能很快找到需要的元素。即使,这比不上dict查找元素O(1)的时间复杂度。

之所以,set要元素不多的时候才使用intset来作为底层实现的原因如下:

  1. 当intset存储的元素很多时,intset需要占用的内存也越大。而且intset是使用数组来存储这些元素的,那么申请一块较大的连续的内存空间的操作会比较耗时。
  2. intset使用了升级机制,这样intset使用更加灵活,并且能够节省内存。这样,若intset存储过多的元素,每个元素都需要放到新申请的空间的合适位置,这样的操作也会比较耗时。

zset 有序集合

有序集合使用ziplist 或 skiplist+dict来作为底层实现。

  • 若有序集合使用ziplist来作为底层实现。有序集合的每个元素使用两个紧挨着的压缩列表节点来进行存储,第一个节点存储元素的成员,第二个节点存储元素的分值。并且压缩列表中的元素按元素的分值从小到大进行排序,分值较小的元素被放到压缩列表的表头方向。
  • 当有序集合使用skiplist和dict来作为底层实现时,skiplist中的每个节点都会存储有序集合的元素,字典键值对的键存储元素的成员,字典键值对的值存储元素的分值。这样,通过skiplist结构,有序集合可以有效的对集合进行范围性操作;通过dict结构,有序集合可以通过O(1)的时间复杂度来完成对元素分值的查找操作。

为什么zset要使用skiplist+dict两个数据结构来实现有序集合?

虽然说zset可以单独使用skiplist或dict来实现有序集合,但相比于同时使用两个数据结构来实现有序集合,性能会有所下降。

当zset使用skiplist来单独实现有序集合时,虽然有序集合执行范围性操作的优点得以保留,但是查找元素分值的操作需要遍历元素才能完成。

当zset使用dict来单独实现有序集合时,虽然有序集合查找元素分值的时间复杂度为O(1),但是执行范围性操作时需要将所有元素进行排序。

而通过同时使用两个数据结构来实现有序集合,可以兼具两个的优点,使得对有序集合的操作更加高效。

zset使用skiplist+dict两个数据结构来实现,那么不会很浪费内存吗?

不会的。这是因为对于共同的分值和成员,skiplist和dict通过指针来共享。通过这样的方式,相同的元素不需要存储两份,从而能够节省内存。

那么zset在什么时候决定使用ziplist或 skiplist+dict来实现?

当zset内的元素占用字节数不多且元素数量不多时,才使用ziplist来作为底层实现。否则使用skiplist+dict。

相关推荐
许野平21 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
ketil272 小时前
Ubuntu 安装 redis
redis
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod2 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。3 小时前
Spring Boot 配置文件
java·spring boot·后端
王佑辉3 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
杜杜的man3 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*3 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu3 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
Karoku0663 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix