type
status
date
slug
summary
tags
category
icon
password
1、概述
Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合),另外还有一些其他扩展的数据类型。
这些数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 7 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
Redis根据使用场景为各种基本数据类型选择最优的底层数据结构实现,实现性能和存储空间的最优解。
2、Redis底层数据结构
底层数据结构 | 描述 | 对应数据类型 |
简单动态字符串(SDS) | redis自定义字符数组 | String |
链表(linkedlist) | redis自定义双端链表 | List |
整数集合(intset) | 整数值集合,本质上是一块连续内存空间。只有Set的元素全部为整数的时候才会用到intset。 | Set |
字典(dict) | 键值对(key-value)数据结构。 | Hash,Set |
跳跃表(skiplist) | 在链表基础上改进过来的,实现了一种「多层」的有序链表,能支持平均 O(logN) 复杂度的节点查找。只有在 Zset 对象的底层实现用到了跳表。 | Zset |
压缩列表(ziplist) | 为了节约内存的设计,连续内存块组成的顺序型数据结构。Redis 对象(List 、Hash、Zset )包含的元素数量较少,或者元素值不大的情况就会使用ziplist作为底层数据结构。 | List,Hash,Zset |
2.1 SDS
C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。例如下图就是字符串
xiaolin
的 char* 字符数组的结构:在 C 语言里,对字符串操作时,char* 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用
\0
表示,意思是指字符串的结束。因此,C 语言标准库中的字符串操作函数就通过判断字符是不是 \0
来决定要不要停止操作,如果当前字符不是 \0
,说明字符串还没结束,可以继续操作,如果当前字符是 \0
是则说明字符串结束了,就要停止操作。C 语言的字符数组的缺点是:- 获取字符串长度的时间复杂度是 O(N),对字符串的操作效率不高。
- 字符串的结尾是以
\0
字符标识,字符串里面不能包含有\0
字符,因此不能保存二进制数据。
- 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止。
Redis 中的字符串没有使用 C 语言自带的字符数组,而是自定义了
SDS
类型。这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 SDS
, 否则的话, 它就是一个 long 类型的值。- 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本身实现了一个双端链表结构。
由上图可以看到,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 对象只包含整数值元素,就会使用整数集这个数据结构作为底层实现。 整数集合本质上是一块连续内存空间。
- 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)的数据结构。
dict的结构由三部分组成:
- dict:dict的整体结构,底层核心是两个dictht(dict hash table)。平时只有ht[0]是有内容的,ht[1]是null。ht[1] 只会在对 ht[0] 进行 rehash 时使用。
- dictht:dictEntry数组结构,底层核心是dictEntry元素
- size 属性记录了哈希表的大小, 也即是 table 数组的大小
- used 属性则记录了哈希表目前已有节点(键值对)的数量。
- dictEntry:真正存储节点元素的结构。不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
2.4.1 rehash过程
Redis 采用了「链式哈希」的方法来解决哈希冲突,但是在哈希冲突比较严重的情况下,还是会发生rehash。rehash的过程:
- 新建一个ht[1],容量一般为ht[0]的两倍以上。
- 把ht[0]的元素搬运到ht[1],最后用ht[1]替换ht[0]。
- 原来的h[0]清空元素,然后把原来的ht[0]改成ht[1]
如果ht[0]的数据量非常大,那么在迁移至ht[1]的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
- 新建一个ht[1],容量一般为ht[0]的两倍以上。
- 每次往集合find、add、delete等操作之前,先把ht[0]的第一个不为空的bucket上的所有元素搬运到ht[1],然后再进行这次命令操作。
- 随着处理客户端发起的哈希表操作请求数量越多,最终会在某个时间点将ht[0]的所有元素迁移到ht[1]。
- 然后同普通rehash操作。
触发rehash的条件:used/size 的值称为负载因子,当负载因子满足某种条件时会进行rehash:
- used/size>1时,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,会进行渐进式rehash。
- used/size>5时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,会进行强制rehash。
2.5 跳表(zkiplist)
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N)。跳表相当于在链表的基础上加了多层稀疏索引,查找过程就是在多级索引上跳来跳去,最后定位到元素。
zkiplist结构分为两部分:
- zskiplist:持有zkiplist的整体结构。虽然多个zskiplistnode就可以组成一个跳表,但是通过zkiplist可以更方便进行整体操作。
- header:指向表头节点(头节点不是一个有效节点,没有成员对象)
- tail:指向表尾节点
- level:跳表内层数最大的的节点的层数(不包含头节点,因为头节点都是32层)
- length:跳表的节点数(不包含头节点)
- zskiplistnode:真正保存元素的数据结构。
- level:zskiplistLevel 结构的数组,这是实现链表层级关系的关键。level 可能包含多个元素,每一个元素对应代表跳表的一层。每次创建一个新跳表节点的时候,会随机生成一个 1 到 32 的随机数作为 level 数组的大小,这个大小也就是 level 的层高。每个 level 元素都包含两部分「前进指针」和「跨度」。
- 前进指针:指向表尾方向的下一个跳表节点。
- 跨度:用来记录两个节点之间的距离,实际上是为了计算这个节点在跳表中的排位。计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。
- backward:后退指针,后退指针只能指向相邻的前一个节点
- score:分值,跳表中的元素按照score从小到大的顺序排序,对应 Zset 对象的元素权重。
- ele:成员对象,用 sds 保存,对应 Zset 对象的元素
跳表的特点:
- 在同一个跳表中,多个节点可以包含相同的分值(score),但是每个节点的成员对象(ele)的必须唯一。
- 跳表节点按照分值(score)的大小进行排序,当分值(score)相同时,节点按照成员对象(ele)的大小进行排序。
- level 上的前进指针可以跳过多个节点,但是跳表节点的后退指针只能返回前一个节点。
2.5.1 跳表的查询过程
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
- 如果当前节点的分值「小于」要查找的分值时,跳表就会访问该层上的下一个节点。
- 如果当前节点的分值「等于」要查找的分值时,并且当前节点的成员对象「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
- 如果上面两个条件都不满足,或者下一个节点为NULL时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针(注意是下一层指针,比如当前是level2,跳到level1指针查找),然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
- 当level已经到最底层,当前节点的分值「小于」要查找的分值时,下一个节点为NULL或者下一个节点的分值「大于或等于」要查找的分值的时候,该位置就是要查找的位置,查找结束。
例如图中skiplist总共包含4层索引,假如从下到上分别为level1~4,查找数字23的位置:
- 从头节点的level4开始查找,找到数字7节点,7 < 23,所以继续查找数字7节点level4的下一节点。
- 下一节点为NULL,返回数字7节点,查找数字7节点level3的下一节点。
- 下一节点为数字37节点,37>23,返回数字7节点,查找数字7节点level2的下一节点。
- 下一节点为数字19节点,19<23,所以继续查找数字19节点level2的下一节点。
- 下一节点为数字37节点,37>23,返回数字9节点,查找数字19节点level1的下一节点。
- 下一节点为数字22节点,22<23,所以继续查找数字22节点level1的下一节点。
- 下一节点为数字26节点,26>23,但level已经到最底层,所以数字23的最终位置为数字22和26之间。
跳表节点层数设置:跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
2.6 压缩列表(ziplist)
压缩列表是redis为了节约内存设计的数据结构,本质是一块连续内存的数据结构,有点类似于数组。
- zlbytes:整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
- zltail:压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
- zllen:压缩列表包含的节点数量。
- entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
- zlend:特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
entry用于存储实际数据,包含三部分内容:
- prevlen,记录了「前一个节点」的长度;
- encoding,记录了当前节点实际数据的类型以及长度;
- data,记录了当前节点的实际数据;
当我们往压缩列表中插入数据时,压缩列表就会根据数据是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。
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、五种基本数据类型
类型 | 描述 | 应用场景 |
string | 字符串,最常用的类型 | 适用于绝大部分key-value对场景 |
list | 元素可重复,从两端压入或弹出元素 | 粉丝列表,文章的评论列表 |
set | 元素不可重复,无序集合 | 通过并集、交集、差集等,计算共同好友 |
hash | 键值对集合,键为不可重复的string,值为任意类型 | 任何需要用到键值对的场景 |
zset(sorted set) | 类似于set,但元素可根据分数排名 | 游戏排名 |
4.1 String
String 类型的底层的数据结构实现主要是整数集合和 SDS(简单动态字符串)。字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
- 如果一个字符串对象保存的是整数值,并且这个整数值可以用
long
类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr
属性里面(将void*
转换成 long),并将字符串对象的编码设置为int
。
- 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为
embstr
,embstr
编码是专门用于保存短字符串的一种优化编码方式。
- 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为
raw
。
可以看到
embstr
和raw
编码都会使用SDS
来保存值,但不同之处在于embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject
和SDS
,而raw
编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject
和SDS
。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 类型的底层数据结构;
- 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
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 类型的底层数据结构; - 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。
- 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
- 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
4.4 Set
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储
2^32-1
个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。Set 类型的底层数据结构是由哈希表或整数集合实现的:
- 如果集合中的元素都是整数且元素个数小于
512
(默认值,set-maxintset-entries
配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
- 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
4.5 Zset
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。
有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于
128
个,并且每个元素的值小于64
字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象,这两个数据结构一个是 skiplist/ziplist,一个是 dict。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。一个 zset 结构同时包含一个字典和一个跳跃表:
- zsl 跳跃表按score从小到大保存了所有集合元素,完成排序功能
- dict 字典则为所有集合元素创建了一个从成员到分值的映射,完成O(1)的查找功能。
- skiplist的obj指针和dict的key指针是指向同一个对象,也就是数据只会保存一份。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
5、扩展数据类型
类型 | 描述 | 应用场景 |
Bitmap(2.2版新增) | 类似于一个以比特位为单位的数组,数组的每个单元只能存储0和1,用少量空间记录大量状态(状态值只能为0或者),适合二值统计 | 布隆过滤器 |
HyperLogLog(2.8版新增) | 使用固定的大小(不超过12k),用来统计海量数据中不重复的元素个数。基于伯努利算法,不会存储原始基数数据,只做基数统计,统计结果是近似结果,有一定的误差,适合基数统计 | uv统计 |
GEO(3.2版新增) | 适合地理位置信息统计 | 附近列表 |
pub/sub | 一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。缺点:
• 没有ack机制
• 阻塞式的,一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。
• 没有持久化功能,消息发送后没有消费者消费就会丢失。 | 简单的IRC(实时聊天室) |
stream(5.0版新增) | pub/sub的升级版,提供持久化、消费者组等功能。适合以最小延迟实时处理消息的场景,不适合长时间大量存储消息。redis5.0以上出现。 | IRC(实时聊天室) |
5.1 BitMap
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行
0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
5.2 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
就能统计完。5.3 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 服务中频繁使用的“搜索附近”的需求。
5.4 Stream
Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
- 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
- List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。
基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
- Author:mcbilla
- URL:http://mcbilla.com/article/0a9fbe68-677e-4538-a6e6-42f997de6811
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts