前言:
Redis 相比于 MySQL 来说执行速度更快,性能更高,主要是由 3 各方面的原因导致的:
- Redis 将数据存储在内存中,提供快速的读写速度,相比于传统的磁盘数据库,内存访问速度更快
- Redis 使用单线程事件驱模型结合I/O多路复用,避免了多线程上下文切换和竞争条件,提高了并发处理效率
- Redis 提供多种高效的数据结构(如字符串、哈希、列表、集合等),这些结构通过优化,能够快速完成各种操作
Redis 是非关系型数据库,使用的是 k-v 的形式进行存储数据的,通过对key值进行 hash 运算找到存储到的哈希槽,同时通过 rehash 来减少 hash 冲突的概率。这样就会使搜索效率变成 O(1),执行速度大大提高。
hash槽下标 = hash(key) % 数组size
Redis 常见的5种数据类型
Redis 常见的5种数据类型:String(字符串),hash(哈希),list(列表),set(集合),zset(有序集合)。每个数据类型底层都有一些不同的数据存储结构,比如:简单动态字符串(SDS),哈希表,压缩列表,双向链表,整数数组,跳表等等。
注: Redis 中的 key 数据结构为string,value 为五种数据结构中的一种。
结构表:
String 数据类型
Redis 没有使用C语言的字符串,而是新建了属于自己的结构SDS,并结合了int、 embstr、raw 等不同的编码方式进行优化存储(Redis中所有的键都是由字符串对象实现的,即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。
int编码:用于存储可以解析为整数的字符串,内存消耗最小,适合数字型 embstr编码:用于存储较短的字符串,将元数据和内容存储在同一块内存中,适合读多写少的场景 raw编码:用于存储较长的字符串,元数据和内容分开存储,适合需要频繁操作的大字符串。
int编码:如果一个字符串可以解析为整数并且数值较小,Redis会直接使用整数编码
embstr编码:当字符串长度比较短(小于等于 44 字节),Redis 会使用 embstr 编码,这种编码将所有的字符串相关结构体和字符数据存放在连续的内存块中,分配内存的时候,只需要分配一次,减少内存分配和管理的开销。
raw 编码:当字符串长度超过 44 字节时,Redis 会使用raw 编码,这种编码方式将结构体和实际字符串数据分开存储,以便处理更长的数据。
hash 数据类型
Redis 的hash是一种键值对的集合,可以将多个字段和值存储在同一个键中,便于管理一些关联数据。可以存储小数据,使用哈希表实现,能够在内存中高效存储和操作,支持快速的字段操作,非常适合存储对象的属性。
底层实现:
- Redis 6 及之前,Hash 的底层是压缩列表加上哈希表的数据结构(ziplist + hashtable)
- Redis7之后,Hash的底层是紧凑列表(Listpack)加上哈希表的数据结构(Listpack + hashtable)
Redis 内有两个值,分别是hash-max-ziplist-entrieshash-max-listpack-entries)及hash-max-ziplist-hash-max-listpack-value),即Hash类型键的字value段个数(默认512)以及每个字段名和字段值的长度(默认64)
hash类型键的字段数小于hash-max-ziplist-entries并且每个字段名和字段值的长度小于hash-max-ziplist-value的时候,redis才会使用紧凑表来存储,前述的条件任意一个不满足就会转变为OBJ_ENCODING_HT(hashtable)的编码格式。Listpack升级到hashtable可以,但是他不会在进行降级。
紧凑列表(Listpack):
zlbytes:紧凑列表所占用的总字节数。
zltail:到最后一个元素的偏移量。
zllen:紧凑列表包含的元素数量。
entryX:列表中的元素(entry),每个元素包含其前一个元素的长度信息及其值。
zlend:紧凑列表的结尾标记,固定为 0xFF。
+--------+--------+--------+--------+--------+--------+--------+
| zlbytes| zltail | zllen | entry1 | entry2 | ... | zlend |
+--------+--------+--------+--------+--------+--------+--------+
prevlen:前一个元素的长度。
encoding:当前元素的编码方式,决定了元素值的存储形式。
value:当前元素的实际值,根据编码方式的不同,可以是整数或字符串。
+--------+--------+--------+
| prevlen| encoding| value |
+--------+--------+--------+
插入操作:插入一个新元素时,需要找到合适的位置,然后调整相应位置的偏移量和长度信息,将新元素插入到列表中。
删除操作:删除一个元素时,需要找到该元素的位置,然后调整其前后元素的偏移量和长度信息,移除该元素。
查找操作:查找元素时,需要遍历紧凑列表,依次比较每个元素的值,找到匹配的元素。
Redis 中紧凑列表的应用
紧凑列表在 Redis 中主要用于以下两种数据类型的底层实现:
列表(list) :当列表元素数量较少或每个元素的长度较短时,Redis 使用紧凑列表来实现。
哈希表(hash) :当哈希表中的键值对数量较少或每个键值对的长度较短时,Redis 使用紧凑列表来实现。
紧凑列表的优缺点:
优点
- 节省内存:紧凑列表通过紧密排列的内存结构,显著减少了内存消耗。
- 较高的缓存命中率:由于紧凑列表在内存中是连续存储的,可以更好地利用 CPU 缓存,提高访问速度。
缺点
- 操作复杂:由于每次插入、删除操作需要调整多个元素的偏移量和长度信息,操作复杂度较高。
- 性能下降:当元素数量较多或元素长度较长时,紧凑列表的操作性能会显著下降。
总结
紧凑列表是一种高效的内存优化数据结构,适用于存储小规模列表和哈希表。通过紧密排列的内存结构,紧凑列表能够显著减少内存消耗(Redis在内存中存储,空间很宝贵),并提高缓存命中率。然而,由于操作复杂度较高,当数据规模增大时,紧凑列表的性能会受到一定影响。因此,在 Redis 中,紧凑列表主要用于存储小规模的数据,当数据规模增大时,Redis 会自动切换到其他更适合的数据结构,如双向链表或哈希表。
Hashtable
结构
Hashtable 就是使用哈希表实现的,查询时间复杂度是O(1),效率很高。当发生哈希冲突的时候使用拉链法。整体逻辑就是指针数组加链表的形式。
typedef struct dictht{
//哈希表数组(指向整个哈希数组)
dictEntry **table;
//哈希表大小
unsigned long size,
//哈希表大小掩码,用于计算索引值(size - 1)
unsigned long sizemask;
//该哈希表已有的节点数量
unsigned long used;
}dictht;
hashtable结构图如下:
对于每一个hash节点(dictEntry的结构):
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值(采用结构体的方式可以节省空间)
union {
void *val;
uint64 t u64;
int64 t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
渐进式rehash(hash扩容)
扩容原因:当hashtable存储的元素过多,可能由于碰撞也过多,导致其中某天链表很长,最后致使查找和插入时间复杂度很大。因此当元素超过一定得时候就需要扩容。当元素比较小的时候就需要缩容以节约不必要的内存。Redis的作者定义这个操作叫做rehash操作,通过rehashidx索引完成。
扩容过程:额外创建一个哈希数组,采用分治的方式进行渐进式扩容(一点点进行扩容),最后用新创建的数组来替换之前数组。
- 为字符ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对的数量。 扩展:ht[1]的大小为第一个大于等于ht[0].used*2。 收缩:ht[1]的大小为第一个大于等于ht[0].used/2 。
- 将所有的ht[0]上的节点rehash到ht[1]上,重新计算hash值和索引,然后放入指定的位置。
- 当ht[0]全部迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0]表,并创建新的ht[1],为下次rehash做准备。
渐进式扩容的整体流程:将字典中的rehashidex的值从-1变到0(代表开始数据迁移),然后将原hash表中的数据分批次、渐进式地迁移到新表中。(这期间插入数据会直接插入到新表中),当迁移结束以后,会将rehashidx值设置为-1,并且将指针指向新表。
注意:扩容和缩容与负载因子有关,当负载因子小于0.1会进行缩容,负载因子大于等于5时,会进行扩容。
List 与 Set 数据类型
List底层是使用紧凑列表和双向链表实现的。 Set底层是使用数字集合和hash表来实现的。
Zset 数据类型(跳表实现)
Zset 是根据分数(score)来实现有序排序的数据结构。当元素数量小于zset_max_ziplist_entries(默认值是128)并且有序集合中新添加元素的member的长度小于zset_max_ziplist_value的值(默认值为64)时,使用紧凑列表来实现。否则,Zset 底层使用跳跃表 来实现有序结合。
跳表的原理:
使用一个多层索引的链表,每一层索引的元素都可以在最底层的链表中找到元素。跳表可以避免在查询、插入、删除数据时遍历整个链表,使操作的时间复杂度从O(N)变成了O(logN),采用的是用空间换时间的思想。如图所示:
1、查询元素:
从多层链表的每一层从上往下进行查询,直到查询到结果以后进行返回。
2、插入元素:
当定位到插入数据的位置以后,是在当前节点创建数据还是新增一个层级是随机的。
3、删除元素
从最高层开始查询要删除的节点,然后在各层中更新指针,保持跳表的结构。
注: 跳表的结构中包含的回退指针,这是为了提高跳表的操作和灵活性。例如在删除数据的时候,我们不仅需要修改当前节点,还需要找到前一个结点,修改其指针,通过回退指针可以避免从最高层进行逐层查询。
Redis 各种数据类型及其底层结构总结:
string字符串(redis会根据当前值的类型和大小决定使用哪种内部编码实现)
int:8个字节的长整型
embstr:小于等于44个字节的嵌入式字符串(内存地址与redisObject连续)
raw:大于44个字节的字符串
hash哈希
Listpack(紧凑列表):当哈希元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会采用Listpack作为哈希的内部实现,Listpack内存更加的紧凑和节省,所以节约内存方面比起来hashtable要优秀
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis就会改用hashtable作为哈希的内部实现,因为此时的ziplist的读写效率下降,而hashtable的读写复杂度为O(1)
list列表
Listpack:当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置(默认是64个字节)时.Redis会采用Listpack来作为列表的内部实现来减少内存的使用
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会采用quicklist作为内部实现.quick是linkedlist和ziplist的结合,以ziplist为节点的链表(linkedlist)
set集合
inset(整数集合):当集合中的元素都是整数且元素个数小于set-max-inset-entries配置(默认512个)时,Redis会采用inset来作为集合的内部实现,从而减少内存的使用
hashtable:当集合类型无法满足inset时,Redis会采用hashtable作为内部实现
zset有序集合
Listpack:当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会采用Listpack来作为有序集合的内部实现,Listpack可以有效减少内存的使用
skiplist(跳跃表):使用多层索引的链表结构,将查询、删除、插入操作的时间复杂度优化为O(logN)