Redis常见数据结构内部详解 | 青训营笔记

138 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天

Redis 提供了5种数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),它们的背后原理很重要,理解为什么用这个数据结构能提升性能和压缩存储。

该节笔记目录如下:常见数据结构内部

  • string
  • list
  • hash
  • zset

1 string

在Redis里面String的设计和其他语言如C++,java的设计都不太一样,它是一种二进制安全的数据结构,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
存储的时候节省空间,能够很快的把数据结构读出来,字符变更的时候很快的把数据写进去。

  • 数据结构:SDS和int

    SDS:Simple Dynamic String,简单动态字符串。Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

  • 通常和expire配合使用
  • 场景:存储计数、Session
  • 内部:
    buf:实际存储内容区,长度不够时会扩容。
    alloc:存储buf长度信息。
    len:存储buf实际用了多长信息。 flags:数据类型。
    1676626551862.png

使用场景:

  • 计数器:文章阅读量统计
  • 对象缓存,单值缓存

2 list

Redis实现了自己的链表数据结构,由一个双向链表quicklist和listpack实现。 Redis的list数据结构是怎么实现的

  • quicklist实现:
    它开辟了一块大的内存空间,所有数据等长空间,总字节(1 byte)和总共存储个数在头部记录。 1676626958054.png
    比较有特点的是entry,它又取了一个名字叫listpack 一般链表一个节点只存储一个数据,但是Redis为了节省内存一个节点上存了多个数据,给数据压缩到listpack节点上,放到一个entry里。

  • listPack实现
    1676627375453.png

使用场景:

  • 微博朋友圈,公众号列表文章展示

3 Hash数据结构dict

Redis键与值得映射关系是通过Dict(字典)来实现得。Dict由三部分组成:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。

观察下图,可以发现Dict内部实现如下:

  • dict(字典):由一个大小为2的dictht(哈希表)数组ht[2]和一个int类型的rehashidx组成
    • ht[2]: 存储两个哈希表
    • rehashidx:辅助变量,用于记录 rehash 过程的进度,以及是否正在进行 rehash 等信息。
  • dictht(哈希表):数组,数组中的每个元素指向dictEntry(哈希节点)的指针
  • dictEntry(哈希节点):包含键和值,以及指向下一个哈希表节点的指针(链式哈希)。

    键:由一个指针指向。
    值:是由一个union定义,节省内存空间。可以是一个指向实际值的指针,或者是一个无符号的 64 位整数或有符号的 64 位整数或double 类的值。当为整数ordouble时,直接内嵌,无需再使用一个指针指向。

    1676627665487.png

哈希冲突:

  • 链式哈希解决:当键冲突了,直接在给下面所有值用单向链表串起来。
  • 缺点:随着某条链表长度增加,查询耗时就会增加。
    ————> rehash解决: 链表长-->扩容-->开辟新槽位-->数据拷贝过去

rehash:

  • 为什么要rehash?
    Redis的哈希表采用了链式哈希去解决哈希冲突问题,也就是说当key冲突,直接给value用单向链表串起来就可以了,这样当链表长度过长时,查询耗时就会增加,失去了哈希O(1)查询优势。于是会做rehash,重新建一个新的哈希表来解决哈希冲突的问题。

  • rehash是什么?
    对哈希表的大小进行拓展。对每一个key采用新的计算方式重新计算索引,创建新的哈希表插入到ht[1]。

  • rehash触发条件?
    负载因子=哈希表已保存节点数量/哈希表大小

    • 负载因子>1 且没有进行持久化操作。则rehash。
    • 负载因子>=5,哈希冲突严重,无条件强制rehash。
  • rehash过程?
    对key重新计算索引-->开辟新槽位-->数据迁移。

    数据迁移:将ht[0],也就是原来的哈希表,全部迁移到新的哈希表ht[1]中。

  • rehash存在问题?
    如果原始哈希表的数据量非常大,那么在迁移新的哈希表的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。
    ————>渐进式哈希解决

渐进式rehash:
也就是:将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。

具体: ht[0]原来的hashTable,rehash发生后会初始化一个ht[1]的结构,并没有被用户访问到 。访问的时候会顺道迁移(拷贝)多个数据到ht[1],完全迁移过去后,指针会发生一个交换。ht0指到ht1,ht1指向null

核心点:

  1. 拷贝的时候不能阻塞用户
  2. rehash把拷贝平摊到了每一个用户访问的过程中

使用场景:

  • 电商购物车 key:用户id field:商品id

4 zset 有序集合 (数据结构 zskiplist)

这是因为有序集合中每个成员都会关联一个 double(双精度浮点数)类型的 score (分数值),Redis 正是通过 score 实现了对集合成员的排序。使用场景:热搜排行榜

内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的 结构可以获得比较高的查找效率,并且在实现上比较简单。
为什么使用HashMap和SkipList组合实现有序:
只使用HashMap可以O(1)复杂度查找,但无序
只使用SkipList,虽然有序,但O(logn)查找

使用了两种不同的存储结构,分别是 zipList(压缩列表)和 skipList(跳跃列表),当 zset 满足以下条件时使用压缩列表:

  • 成员的数量小于128 个;
  • 每个 member (成员)的字符串长度都小于 64 个字节。

ziplist:
每个集合元素使用两个紧挨着一起的两个压缩列表节点表示,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。

skiplist:
又称“跳表”是一种基于链表实现的随机化数据结构,其插入、删除、查找的时间复杂度均为 O(logN)。从名字可以看出“跳跃列表”,并不同于一般的普通链表,它的结构较为复杂。
通过空间换时间,时间复杂度为O(logn)。
换句话说,基于单链表实现了二分查找。
1676637657332.png

5. set 无序集合

redis集合(set)类型和list列表类型类似,都可以用来存储多个字符串元素的集合。但是和list不同的是set集合当中不允许重复的元素。而且set集合当中元素是没有顺序的,不存在元素下标。

redis的set类型是使用哈希表构造的,它支持集合内的增删改查,并且支持多个集合间的交集、并集、差集操作。可以利用这些集合操作,解决程序开发过程当中很多数据集合间的问题)

使用场景:

  • 抽奖活动
  • 朋友圈点赞

参考

  1. Redis综述篇 juejin.cn/post/709752…
  2. Redis哈希表原理 juejin.cn/post/717218…
  3. Redis zset有序集合(底层原理+图解) (biancheng.net)