type
status
date
slug
summary
tags
category
password

1、概述

Redis 的数据类型和底层结构之间的关系是 “抽象与实现” 的关系。Redis 对外提供高级的数据类型(如 String、List、Hash 等),但内部会根据数据的特点(如大小、数量、元素类型等)选择不同的底层结构来实现,以达到性能与内存的平衡。
Redis 共有五种基本数据类型:
  • String(字符串):最基本的数据类型,可以存储文本、数字或二进制数据(最大 512MB)。
  • List(列表):有序的字符串集合,基于双向链表实现,支持两端插入和删除。
  • Set(集合):无序不可重复的字符串集合,支持交集、并集、差集运算。
  • Hash(散列):键值对集合,键为不可重复的字符串,值为任意类型。
  • Sorted Set(有序集合):又称为 Zset,类似于 Set,但每个元素关联一个 score(分数),用于排序。
另外还有一些其他扩展的数据类型:
  • Bitmap(位图):2.2 版新增,类似于一个以 Bit 为单位的数组,数组的每个单元只能存储 0 和 1,用少量空间记录大量状态(状态值只能为0或者),适合二值统计
  • HyperLogLog(基数统计):2.8 版新增,用于高效估算集合的基数(不重复元素的数量)。使用固定的大小(不超过12k),用来统计海量数据中不重复的元素个数。基于伯努利算法,不会存储原始基数数据,用于基数统计,误差率约 0.81%。
  • GEO(地理空间):3.2 版新增,存储地理位置(经纬度),支持距离计算、范围查询。
  • Stream(流):5.0 版新增,发布订阅(Pub/Sub)模式的升级版,类似于 Kafka 的消息队列,提供持久化、多消费者组等功能。
这些数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这七种数据结构:
  • 简单动态字符串(SDS)
  • LinkedList(双向链表)
  • Dict(哈希表/字典)
  • SkipList(跳跃表)
  • Intset(整数集合)
  • ZipList(压缩列表)
  • QuickList(快速列表)
Redis 根据使用场景为各种数据类型选择最优的底层数据结构实现,实现性能和存储空间的最优解。

2、Redis的底层数据结构

底层数据结构
描述
对应数据类型
简单动态字符串(SDS)
用于存储字符串值和键名。
String
LinkedList(双向链表)
双端链表(3.2 版本后被 QuickList 代替)。
List
Intset(整数集合)
整数值集合,用于 Set 中只包含整数元素的场景。本质上是一块连续内存空间。
Set
Dict(哈希表/字典)
Redis 的键值对存储核心结构。
Hash,Set
SkipList(跳跃表)
用于实现有序集合(Sorted Set)。在链表基础上改进过来的,实现了一种多层的有序链表,能支持平均 O(logN) 复杂度的节点查找。
Zset
ZipList(压缩列表)
用于列表(List)、哈希(Hash)和有序集合(Sorted Set)的小数据存储。为了节约内存的设计,连续内存块组成的顺序型数据结构。
List,Hash,Zset
QuickList(快速列表)
3.2 版引入,QuickList 是 ZipList 和 LinkedList 的混合体,是由多个 ZipList 组成的双向链表
List

2.1 简单动态字符串(SDS)

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。例如下图就是字符串 xiaolin 的 char* 字符数组的结构:
notion image
在 C 语言里,对字符串操作时,char* 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用 \0 表示,意思是指字符串的结束。因此,C 语言标准库中的字符串操作函数就通过判断字符是不是 \0 来决定要不要停止操作,如果当前字符不是 \0 ,说明字符串还没结束,可以继续操作,如果当前字符是 \0 是则说明字符串结束了,就要停止操作。C 语言的字符数组的缺点是:
  • 获取字符串长度的时间复杂度是 O(N),对字符串的操作效率不高。
  • 字符串的结尾是以 \0 字符标识,字符串里面不能包含有 \0 字符,因此不能保存二进制数据。
  • 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止。
Redis 中的字符串没有使用 C 语言自带的字符数组,而是自定义了 SDS 类型。这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 SDS, 否则的话, 它就是一个 long 类型的值。
notion image
  • len:已使用长度,len属性可以实现字符串长度O(1)事件的计算。
  • free:未使用长度,free属性可以用于动态扩容。
  • buf:实际存储字符串的数组
使用 SDS 的优点:
  • O(1)时间复杂度计算字符串长度。因为有专门的 len 成员变量来记录长度。
  • 二进制安全。因为 SDS 不需要用 \0 字符来标识字符串结尾了,所以可存储包含 \0 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 \0 字符。
  • 支持动态扩容。使用append命令追加字符串时,假设 SDS_MAX_PREALLOC 默认大小是1M。如果新字符串的总长度小于 SDS_MAX_PREALLOC,就为字符串分配 2 倍于所需长度(新字符串的长度)的空间;否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
使用 SDS 的缺点:
  • 如果记录的值长度很短,元数据的内存开销可能会比字符串本身开销还大。

2.2 LinkedList(双向链表)

c语言底层没有链表结构,redis本身实现了一个双端链表结构。
notion image
由上图可以看到,linkedlist结构分为两部分:
  • list:用来持有链表,可以迅速获取头尾节点,计算节点数等。
    • 跳表的特点:
    • 在同一个跳表中,多个节点可以包含相同的分值(score),但是每个节点的成员对象(ele)的必须唯一。
    • 跳表节点按照分值(score)的大小进行排序,当分值(score)相同时,节点按照成员对象(ele)的大小进行排序。
    • level 上的前进指针可以跳过多个节点,但是跳表节点的后退指针只能返回前一个节点。
    • head:表头指针。
    • tail:表尾指针。
    • len:链表长度。
    • dup 函数用于复制链表节点所保存的值。
    • free 函数用于释放链表节点所保存的值。
    • match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。(dup 、 free 和 match 用于实现多态链表所需的类型特定函数。)
  • listNode:包含具体的节点内容
    linkedlist 的优点:
    • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)。
    • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
    • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值。
    linkedlist 的缺点:
    • 链表每个节点之间的内存都是不连续的,无法很好利用 CPU 缓存。
    • 不管保存节点数多少,哪怕保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。

    2.3 Intset(整数集合)

    整数集合(intset)是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,就会使用整数集这个数据结构作为底层实现。 整数集合本质上是一块连续内存空间。
    notion image
    • encoding:整数集合的实际类型。
      • 如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组中每一个元素的类型都是 int16_t。
      • 如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组中每一个元素的类型都是 int32_t。
      • 如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组中每一个元素的类型都是 int64_t。
    • length:整数集合包含的元素数量,也就是contents数组的长度
    • contents:整数数组,各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但 contents 数组的真正类型取决于 encoding 属性的值。
    整数集合升级过程:当向一个底层为 int16_t 数组的整数集合添加一个 int64_t 类型的整数值时, 整数集合已有的所有元素都会被转换成 int64_t 类型, 所以 contents 数组保存的四个整数值都是 int64_t 类型的。

    2.4 Dict(哈希表/字典)

    哈希表是一种保存键值对(key-value)的数据结构。
    notion image
    dict的结构由三部分组成:
    • dict:dict的整体结构,底层核心是两个dictht(dict hash table)。平时只有ht[0]是有内容的,ht[1]是null。ht[1] 只会在对 ht[0] 进行 rehash 时使用。
      • dictht:dictEntry数组结构,底层核心是dictEntry元素
        • size 属性记录了哈希表的大小, 也即是 table 数组的大小
        • used 属性则记录了哈希表目前已有节点(键值对)的数量。
      • dictEntry:真正存储节点元素的结构。不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
        Hash 冲突时候 rehash 过程是怎样的?
        Redis 采用了「链式哈希」的方法来解决哈希冲突,但是在哈希冲突比较严重的情况下,还是会发生rehash。rehash的过程:
        1. 新建一个ht[1],容量一般为ht[0]的两倍以上。
        1. 把ht[0]的元素搬运到ht[1],最后用ht[1]替换ht[0]。
        1. 原来的h[0]清空元素,然后把原来的ht[0]改成ht[1]
        如果ht[0]的数据量非常大,那么在迁移至ht[1]的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
        1. 新建一个ht[1],容量一般为ht[0]的两倍以上。
        1. 每次往集合find、add、delete等操作之前,先把ht[0]的第一个不为空的bucket上的所有元素搬运到ht[1],然后再进行这次命令操作。
        1. 随着处理客户端发起的哈希表操作请求数量越多,最终会在某个时间点将ht[0]的所有元素迁移到ht[1]。
        1. 然后同普通rehash操作。
        触发rehash的条件:used/size 的值称为负载因子,当负载因子满足某种条件时会进行rehash
        1. used/size>1时,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,会进行渐进式rehash。
        1. used/size>5时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,会进行强制rehash。

        2.5 SkipList(跳跃表)

        链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N)。跳表相当于在链表的基础上加了多层稀疏索引,查找过程就是在多级索引上跳来跳去,最后定位到元素。
        为什么要使用跳表?
        1. 查找、插入、删除效率高,平均 O(log n),最坏情况 O(n)
        1. 相比平衡树实现更简单
        1. 并发环境下更容易实现(Redis 单线程不需要考虑)
        notion image
        zkiplist 结构分为两部分:
        • zskiplist:持有 zkiplist 的整体结构。虽然多个 zskiplistnode 就可以组成一个跳表,但是通过 zkiplist 可以更方便进行整体操作。
          • header:指向表头节点(头节点不是一个有效节点,没有成员对象)
          • tail:指向表尾节点
          • level:跳表内层数最大的的节点的层数(不包含头节点,因为头节点都是32层)
          • length:跳表的节点数(不包含头节点)
        • zskiplistnode:真正保存元素的数据结构。
          • level:zskiplist Level 结构的数组,这是实现链表层级关系的关键。level 可能包含多个元素,每一个元素对应代表跳表的一层。每次创建一个新跳表节点的时候,会随机生成一个 1 到 32 的随机数作为 level 数组的大小,这个大小也就是 level 的层高。每个 level 元素都包含两部分「前进指针」和「跨度」。
            • 前进指针:指向表尾方向的下一个跳表节点。
            • 跨度:用来记录两个节点之间的距离,实际上是为了计算这个节点在跳表中的排位。计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。
          • backward:后退指针,后退指针只能指向相邻的前一个节点
          • score:分值,跳表中的元素按照 score 从小到大的顺序排序,对应 Zset 对象的元素权重。
          • ele:成员对象,用 sds 保存,对应 Zset 对象的元素
        特点:
        • 在同一个跳表中,多个节点可以包含相同的分值(score),但是每个节点的成员对象(ele)的必须唯一。
        • 跳表节点按照分值(score)的大小进行排序,当分值(score)相同时,节点按照成员对象(ele)的大小进行排序。
        • level 上的前进指针可以跳过多个节点,但是跳表节点的后退指针只能返回前一个节点。
        跳表的插入过程
        1. 确定插入层数(随机化)
            • 插入一个新节点时,首先通过随机算法(如抛硬币)决定该节点的层数(高度)。
            • 从第 0 层开始,每次“抛硬币”若为正面(概率通常为 1/2),则增加一层,直到抛出反面为止。
        1. 搜索插入位置
            • 从跳表的最高层(当前存在的最高层)开始,向右遍历,找到每一层中最后一个小于待插入值的节点(即插入位置的前驱节点)。
            • 记录这些前驱节点到一个数组 update 中,供后续链接使用。
        1. 创建新节点:根据随机层数创建新节点,并为每一层初始化指针。
        1. 链接新节点:从第 0 层到新节点的最高层,依次更新指针:
            • 新节点的 next 指向 update[i] 的 next
            • update[i] 的 next 指向新节点。
        1. 更新跳表高度(可选):如果新节点的层数超过当前跳表的最大高度,更新跳表的最大高度。
        跳表的查询过程
        查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
        • 如果当前节点的分值「小于」要查找的分值时,跳表就会访问该层上的下一个节点。
        • 如果当前节点的分值「等于」要查找的分值时,并且当前节点的成员对象「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
        • 如果上面两个条件都不满足,或者下一个节点为NULL时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针(注意是下一层指针,比如当前是level2,跳到level1指针查找),然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
        • 当level已经到最底层,当前节点的分值「小于」要查找的分值时,下一个节点为NULL或者下一个节点的分值「大于或等于」要查找的分值的时候,该位置就是要查找的位置,查找结束。
        notion image
        例如图中skiplist总共包含4层索引,假如从下到上分别为level1~4,查找数字23的位置:
        1. 从头节点的level4开始查找,找到数字7节点,7 < 23,所以继续查找数字7节点level4的下一节点。
        1. 下一节点为NULL,返回数字7节点,查找数字7节点level3的下一节点。
        1. 下一节点为数字37节点,37>23,返回数字7节点,查找数字7节点level2的下一节点。
        1. 下一节点为数字19节点,19<23,所以继续查找数字19节点level2的下一节点。
        1. 下一节点为数字37节点,37>23,返回数字9节点,查找数字19节点level1的下一节点。
        1. 下一节点为数字22节点,22<23,所以继续查找数字22节点level1的下一节点。
        1. 下一节点为数字26节点,26>23,但level已经到最底层,所以数字23的最终位置为数字22和26之间。
        跳表节点层数设置:跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。

        2.6 ZipList(压缩列表)

        压缩列表是redis为了节约内存设计的数据结构,本质是一块连续内存的数据结构,有点类似于数组。
        notion image
        • zlbytes:整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
        • zltail:压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
        • zllen:压缩列表包含的节点数量。
        • entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
        • zlend:特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
        entry用于存储实际数据,包含三部分内容:
        • prevlen,记录了「前一个节点」的长度;
        • encoding,记录了当前节点实际数据的类型以及长度;
        • data,记录了当前节点的实际数据;
        当我们往压缩列表中插入数据时,压缩列表就会根据数据是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。

        2.7 QuickList(快速列表)

        Quicklist 是 Redis 3.2 版本引入的一种高级数据结构,作为 List 类型的底层实现,它结合了双向链表压缩列表(ziplist)的优点,在内存使用效率和操作性能之间取得了很好的平衡。
        Quicklist 本质上是一个由多个 ziplist 组成的双向链表:
        • 每个链表节点(quicklistNode)包含一个 ziplist
        • 整个结构通过指针连接形成双向链表
        这种混合设计带来了两大优势:
        1. 内存效率:ziplist 的紧凑存储减少了传统链表每个元素需要单独分配内存的开销
        1. 操作性能:保持了链表在插入/删除操作上的优势,同时通过 ziplist 提高了内存局部性

        3、Redis对象

        上面介绍了Redis的主要底层数据结构,包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表等。但是Redis并没有直接使用这些数据结构作为基本数据类型使用,而是基于这些数据结构创建了一些对象类型,也是我们熟悉的五大基本数据类型:字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set)。 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个是字符串对象类型的键对象, 另一个是各种数据类型之一的值对象。每个值对象都由一个 redisObject 结构表示:
        • type:对象类型,包含五大对象类型
          • REDIS_STRING:字符串对象
          • REDIS_LIST:列表对象
          • REDIS_HASH:哈希对象
          • REDIS_SET:集合对象
          • REDIS_ZSET:有序集合对象
        • encoding:编码方式,也就是底层数据结构。
          • REDIS_ENCODING_INT:long 类型的整数
          • REDIS_ENCODING_EMBSTR:embstr 编码的简单动态字符串
          • REDIS_ENCODING_RAW:简单动态字符串
          • REDIS_ENCODING_HT:字典
          • REDIS_ENCODING_LINKEDLIST:双端链表
          • REDIS_ENCODING_ZIPLIST:压缩列表
          • REDIS_ENCODING_INTSET:整数集合
          • REDIS_ENCODING_SKIPLIST:跳跃表和字典
        • refcount:引用计数,用于内存回收和对象共享。当refcount为0时会进行对象回收,当新增一个键来共享已经存在的对象时,refcount加1。注意只有整数对象支持共享。
        • ptr:指向底层实现数据结构的指针
        注意type和encoding的区别:每种对象类型底层会有多种数据结构的实现
        类型
        编码
        对象
        条件
        REDIS_STRING
        REDIS_ENCODING_INT
        使用整数值实现的字符串对象。
        保存的是整数值, 并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long )
        REDIS_ENCODING_EMBSTR
        使用 embstr 编码的简单动态字符串实现的字符串对象。
        保存的是字符串且长度<=44字节,只分配一次内存,在一块连续的内存空间中依次存储 redisObject 和 sdshdr 两个结构
        REDIS_ENCODING_RAW
        使用简单动态字符串实现的字符串对象。
        保存的是字符串且长度>44字节,分配两次内存,分别存储redisObject 和 sdshdr 两个结构
        REDIS_LIST
        REDIS_ENCODING_ZIPLIST
        使用压缩列表实现的列表对象。
        当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码: 1. 列表对象保存的所有字符串元素的长度都小于 64 字节。 2. 列表对象保存的元素数量小于 512 个。
        REDIS_ENCODING_LINKEDLIST
        使用双端链表实现的列表对象。
        不满足以上任意条件
        REDIS_HASH
        REDIS_ENCODING_ZIPLIST
        使用压缩列表实现的哈希对象。
        当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码: 1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节。 2. 哈希对象保存的键值对数量小于 512 个。
        REDIS_ENCODING_HT
        使用字典实现的哈希对象。
        不满足以上任意条件
        REDIS_SET
        REDIS_ENCODING_INTSET
        使用整数集合实现的集合对象。
        当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码: 1. 集合对象保存的所有元素都是整数值。 2. 集合对象保存的元素数量不超过 512 个。
        REDIS_ENCODING_HT
        使用字典实现的集合对象。
        不满足以上任意条件
        REDIS_ZSET
        REDIS_ENCODING_ZIPLIST
        使用压缩列表实现的有序集合对象。
        当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码: 1. 有序集合保存的所有元素成员的长度都小于 64 字节。 2. 有序集合保存的元素数量小于 128 个。
        REDIS_ENCODING_SKIPLIST
        使用跳跃表和字典实现的有序集合对象。
        不满足以上任意条件。

        4、Redis的数据类型

        类型
        描述
        常用命令
        应用场景
        String(字符串)
        最基本的数据类型,可以存储文本、数字或二进制数据(最大 512MB)。
        SETGETINCRDECRAPPENDSTRLEN
        缓存、计数器、分布式锁
        List(列表)
        有序的字符串集合,基于双向链表实现,支持两端插入和删除。
        LPUSHRPUSHLPOPRPOPLRANGELLEN
        消息队列、最新消息列表、历史记录、粉丝列表、文章的评论列表
        Set(集合)
        无序不可重复的字符串集合,支持交集、并集、差集运算。
        SADDSMEMBERSSISMEMBERSINTERSUNIONSDIFFSCARD
        标签系统、共同好友、去重数据
        Hash(哈希表)
        键值对集合,键为不可重复的字符串,值为任意类型
        HSETHGETHMSETHMGETHGETALLHDELHKEYS
        存储对象(如用户信息、商品详情)
        Sorted Set(有序集合)
        类似于 Set,但每个元素关联一个 score(分数),用于排序。
        ZADDZRANGEZREVRANGEZSCOREZRANKZREMZCARD
        排行榜、优先级队列、带权重的数据存储
        Bitmap(位图)
        2.2 版新增,类似于一个以 Bit 为单位的数组,数组的每个单元只能存储 0 和 1,用少量空间记录大量状态(状态值只能为0或者),适合二值统计
        SETBITGETBITBITCOUNTBITOP
        布隆过滤器、用户在线状态、签到统计
        HyperLogLog(基数统计)
        2.8 版新增,用于高效估算集合的基数(不重复元素的数量)。使用固定的大小(不超过12k),用来统计海量数据中不重复的元素个数。基于伯努利算法,不会存储原始基数数据,用于基数统计,误差率约 0.81%。
        PFADDPFCOUNTPFMERGE
        UV统计、大规模去重计数
        GEO(地理空间)
        3.2 版新增,存储地理位置(经纬度),支持距离计算、范围查询。
        GEOADDGEOPOSGEODISTGEORADIUSGEOSEARCH
        附近的人、地理位置搜索
        Stream(流)
        5.0 版新增,发布订阅(Pub/Sub)模式的升级版,类似于 Kafka 的消息队列,提供持久化、多消费者组等功能。
        XADDXREADXGROUPXRANGEXACK
        事件日志、消息队列、实时数据处理
        另外,Redis 的发布订阅(Pub/Sub)模式是一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。发布订阅模式并不是一种数据类型,存在的缺点:
        • 没有ack机制
        • 阻塞式的,一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。
        • 没有持久化功能,消息发送后没有消费者消费就会丢失。
        Redis 5.0 推出的 Stream 是基于发布订阅(Pub/Sub)模式的升级版,提供持久化、可靠消费、回溯消息等功能。Pub/Sub 适合轻量级场景,而 Stream 适合需要保证消息不丢失的复杂场景。

        4.1 String

        String 类型的底层的数据结构实现主要是整数集合和 SDS(简单动态字符串)。字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr
        • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int
          • notion image
        • 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr, embstr编码是专门用于保存短字符串的一种优化编码方式。
          • notion image
        • 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw
          • notion image
        可以看到embstrraw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObjectSDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObjectSDS。Redis这样做会有很多好处:
        • embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;
        • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;
        • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
        但是 embstr 也有缺点的:
        • 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

        4.2 List

        List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
        列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。
        List 类型的底层数据结构是由双向链表或压缩列表实现的:
        • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
          • notion image
        • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
          • notion image
        Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。

        4.3 Hash

        Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。
        Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
        • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
          • 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。
          • 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
          • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
          • notion image
        • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
          • notion image
            notion image
        在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

        4.4 Set

        Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
        一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
        Set 类型的底层数据结构是由哈希表或整数集合实现的:
        • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
          • notion image
        • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
          • notion image

        4.5 Zset

        Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。
        有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
        Zset 类型的底层数据结构是由压缩列表或跳表实现的:
        • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
        notion image
        • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象,这两个数据结构一个是 skiplist/ziplist,一个是 dict。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。一个 zset 结构同时包含一个字典和一个跳跃表:
          • zsl 跳跃表按score从小到大保存了所有集合元素,完成排序功能
          • dict 字典则为所有集合元素创建了一个从成员到分值的映射,完成O(1)的查找功能。
          • skiplist的obj指针和dict的key指针是指向同一个对象,也就是数据只会保存一份。
          • notion image
        在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

        4.6 BitMap

        Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
        由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
        Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
        notion image

        4.7 HyperLogLog

        Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
        所以,简单来说 HyperLogLog 提供不精确的去重计数
        HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。
        在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
        这什么概念?举个例子给大家对比一下。
        用 Java 语言来说,一般 long 类型占用 8 字节,而 1 字节有 8 位,即:1 byte = 8 bit,即 long 数据类型最大可以表示的数是:2^63-1。对应上面的2^64个数,假设此时有2^63-1这么多个数,从 0 ~ 2^63-1,按照long以及1k = 1024 字节的规则来计算内存总数,就是:((2^63-1) * 8/1024)K,这是很庞大的一个数,存储空间远远超过12K,而 HyperLogLog 却可以用 12K 就能统计完。

        4.8 GEO

        Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
        在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。
        GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
        GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
        这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

        4.9 Stream

        Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
        在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
        • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
        • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。
        基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
         
        Redis系列:键空间和内存策略分布式锁的实现汇总
        Loading...