文章目录
一、Redis数据结构概述
二、String类型
三、List列表类型
四、Set集合类型
五、Hash哈希类型
六、ZSet类型
七、Stream类型
还记得Redis五种数据类型、String、List、Set、Hash、ZSet吗?如果忘记可以到这里重新温习:Redis基础(超详解)一 :Redis定义、SQL与NoSQL区别、Redis常用命令、Redis五种数据类型、String、List、Set、Hash、ZSet;Redis的Java客户端
Redis 是一款开源的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。Redis 支持多种类型的数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog 和地理空间索引(Geospatial)。这些数据结构提供了丰富的操作,使得 Redis 能够应对各种各样的场景。本文将详细介绍 Redis 的各种数据结构,包括它们的特性、底层实现、常用命令以及应用场景。
Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:包含 6 种基本类型——String(字符串)、List(列表)、Hash(哈希)、Set(集合)、Sorted Set(有序集合)、Stream(流、Redis5.0引入),和三种特殊类型——geospatial(地理位置)、Bitmap(位存储)、HyperLog(基数统计)。
数据结构的底层实现:底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
从上图可以看出:String 的底层是简单动态字符串;List 的底层是双向链表和压缩链表;Set 的底层是整数数组和哈希表;Hash 的底层是压缩链表和哈希表;Sorted Set 底层压缩链表和跳表。即 String 类型的底层实现只有一种数据结构,也就是简单动态字符串。而 List、Set、Hash 和 Sorted Set这四种数据类型,都有两种底层实现结构。通常情况下,我们会把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据。
Redis 之所以采用不同的数据结构,其实是在性能和内存使用效率之间的平衡。
Redis 本身是一个键值对数据库,这种键值对的存储方式就是哈希映射(Hashmap)的一种体现,即通过键(Key)来快速查找对应的值(Value)。
根据下图可看出,哈希桶中的 entry 元素中保存了 ‘*key
’ 和 '*value
’ 指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过 '*value
指针被查找到:
因为这个哈希表保存了所有的键值对,所以它也叫做全局哈希表。
也就是说,整个数据库就是一个全局 Hash 表,而 Hash 表的时间复杂度就是 O(1),只需要计算每个键的 Hash 值,就知道对应的 Hash 桶位置,定位桶里面的 Entry 找到对应数据,这个也是 Redis 快的原因之一。
但如果我们只是了解哈希表 O(1) 复杂度和快速查找特性,那么当我们向 Redis 中写入大量数据之后,就可能发现 操作有时候会突然变慢了。原因是哈希表的冲突问题和 rehash 可能带来的操作阻塞。
Redis 使用哈希表作为其底层数据结构,哈希冲突是哈希表中常见的问题。当两个或更多的键被哈希函数映射到同一个哈希桶时,就会发生哈希冲突。Redis 通过链地址法来解决哈希冲突,即在每个哈希桶中维护一个链表,所有哈希到同一个桶的键值对都存储在这个链表中。
当哈希表中的元素数量增长到一定程度,或者哈希表中的元素数量减少到一定程度,Redis 会触发哈希表的扩容或收缩,这个过程称为 rehash。为了避免 rehash 过程中一次性复制所有元素导致的长时间阻塞,Redis 使用了一种称为“渐进式 rehash”的策略。
在渐进式 rehash 过程中,Redis 会同时维护新旧两个哈希表,并在每次对哈希表进行操作时,将一部分桶从旧哈希表移动到新哈希表。同时,为了保证查询操作的正确性,Redis 在查询时会同时查找新旧两个哈希表。这样,通过分摊在一段时间内完成 rehash,避免了一次性操作带来的性能问题。
我们都知道Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种
数据结构。
不过Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题。在C语言中定义字符串 char *str = "message"
,本质是字符数组 {'m', 'e', 's', 's', 'a', 'g', 'e', '\0'}。它会存储如下图的样子:
以“\0”代表结束。这样就会产生以下问题:
Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。(Redis是C语言实现的)
例如我们执行命令:set name 大哥
,那么Redis将在底层创建两个SDS,其中一个是包含“name”的SDS,另一个是包含“大哥”的SDS。
SDS优点:
LinkedList 是标准的双向链表,Node 节点包含 prev 和 next 指针,分别指向后继与前驱节点,因此从双向链表中的任意一个节点开始都可以很方便地访问其前驱与后继节点。
LinkedList 可以进行双向遍历;添加删除元素快 O(1),查找元素慢 O(n),高效实现了 LPUSH 、RPOP、RPOPLPUSH,但由于需要为每个节点分配额外的内存空间,所以会浪费一定的内存空间。这种编码方式适用于元素数量较多或者元素较大的场景。
LinkedList 结构为链表提供了表头指针 head、表尾指针 tail,以及节点数量计算 len。下图展示一个由 list 结构和三个 listNode 节点组成的链表:
Redis 的链表实现的特性可以总结如下:
ZipList是一种特殊的“双端链表”(并非链表),由一系列特殊编码的连续内存块组成,像内存连续的数组。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。
压缩列表 底层数据结构:本质是一个数组,增加了列表长度、尾部偏移量、列表元素个数、以及列表结束标识,有利于快速寻找列表的首尾节点;但对于其他正常的元素,如元素2、元素3,只能一个个遍历,效率仍没有很高效。
属性 | 类型 | 长度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 一个 4 字节的整数,表示整个压缩列表占用的字节数量,包括 |
zltail | uint32_t | 4字节 | 一个 4 字节的整数,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址 |
zllen | uint16_t | 2字节 | 一个 2 字节的整数,表示压缩列表中的节点数量。最大值为UINT16_MAX(65534),如果超过这个数,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算出 |
entry | 列表节点 | 不定 | 压缩列表中的元素,每个元素都由一个或多个字节组成,节点的长度由节点保存的内容决定。每个元素的第一个字节(又称为"entry header")用于表示这个元素的长度以及编码方式 |
zlend | uint8_t | 1字节 | 一个字节,特殊值0xFF(十进制255),表示压缩列表的结束 |
注意:
Redis 的散列表(hashtable)是一种常见的键值对映射结构,它通过一个散列函数将键映射到一个桶中,然后在桶中进行查找。Redis 的散列表使用链表法解决哈希冲突,即当多个键映射到同一个桶时,将它们存储在同一个链表中。
在Redis的源代码中,散列表的结构定义如下:
typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
其中:
**SkipList(跳表)**首先是链表,在链表的基础上,增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素。但与传统链表相比有几点差异:
普通链表想查找元素27,只能从链表头部一个个往后遍历,需要遍历6次 才能找到元素27
跳表怎么做的(how):建立多级索引
如建立一级索引
如果觉得慢,可以建立二级索引
当数据量特别大的时候,跳表的时间复杂度为 O(logN)。其本身利用的思想,有点类似于二分法。
SkipList的特点:
IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。
(int8_t int表示整数、8表示8bit位,即1个字节)
为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:
IntSet升级
现在,假设有一个intSet,元素为{5,10,20},采用的编码是INTSET_ENC_INT16,则每个整数占2字节:
我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。以当前案例来说流程如下:
Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象,源码如下:
Redis中会根据存储的数据类型不同,选择不同的编码方式,共包含11种不同类型:
编号 | 编码方式 | 说明 |
---|---|---|
0 | OBJ_ENCODING_RAW | raw编码动态字符串 |
1 | OBJ_ENCODING_INT | long类型的整数的字符串 |
2 | OBJ_ENCODING_HT | hash表(字典dict) |
3 | OBJ_ENCODING_ZIPMAP | 已废弃 |
4 | OBJ_ENCODING_LINKEDLIST | 双端链表(后续已废弃) |
5 | OBJ_ENCODING_ZIPLIST | 压缩列表 |
6 | OBJ_ENCODING_INTSET | 整数集合 |
7 | OBJ_ENCODING_SKIPLIST | 跳表 |
8 | OBJ_ENCODING_EMBSTR | embstr的动态字符串 |
9 | OBJ_ENCODING_QUICKLIST | 快速列表 |
10 | OBJ_ENCODING_STREAM | Stream流 |
五种数据结构
Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:
数据类型 | 编码方式 |
---|---|
OBJ_STRING | int、embstr、raw |
OBJ_LIST | LinkedList和ZipList(3.2以前)、QuickList(3.2以后) |
OBJ_SET | intset、HT |
OBJ_ZSET | ZipList、HT、SkipList |
OBJ_HASH | ZipList、HT |
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
String 是 Redis 最简单的数据类型,也是最常用的数据类型。它可以包含任何数据,包括字符串、整数或者浮点数。在 Redis 中,字符串的最大长度可以达到 512MB。
应用场景:
底层结构:Redis 的 String 类型是二进制安全的,基于简单动态字符串(SDS)实现,它的底层实际上是一个字节数组,因此 String 类型可以包含任何数据,例如 jpg 图片或者序列化的对象。
在Redis实现中并没有直接采用C语言的字符串,而是自定义了一个SDS(简单动态字符串)的结构体来标识字符串。
在C语言中定义字符串 char *str = "message"
它会存储如下图的样子:
以“\0”代表结束。这样就会产生以下问题:
redis要解决上述问题,就自定义了一个SDS结构,如下图:
redis可以根据字符串的大小使用不同类型的sds,这样可以进一步的节省内存。这里不需要担心buf的长度不够用,2的64次幂是一个非常巨大的数字,同时redis默认也会限制最大的字符为512M,在6.3版本开始可以对最大限制字符大小进行配置。注意:使用不同类型的sds并不是一次性分配这么多空间,而是逐步分配的,例如:使用sdshdr16这种类型,存入一个长度为14的字符串,那么会根据存入的字符串长度预留空闲长度,这里是14;如果字符串大小超过1M,那么预留空间就是1M。
redis除了自定义了SDS类型来存储字符串,还定义了三种编码:
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构,按插入顺序排序,你可以添加一个元素到头部(左边)或尾部(右边)。既可以支持正向检索和也可以支持反向检索。特征也与LinkedList类似:有序、元素可重复、插入和删除快、查询速度一般。在 Redis 中,列表最多可以包含 2^32 - 1 个元素。
应用场景:
底层结构:
Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素:
QuickList结构如下:
在 Redis3.2 版本前,Redis 列表 List 使用两种数据结构作为底层实现:
在 Redis3.2 版本前 压缩列表不仅是 List 的底层实现之一,同时也是 Hash、 ZSet 两种数据类型底层实现之一。
ZipList是一种特殊的“双端链表”(并非链表),由一系列特殊编码的连续内存块组成,像内存连续的数组。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。
压缩列表 底层数据结构:本质是一个数组,增加了列表长度、尾部偏移量、列表元素个数、以及列表结束标识,有利于快速寻找列表的首尾节点;但对于其他正常的元素,如元素2、元素3,只能一个个遍历,效率仍没有很高效。
当我们的 List 列表数据量比较少的时候,且存储的数据轻量的(如小整数值、短字符串)时候, Redis 就会通过压缩列表来进行底层实现。
属性 | 类型 | 长度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 一个 4 字节的整数,表示整个压缩列表占用的字节数量,包括 |
zltail | uint32_t | 4字节 | 一个 4 字节的整数,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址 |
zllen | uint16_t | 2字节 | 一个 2 字节的整数,表示压缩列表中的节点数量。最大值为UINT16_MAX(65534),如果超过这个数,此处会记录为65535,但节点的真实数量需要遍历整个压缩列表才能计算出 |
entry | 列表节点 | 不定 | 压缩列表中的元素,每个元素都由一个或多个字节组成,节点的长度由节点保存的内容决定。每个元素的第一个字节(又称为"entry header")用于表示这个元素的长度以及编码方式 |
zlend | uint8_t | 1字节 | 一个字节,特殊值0xFF(十进制255),表示压缩列表的结束 |
注意:
Redis后续版本已废弃双向链表LinkedList,有关LinkedList的细节 可查阅本文1.4.2小节。
QuickList底层 LinkedList + ZipList,可以从双端访问,内存占用较低,保含多个ZipList,存储上限高。其特点:
ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请效率较低。
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
Set 是 Redis 的一种数据类型,它是字符串类型的无序集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高。和列表一样,你可以添加、删除、查找元素。但它保证每个元素只出现一次。在 Redis 中,集合最多可以包含 2^32 - 1 个元素。满足下列特点:
应用场景:
底层结构:
Redis Set 的底层实现为整数集合和哈希表两种,当集合中的元素都是整数且元素数量较少时,Redis 会选择整数集合作为底层实现,这样可以更加节省内存;当数据量变大或者集合中的元素不全是整数时,Redis 会自动将底层实现从整数集合切换为哈希表(类似于Java 中,hashset是基于hashmap实现的)
注意事项:
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
Hash 是 Redis 的一种数据类型,也叫散列,其value是一个无序字典,类似于Python 的字典、Java中的HashMap结构。它是键值对集合,是一个字符串字段和字符串值之间的映射表,其字段和值的最大长度都是 512MB。在 Redis 中,哈希可以存储超过 4 亿个键值对。
String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便:
KEY | VALUE |
---|---|
jw:user:1 | {name:"Jack", age:21} |
jw:product:1 | {name:"Rose", age:18} |
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,只需要通过key + field找到:
Hash结构与Redis中的Zset非常类似:
区别如下:
因此Hash底层采用的编码与ZSet也基本一致,只需要把排序有关的SkipList去掉即可
应用场景:
底层结构:
Hash底层采用的编码与ZSet也基本一致,只需要把排序有关的SkipList去掉即可。Redis Hash 的底层实现为压缩列表和哈希表两种,当 Hash 中的元素个数较少且每个元素的大小较小的时候,Redis 会选择压缩列表作为底层实现,这样可以更加节省内存;当数据量变大时,Redis 会自动将底层实现从压缩列表切换为哈希表。
Redis 的 Hash 类型的底层实现是一个非常优化的数据结构,它会根据实际情况选择使用紧凑的**压缩列表(ziplist)或者散列表(hashtable)**作为底层实现。
Redis 的 Hash 类型会根据实际情况在压缩列表(ziplist)和散列表(hashtable)之间进行切换,这主要取决于两个配置参数:hash-max-ziplist-entries
和 hash-max-ziplist-value
。
这两个参数都可以在 Redis 的配置文件中进行设置。通过调整这两个参数,你可以根据自己的应用特性,选择更倾向于节省内存,还是更倾向于提高性能。
压缩列表(ziplist)与散列表(hashtable)的详细介绍 可见本文1.4章节。
详细介绍:Redis五种数据类型、String、List、Set、Hash、ZSet
ZSet(有序集合)也就是SortedSet,其中每一个元素都需要指定一个score值和member值。它是一个可排序的set集合,在 Set 的基础上增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。在 Redis 中,有序集合的最大成员数是 2^32 - 1。ZSet具备下列特性:
因为ZSet的可排序特性,经常被用来实现排行榜这样的功能。
应用场景:
底层结构:
ZSet与Java中的TreeSet有些类似,但底层数据结构却差别很大。ZSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。注意,集合成员是唯一的,但是评分可以重复。
补充:ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:
注意事项:
什么时候采用压缩列表、什么时候采用跳表呢
上述 1且2的时候,采用压缩列表;否则采用跳表
学习一个新知识,从三方面分析:WHAT、WHY、HOW
**SkipList(跳表)**首先是链表,在链表的基础上,增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素。但与传统链表相比有几点差异:
SkipList的特点:
普通链表想查找元素27,只能从链表头部一个个往后遍历,需要遍历6次 才能找到元素27
跳表怎么做的(how):建立多级索引
如建立一级索引
如果觉得慢,可以建立二级索引
当数据量特别大的时候,跳表的时间复杂度为 O(logN)。其本身利用的思想,有点类似于二分法。
因为普通链表查找一个元素 时间复杂度O(n);而跳表查找的时间复杂度为O(logn),查找速度更快。不仅如此,删除、插入等操作的时间复杂度也是O(logn)
红黑树、二叉树查找一个元素的时间复杂度也是O(logn)
Redis是直接操作内存的、并不需要磁盘io,而MySQL需要去读取磁盘io,所以MySQL使用b+树的方式去减少磁盘io。B+树原理是 叶子节点存储数据、非叶子节点存储索引,每次读取磁盘页时就会读取一整个节点,每个叶子节点还要指向前后节点的指针,其目的是最大限度地降低磁盘io
数据在内存种读取 耗费的时间是磁盘IO读取的百万分之一,而Redis是内存中读取数据、不涉及IO,因此使用了跳表,跳表模型是更快更简单的方式
Stream 是 Redis 5.0 版本引入的新特性,它是一种类似于日志系统的数据结构,用于存储多个键值对的列表,每个键值对都会被分配一个自动递增的ID。Stream 主要用于实现消息队列的功能,如 Apache Kafka。
应用场景:
底层结构:
Redis Stream 的底层实现为一种叫做快速列表(quicklist)的数据结构,这是一种同时包含了压缩列表(ziplist)和双向链表特性的数据结构,既可以利用压缩列表节省内存,又可以利用双向链表在两端进行快速的添加、删除操作。
常用命令:
XADD key ID field value [field value …]:向 Stream 中添加元素。
XRANGE key start end [COUNT count]:获取 Stream 中指定范围的元素。
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]:从 Stream 中读取数据。
XDEL key ID [ID …]:从 Stream 中删除指定 ID 的元素。
XLEN key:获取 Stream 中的元素数量。
注意事项:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。