决战圣地玛丽乔亚Day44---Redis数据结构的底层实现和高可用Cluster

发布时间 2023-03-30 09:11:28作者: EmiXXXt

Redis为什么快?

前面讲了独有的数据结构+IO模型的选用。

除此之外还有什么因素的加持呢?

1.内存存储,避免磁盘IO的开销,提高读取速度

2.网络模型,使用Reactor模型,处理大量连接请求,同时保持低延迟和高吞吐

3.单线程处理请求。但是RDB,AOF等场景会用到多线程模式。

 

Redis数据结构的底层逻辑?为什么这么设计?

https://baijiahao.baidu.com/s?id=1739547372042800800&wfr=spider&for=pc

前面大概讲了一下数据结构的概念,接下来需要深入讨论一下底层是如何设计的?为什么Redis需要这样设计?

String:

底层是字节数组构成字符串,数据结构是SDS。

SDS的好处之前已经提过了,动态分配,二进制安全传输,惰性删除。

SDS还实现了字符串的拼接,字符串长度计算,字符串的截取等提高字符串的效率。

Redis对字符串的设计兼顾了内存效率,安全和处理效率。

 

Hash:

底层采用ziplist和hashtable两种形式:

如果hash对象保存的键值对数量小于512且键值对的字符串长度都小于64字节。使用ziplist,否则用hashtable

ziplist:

 

 

<uint32_t zlbytes>: 一个无符号整数,用于保存ziplist占用的字节数,包括zlbytes字段本身的四个字节。需要存储这个值,以便能够调整整个结构的大小,而不需要首先遍历它。
<uint32_t zltail> : 列表中最后一个条目的偏移量。
<uint16_t zllen> : 条目的数量。当有超过2^16-2个条目时,这个值被设置为2^16-1,我们需要遍历整个列表来知道它包含多少项。
<uint8_t zlend> : 一个特殊的条目,表示 ziplist 的结尾 

entry:

  prev_length:确定entry的长度,以找到下一个entry的位置

  encoding:确定entry存储的数据类型和内容

Entry是存储信息的媒介。可以把相邻的Entry作为键值对。但是Redis为了提高存储效率,把多个Entry看做一个节点,

第一个entry存储节点的长度信息,长度是固定的。然后用剩下的Entry存放多组键值对,存不下需要新开节点存。

要存键值对,至少需要两个Entry,一个键一个值。

 

HahsTable实现Hash结构:
hashTable就是dict的实现。

 

 

 

typedef struct dict {
    dictType *type;     // 类型特定函数
    void *privdata;     // 私有数据
    dictht ht[2];       // 哈希表
    long rehashidx;     // rehash 索引
    unsigned long iterators; // 正在迭代的迭代器数量
} dict;
typedef struct dictht {
    dictEntry **table;  // 哈希表数组
    unsigned long size; // 哈希表大小
    unsigned long sizemask; // 哈希表大小掩码,始终等于 size - 1
    unsigned long used; // 已使用的节点数量
} dictht;
typedef struct dictEntry {
    void *key;          // 键
    union {
        void *val;      // 值
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 指向下一个键值对的指针,用于解决哈希冲突
} dictEntry;

为什么Hash的时候,数据量小的时候用ziplist,数据量大的时候用hashTable?

1.HashTable 可以提高性能。当 Hash 中的元素数量比较大,或者元素的值比较大(比如对象、数组等)时,使用 HashTable 可以提高性能。因为 HashTable 采用了哈希表的数据结构,可以快速定位到对应的键值对,而且 HashTable 的查找、插入、删除等操作都可以在常数时间内完成,因此在数据量比较大的情况下,使用 HashTable 是更加高效的选择。

2.ziplist 可以节省内存。当 Hash 中的元素比较小且元素值也比较小(比如字符串、整数等)时,使用 ziplist 可以节省内存。因为 ziplist 是一种连续的内存结构,不需要像 HashTable 一样分配大量的指针和额外的内存空间,因此在数据量比较小的情况下,使用 ziplist 是更加节省内存的选择。

3.在数据量不确定的情况下,使用 HashTable 是更加稳定的选择。因为在数据量不确定的情况下,如果始终使用 ziplist,可能会因为数据量过大而导致内存占用过高,甚至导致内存溢出。而使用 HashTable,则可以根据实际数据量动态调整哈希表的大小,保证内存占用在可控范围内。

简单来说,数据量小的时候,使用ziplist更紧凑。hashtable分配的空间多,数据量小使用比较浪费。

hashtable的性能比ziplist要高,所以数据量多用hashtable。

 

List:

Ziplist和linkedList和quickList

ziplist:

一整块连续内存存储,利用率高。

修改操作性能差,每次数据变动都会引起内存realloc

当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝,进一步降低性能

 

linkedlist:

两端的push和pop操作方便,但是内存开销大

除了保存数据之外还要保存双端指针,就算是一个节点也需要头尾指针。

地址不连续,容易产生碎片

 

quicklist: 

quicklistNode中有指向压缩列表的指针

 

空间效率和时间效率折中

结合linkedlist和ziplist的优点

使用选择:

ziplist在数据量小的时候选用,内存利用率高,但是只支持基础的插入,删除,访问,迭代,不支持高级操作。

linkedlist在数据量大的时候选用,方便头尾操作,所以频繁的操作头尾数据的时候可以使用。

quicklist通过ziplist来提高内存使用率,同时可以存储大量数据。需要注意quicklist的每个ziplist节点大小固定,如果超出单节点大小会分配新节点,就会导致内存使用率不如linkedlist。

 

Set:

hashTable和intset

inset:

  • 结合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

 

整数数组,升序排列,查询方式一般是二分查找,一般是连续内存空间,对cpu高速缓存支持友好。 

其他情况使用hashtable。

 

ZSet:

skiplist和ziplist

ziplist前面讲过,适用于存储元素数量比较少、并且每个元素的值比较小的 Sorted Set。

skiplist 的节点是按照元素值的大小进行排序的,并且可以支持范围查询等操作。在元素数量比较多、或者需要支持范围查询等操作的情况下,可以选择使用 skiplist 来实现 Sorted Set 

为什么不用红黑树或者B+树之类的?

1)插入元素

2)删除元素

3)查找元素

4)有序输出所有元素

5)查找区间内所有元素

前四项,他们之间的性能都差不太多。在5范围查找上,跳表的效率是高的。我们只需要定位两个区间端点的第一层上的位置然后遍历即可。

红黑树:中序遍历,直到遍历到临界点。

B+树的范围查找效率比红黑树高:查询左右临界点,然后叶子结点遍历,由于遍历叶子结点,所以时间复杂度是O(N/B)B为每个节点存储的元素数量,效率比红黑树高一点

跳表:查询到左临界点,然后依次遍历。由于 skiplist 的节点是按层级链接的,因此可以通过快速跳过不在查询范围内的节点来提高查询效率。时间复杂度为 O(logN+M),其中 N 是跳表中的元素数量,M 是查询的元素数量

平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速

作者关于Redis为什么选用跳表作为ZSET的实现的解释:

可以看到redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。

 

 

高可用:

 

Redis  Cluster 集群模式

如果单机吞吐量过大,我们可以横向和纵向进行扩展,横向就是加节点(scale out),纵向就是加配置(scale up)。

如果加配置,治标不治本,单机局限性和持久化问题无法解决(如轮式RDB快照还是AOF指令)

横向扩展更容易扩展,可以解决很多问题,包括单一实例节点的硬件扩容限制、成本限制,还可以分摊压力,精细化治理,精细化维护

 

 

集群的组成:

CLUSTER MEET <ip> <port>

 

 

数据自动分片:

cluster create 创建,会将 16384 个slots 平均分配在我们的集群实例上

通过 addslots 命令指定哈希槽范围
cluster addslots 0,7120redis-cli -h 192.168.0.2 –p 6379

通过一致性哈希算法把数据分到2^14个哈希槽中,每个节点负责一部分的槽位数据(自定义分配),节点之间通过goosip协议进行通信,更新信息,故障转移和故障检测。

如果请求所在的节点不是负责该槽位的节点,那么请求会被转发到负责该槽位的节点上。如果请求的键名所在的槽位没有被分配到任何节点上,那么Redis Cluster会返回一个错误信息

集群处于online状态说明数据对应的槽位都有节点进行管理,如果状态为offline说明有数据对应的槽位没有被任何节点管理。

什么是goosip协议?

数据复制过程和故障转移

数据复制

故障检测

主从故障转移

client 访问 数据集群的过程

定位数据所在节点