文章目录

  • 一、redis 特性
  • redis为什么这么快
  • 1.基于内存
  • 2.合理线程模型
  • 单线程上下文切换
  • IO多路复用技术
  • 3.高效数据结构
  • 4.合理使用数据编码
  • Redis实现原理
  • 字典表
  • redis如何添加键值对
  • 渐进式rehash
  • 为什么需要渐进式rehash
  • 共存的策略
  • redis数据结构
  • 对象
  • 对象的类型和编码
  • Redis垃圾回收
  • 数据类型
  • Ziplist
  • 压缩列表的构成
  • ziplist 节点entry
  • hash:ziplist与hashtable转化
  • ziplist优势
  • redis基本类型底层实现
  • string
  • 字符串的实现
  • RedisObject
  • SDS
  • hash 字典 (ziplist+hash)
  • 字典实现
  • 哈希算法
  • 什么是rehash
  • rehash的步骤
  • List (3.2前ziplist+linked_list 3.2后quicklist)
  • Set(intset、hash)
  • zset(ziplist+skiplist)
  • ziplist、skipList 为什么需要转换?
  • 基本数据结构 底层实现 总结
  • Redis链表的实现
  • Redis高可用、高性能
  • 【2013年】Redis 哨兵模式(sentinel)
  • Sentinel 本质上是主从模式
  • 故障转移过程
  • 【2015】Redis 集群模式(Cluster)
  • 迁移(重分片)
  • 【2013】Redis 主从复制 (replication)
  • 主从复制过程
  • 增量复制的过程
  • 为什么全量复制使用 RDB 而不是使用 AOF 呢?
  • 全量、增量同步区别
  • 全量同步的缺点
  • 如果过程发生了网络中断或者阻塞,该如何解决?
  • Redis缓冲区
  • Replication Buffer 复制缓冲区
  • 复制缓冲区溢出问题
  • Repl Backlog Buffer 复制积压缓冲区(增量复制、主只有一个)
  • 复制积压缓冲区的溢出问题
  • 主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?
  • Redis底层持久化实现
  • 2.1 持久化 rdb
  • 2.1.1 Redis BG Save
  • 2.2 持久化 aof
  • 2.2.1 AOF 刷盘对比
  • 2.2.2 重写 AOF
  • 2.3 混合持久化
  • Redis 内存淘汰策略
  • 内存淘汰策略
  • 1、noevction(不进行数据淘汰)
  • 2、在设置了过期时间的数据中进行淘汰
  • 3、在所有数据中淘汰
  • Redis是如何淘汰key的?
  • Redis 在执行命令请求时,检查当前内存占用
  • 定期执行淘汰(简单贪婪策略)
  • 惰性删除
  • Redis IO模型
  • Redis的客户端与服务端的交互
  • 单线程性能瓶颈与Redis 6.0多线程
  • 多线程方案优劣
  • redis运行模式
  • 单副本模式
  • 主从模式(多副本):
  • 哨兵模式:
  • 主观下线和客观下线
  • 主从切换的步骤
  • 哨兵模式优缺点
  • 集群模式:
  • Redis Cluster集群具有如下几个特点:
  • 集群如何判断某个主节点挂掉?
  • 集群如何选举新的主节点?
  • redis cluster的数据迁移方案
  • 整体流程
  • 可用性
  • 一致性 Hash
  • 数据倾斜问题
  • 虚拟节点
  • redis管理
  • redis异步线程
  • Redis 集群管理
  • Redis高效数据结构
  • string: SDS简单动态字符串
  • SDS简单动态字符串介绍
  • Redis6.x VS Redis3.0.0 的 SDS
  • redis 3.0.0
  • 数据结构优化
  • Redis6.x中优化
  • Redis SDS内存不对齐
  • SDS相对C字符串的好处
  • C语言传统字符串获取长度的时间复杂度为 n
  • 二进制安全性
  • 预分配空间减少内存分配次数
  • 杜绝缓冲区溢出
  • ziplist实现原理
  • quicklist实现原理
  • skiplist 跳跃列表
  • Redis事务


一、redis 特性

redis为什么这么快

如何查找本地redis配置文件_数据库

1.基于内存

Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。

2.合理线程模型

reactor单线程、多线程反应模型

单线程上下文切换

Redis 的网络 IO 和命令处理,都在核心进程中由单线程处理

https://www.jianshu.com/p/c4aa888b3538 线程只需要保存线程的上下文(相关寄存器状态和栈的信息)

Redis采用了单线程的模型,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。

IO多路复用技术

redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。

多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。

Epoll 事件模型开发,可以进行非阻塞网络 IO,同时由于单线程命令处理,整个处理过程不存在竞争,不需要加锁,没有上下文切换开销

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

https://zhuanlan.zhihu.com/p/58038188

3.高效数据结构

如何查找本地redis配置文件_1024程序员节_02

4.合理使用数据编码

Redis 支持多种数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。

  • String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
  • List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码
  • Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。
  • Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。
  • Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码

Redis实现原理

字典表

redis单机服务端有16个数据库,每个数据库都有一个字典结构,这个字典里存着两个hash表(为了之后的扩缩容),而这个hash表里有一个dictEntry 组成的数组,里面存放的就是所有的键值对。这个dictEntry还有指向下一个节点的指针,就是为了在hash冲突的情况,采用拉链法扩展出一个链表。

如何查找本地redis配置文件_数据库_03

/*
 * 字典
 */
typedefstruct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;
/*
 * 哈希表
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedefstruct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsignedlong size;
    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsignedlong sizemask;

    // 该哈希表已有节点的数量
    unsignedlong used;

} dictht;
/*
 * 哈希表节点
 */
typedefstruct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

redis如何添加键值对

向字典表再添加一个元素 set name abin 我们会先对key做散列运算,将得到的值再对哈希表的大小4做一个取余,假设得到的值是3,那么这个key就会落在3的位置,比如:

如何查找本地redis配置文件_如何查找本地redis配置文件_04

渐进式rehash

当我们哈希表中存的数据越来越多,哈希冲突的概率就会越来越大。这样所有的键值对冲突后会形成一个链表,查询的效率就由原先的O(1)变成了O(n),所以我们要有一个评估的标准,用来判断是否需要扩缩容。

  • 扩容
    程序没有执行BGSAVE命令或者BGREWRITEAOF(AOF重写)命令,并且哈希表的负载因子大于等于1
    如果程序正在执行BGSAVE或者BGREWRITEAOF(AOF重写)命令并且哈希表的负载因子大于等于5。在执行RDB或者AOF重写操作时,redis会创建当前服务器的子进程执行相应操作,为了避免在子进程存在期间对哈希表进行扩展操作,将扩展因子提高。可以避RDB或者AOF重写时不必要的内存写入操作,最大限度的节约内存。
  • 缩容:当负载因子小于0.1
为什么需要渐进式rehash

然而redis并不像我画的那样,只有一两个key。一个生产使用的redis可以达到几百上千万个。而redis的核心计算是单线程的,一次性重新散列这么多的key会造成长时间的服务不可用,因此需要采用渐进式的rehash。

具体步骤

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
  • 在字典中维持-一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  • 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
  • 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
  • 最后将h[1]的地址设置给h[0],并将h[1]设置为null,也就是将新哈希表替换旧hash表。

渐进式rehash的好处在于它采取分而治之,的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。

共存的策略

因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间:

  • 所有增删改查都会先访问ht[0],再访问ht[1].比如查询会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类
  • rehash期间所有新增的键值对都会添加到h[1]里,保证ht [0]的键值对数量会只减不增,最终会变成空表

redis数据结构

对象

Redis并没有直接使用我们之前提到的数据结构实现键值对数据库 ,而实基于这些数据结构构建一个对象,分为字符串对象,列表对象,哈希对象,集合对象,有序集合对象。

Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一对象来节约内存。

对象的类型和编码

redis数据库中的每个键值对的键和值都是一个对象

Redis中的每个对象都由一个redisObject结构表示,该结构中保存数据有关的三个属性分别是type属性,encoding属性,ptr属性:

/*
 * Redis 对象
 */
typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码方式
    unsigned encoding:4;

    // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
    unsigned lru:LRU_BITS; // LRU_BITS: 24

    // 引用计数
    int refcount;

	//指向底层实现数据结构的指针
    void *ptr;

} robj;

每个对象都有相应的类型,这些类型决定了你能对他们操作的指令,比如string类型的对象只能用set命令设置。

每种类型的对象又有两种以上的编码,不同编码可以在不同场景上优化使用效率

这里的每个字段都很重要,比如类型和编码,有一个基于6.0的关系图

如何查找本地redis配置文件_如何查找本地redis配置文件_05

比如我们执行一个命令 hset user age 25 在字典上的数据结构大概是这样,为了方便,string类型的对象就简画成了stringobject。

核心就是搞明白,无论是key还是value,都是一个redisObject即可。

如何查找本地redis配置文件_redis_06

Redis垃圾回收

引用计数 Redis在自己的对象系统中构建了一个引用计数技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。

对象的引用计数信息会随着对象的使用状态而不断变化:

在创建一个新对象时, 引用计数的值会被初始化为1;

当对象被一个新程序使用时,它的引用计数值会被增一;

当对象不再被一个程序使用时,它的引用计数值会被减一;

当对象的引用计数值变为0时,对象所占用的内存会被释放;

数据类型

Ziplist

压缩列表的构成

压缩列表是Redis为节约内存而开发的,是由一系列特殊编码的连续内存组成。一个压缩列表可以包含任意多个节点,每个节点可以保存一个小整数或者一个短的字符串。

如何查找本地redis配置文件_redis_07


如何查找本地redis配置文件_big data_08

下图是一个ziplist编码的实例:

如何查找本地redis配置文件_redis_09

下图是一个双端链表编码的实例:

如何查找本地redis配置文件_数据库_10

ziplist 节点entry

压缩列表并没有存储指针,那么压缩列表各个节点之间是如何做到两端都能访问的呢?

这就是每个列表节点entry的结构厉害之处,每个entry保存了三个属性,分别是:

  • previous_entry_length(前一个节点的长度)
  • 如果前一个节点长度小于254 (<254),则用1个字节保存这个长度
  • 如果前一个节点长度大于等于254 (>=254),则采用5个字节长度保存,其中第一个字节为0xfe,后面四个节点才是真实的数据长度;
  • encoding(编码)
  • 记录content内容的 数据类型 以及 长度,占1、2或5个字节长度
  • 以’00’, ‘01’, ‘10’ 开头,那么content保存的是字符串
  • 以 ‘11’ 开头则保存的是整数
  • contents(保存的内容)
    保存数据的地方,可以是字符串或者整数。

如何查找本地redis配置文件_1024程序员节_11

hash:ziplist与hashtable转化

项目中使用到了redis的哈希结构 , 哈希结构的内部编码类型是 ziplist 和 hashtable

  • ziplist。当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
  • 当元素个数的个数小于hash-max-ziplist-entries(默认512个)
  • 当所有值大小都小于hash-max-ziplist-value(默认64字节)
  • 一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。
ziplist优势

1.连续的内存 , 可以极大的提升cpu的缓存命中率

ziplist最大的优势就是存储的时候是连续的内存 , 可以极大的提升cpu的缓存命中率, 但是,如果数据很大,则不会获得太多的增益,同时会花费大量的CPU时间, 因为几万几十万的列表压缩, 查询插入操作时,列表压缩的编解码造成比较大的性能损耗

2.节省内存空间

只除了字符串本身以外,ziplist需要存储两个整数,一个整数是前一个节点的长度,另外一个整数是当前节点的长度

列表List: 双链表结构,每个节点3个指针+两个长度存储+字符串本身=32位系统 一个指针6-7个字节
每个节点有: 3个指针18个字节+2个整数2字节=20字节

开启压缩列表单节点节省18个字节

  • 数据紧凑性:压缩列表通过将相邻的元素合并在一起,可以大大减少存储元素时的内存开销。相同或相似的元素在压缩列表中只需要存储一次,而在双端链表中,每个元素都需要独立的指针和数据存储。
  • 较少的指针:双端链表中的每个元素都需要维护前驱和后继指针,这些指针占据了额外的内存空间。而在压缩列表中,相邻元素合并在一起,只需要一个指针来表示这一组合并的元素。

redis基本类型底层实现

如何查找本地redis配置文件_数据库_12

比如我们执行一个命令 hset user age 25 在字典上的数据结构大概是这样,为了方便,string类型的对象就简画成了stringobject。

如何查找本地redis配置文件_1024程序员节_13


https://www.modb.pro/db/552315

string

字符串的实现

Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

如何查找本地redis配置文件_redis_14

字符串 内部结构
Redis 中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的。

C 语言里面的字符串标准形式是以 NULL 作为结束符,但是在 Redis 里面字符串不
是这么表示的。因为要获取 NULL 结尾的字符串的长度使用的是 strlen 标准库函数,这个函数的算法复杂度是 O(n),它需要对字节数组进行遍历扫描,作为单线程的 Redis 表示承受不起。

Redis 的字符串叫着「SDS」,也就是 Simple Dynamic String。它的结构是一个带长度信息的字节数组。

上面的 SDS 结构使用了范型 T,为什么不直接用 int 呢 ?

这是因为当字符串比较短时,len 和 capacity 可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。

Redis 规定字符串的长度不得超过 512M 字节。创建字符串时 len 和 capacity 一样长,不会多分配冗余空间,这是因为绝大多数场景下我们不会使用 append 操作来修改字符串。

RedisObject

我们首先来了解一下 Redis 对象头结构体,所有的 Redis 对象都有这样一个 RedisObject 对象头需要占据 16 字节( 4bit + 4bit + 24bit + 4bytes + 8bytes )的存储空间。

struct RedisObject {
	int4 type; // 4bits
	int4 encoding; // 4bits
	int24 lru; // 24bits
	int32 refcount; // 4bytes
	void *ptr; // 8bytes,64-bit system
} robj;
  • 不同的对象具有不同的类型 type(4bit),
  • 同一个类型的 type 会有不同的存储形式encoding(4bit),
  • 为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。
  • 每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。
  • ptr 指针将指向对象内容 (body) 的具体存储位置

这样一个 RedisObject 对象头需要占据 16 字节( 4bit + 4bit + 24bit + 4bytes + 8bytes )的存储空间。

SDS

接着我们再看 SDS 结构体的大小,在字符串比较小时,SDS 对象头的大小是capacity+3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)

struct SDS {
	int8 capacity; // 1byte
	int8 len; // 1byte
	int8 flags; // 1byte
	byte[] content;  // 内联数组,长度为 capacity
}
  • content 里面存储了真正的字符串内容
  • capacity 表示所分配数组的长度
  • len 表示字符串的实际长度

如何查找本地redis配置文件_big data_15

如图所示,embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。

而 raw 存储形式不一样,它需要两次malloc,两个对象头在内存地址上一般是不连续的。

而内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。

如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。

当内存分配器分配了 64 空间时,那这个字符串的长度最大可以是多少呢?这个长度就是 44。那为什么是 44 呢?

SDS 结构体中的 content 中的字符串是以字节\0 结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。

看上面这张图可以算出,留给 content 的长度最多只有 45(64-19) 字节了。字符串又是以\0 结尾,所以 embstr 最大能容纳的字符串长度就是 44。

redisObject我们可以看下源码,在server.h中有对于redisObject的定义,关于encoding,string类型有三个编码格式分别为int,embstr,raw这个区别在本文的最后做解释,因为需要有SDS的铺垫才可以。

关于string的三个编码的区别

1,int,存储8个字节的长整型,最大数字为2^63-1。
2,关于embstr和raw,embstr存储小于等于44个字节的字符串, raw 存储大于44个字节的字符串

bao test:0>set test111 1
OK
bao test:0>object encoding test111
int
  • embstr
    假设SDS中没有任何数据的情况下,emstr需要消耗的字节数就有 4位+4位+24位 = 4字节 系统如果是64位,则地址需要8字节存储,4+4+8+3 = 19字节, 64-19=45字节,在减去最后的buf的结束符’\0’所占用一字节,所以最终embstr能存储的字符串最大为44字节。
  • raw
  • 如何查找本地redis配置文件_数据库_16

  • 可以比较明显的体现,embstr所分配的内存是连续的,而raw所分配的内存是非连续的,所以这就导致了,embstr只需要分配一次内存,而raw需要分配两次内存(第一次为redisObject,第二次为SDS),相对地,释放内存的次数也由一次变为两次。

hash 字典 (ziplist+hash)

https://redisbook.readthedocs.io/en/latest/internal-datastruct/dict.html

字典实现
/*
 * 字典
 *
 * 每个字典使用两个哈希表,用于实现渐进式 rehash
 */
typedef struct dict {

    // 特定于类型的处理函数
    dictType *type;

    // 类型处理函数的私有数据
    void *privdata;

    // 哈希表(2 个)
    dictht ht[2];

    // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
    int rehashidx;

    // 当前正在运作的安全迭代器数量
    int iterators;

} dict;
/*
 * 哈希表
 */
typedef struct dictht {

    // 哈希表节点指针数组(俗称桶,bucket)
    dictEntry **table;

    // 指针数组的大小
    unsigned long size;

    // 指针数组的长度掩码,用于计算索引值
    unsigned long sizemask;

    // 哈希表现有的节点数量
    unsigned long used;

} dictht;

table 属性是个数组, 数组的每个元素都是个指向 dictEntry 结构的指针。

每个 dictEntry 都保存着一个键值对, 以及一个指向另一个 dictEntry 结构的指针:

/*
 * 哈希表节点
 */
typedef struct dictEntry {

    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 链往后继节点
    struct dictEntry *next;

} dictEntry;

如何查找本地redis配置文件_big data_17


如果再加上之前列出的 dict 类型,那么整个字典结构可以表示如下:

如何查找本地redis配置文件_1024程序员节_18

在上图的字典示例中, 字典虽然创建了两个哈希表, 但正在使用的只有 0 号哈希表, 这说明字典未进行 rehash 状态。

当Hash的数据项较少时,Hash底层才会用压缩列表zipList进行存储数据, 数据增加,底层的zipList会转成dict,

ziplist

如何查找本地redis配置文件_数据库_19


上图中可以看到,当数据量比较小的时候,我们会将所有的key及value都当成一个元素,顺序的存入到ziplist中,构成有序。

哈希算法

Redis 目前使用两种不同的哈希算法:

  • MurmurHash2 32 bit 算法:这种算法的分布率和速度都非常好, 具体信息请参考 MurmurHash 的主页:http://code.google.com/p/smhasher/ 。
  • 基于 djb 算法实现的一个大小写无关散列算法:具体信息请参考 http://www.cse.yorku.ca/~oz/hash.html 。

使用哪种算法取决于具体应用所处理的数据:

  • 命令表以及 Lua 脚本缓存都用到了算法 2 。
  • 算法 1 的应用则更加广泛:数据库、集群、哈希键、阻塞操作等功能都用到了这个算法。
什么是rehash

哈希冲突

如何查找本地redis配置文件_数据库_20

哈希表中桶的数量是有限的,当Key的数量较大时自然避免不了哈希冲突(多个Key落在了同一个哈希桶中)。当哈希桶中存在哈希冲突时那么多个Entry就形成了链表,每个链表中有一个Next指针指向了下一个元素。当哈希桶中的链表过长时,那么查询性能会显著降低(链表的查找时间复杂度为O(N)),Redis为了避免类似的问题从而会进行Rehash操作

为了能够减少哈希冲突,其实最直接的做法是增加哈希桶数量从而让元素能够更加均匀的分布在哈希表中。而Redis中的Rehash操作的原理其实也是如此,只不过他的设计更加巧妙。
Redis中其实有两个「全局哈希表」,一开始时默认使用的Hash Table1来存储数据,而Hash Table2并没有分配内存空间。随着Hash Table1中的元素越来越多时,Redis会进行Rehash操作。

首先会给Hash Table2分配一定的内存空间(肯定比哈希表一大),然后将Hash Table1中的元素重新映射至Hash Table2中,最后会释放Hash Table1。这样来看的话,Redis的Rehash操作的确能减少哈希冲突,但是你有没有想过如果Hash Table1中的元素特别多时,如果这么粗暴的将数据往Hash Table2中搬,那势必会阻塞Redis的主线程进而影响Redis的性能。其实Redis也考虑到了这个问题,那么接下来我们看看Redis是如何解决这种问题的

rehash的步骤

1.为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2.在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
3.在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值+1。
4.随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表

List (3.2前ziplist+linked_list 3.2后quicklist)

3.2 版本前采用ziplist和linkedlist结构
List 是一个有序(按加入的时序排序)的数据结构,一般有序我们会采用数组或者是双向链表,其中双向链表由于有前后指针实际上会很浪费内存。

3.2版本之前采用两种数据结构作为底层实现:

  • 压缩列表ziplist
  • 双向链表linkedlist

压缩列表相对于双向链表更节省内存,所以再创建列表时,会先考虑压缩列表,并在一定条件下才转化为双向链表。

压缩列表转化成双向链表的条件:

  • 如果添加的字符串元素长度超过默认值64
  • zip包含的节点数超过默认值512 这两个条件是可以修改的,在redis.conf中

3.2版本之后升级为 quicklist(双向链表)
quicklist是3.2版本之后引入的。

根据上文谈到,ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList。

quickList 是一个 ziplist 组成的双向链表。每个节点使用 ziplist 来保存数据。本质上来说,quicklist 里面保存着一个一个小的 ziplist。

Set(intset、hash)

其底层主要有整数数组(INTSET)和哈希表两种实现方式。当我们创建set的时候如果遇上成员是整形字符串时,会直接使用intset编码存储。intset的数据结构:

如何查找本地redis配置文件_redis_21

  • encoding表示编码格式:表示intset中每个元素采用以下哪种方式存储,有三种类型INT16(2字节),INT32(4个字节),INT64(8个字节)
  • length:元素个数,表示intset保存在contents中的元素个数
  • contents:实际存储的数据

其中:inset为可以理解为整数数组的一个有序集合,其内部是采用二分查找来定位元素位置。实际查询复杂度也就在log(n)

使用inset数据结构需要满足下述两个条件:

  • 元素个数少于默认值512 : set-max-inset-entries 512
  • 元素可以用整型表示

zset(ziplist+skiplist)

如何查找本地redis配置文件_1024程序员节_22

在redis中是通过两种底层数据结构实现的。

ziplist压缩列表 或者 skipList跳跃表与字典hash_table实现

  • ziplist:和 hashtable 和 list 一样,可以配置长度小于一定阈值时,降级成 ziplist 压缩列表实现,ziplist 实现是用一块连续的内存空间,节省了链表实现的前后节点,并且能够顺序查找
    数量小的时候使用 ziplist 实现,是因为小的连续空间容易申请。

zipList:

满足以下两个条件:

  • [score,value]键值对数量少于128个;
  • 每个元素的长度小于64字节;

zset 长度越来越大的时候难以申请一块足够大的连续空间,所以转而使用了skiplist 跳跃表实现

skipList:
不满足以上两个条件时使用跳表(组合了hash和skipList)

  • hash用来存储value到score的映射,这样就可以在O(1)时间内找到value对应的分数;
  • skipList按照从小到大的顺序存储分数;
  • skipList每个元素的值都是[score,value]对

虽然同时使用两种结构,但它们会通过指针来共享相同元素的 member 和 score,因此不会浪费额外的内存。

ziplist、skipList 为什么需要转换?

1.zset 的数据结构,为什么数量小的时候使用 ziplist

当刚开始选择了ziplist,会在下面两种情况下转为skipList。

ziplist所保存的元素超过服务器属性server.zset_max_ziplist_entries 的值(默认值为 128)
新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64)

那我们是否思考一下为什么需要转换呢?

ziplist 是一个紧挨着的存储空间,并且是没有预留空间的,随意对于ziplist优势在于节省空间,是因为小的连续空间容易申请,但是在容量大到一定成度扩容就是影响他的性能的主要原因之一。

基本数据结构 底层实现 总结

大多数情况下,Redis使用简单字符串SDS作为字符串的表示,相对于C语言字符串,SDS具有常数复杂度获取字符串长度,杜绝了缓存区的溢出,减少了修改字符串长度时所需的内存重分配次数,以及二进制安全能存储各种类型的文件,并且还兼容部分C函数。

通过为链表设置不同类型的特定函数,Redis链表可以保存各种不同类型的值,除了用作列表键,还在发布与订阅、慢查询、监视器等方面发挥作用(后面会介绍)。

  • string: int、embstr、raw
  • hash: Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用链地址法解决哈希冲突。
  • set: 整数集合是集合键的底层实现之一,底层由数组构成,升级特性能尽可能的节省内存。
  • zset:跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。
  • zset:压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一。

Redis链表的实现

Redis的链表实现比较简单,但具有五个主要特性:

  1. 双端:链表节点包含prev和next两个指针,分别指向前一个节点和后一个节点,这样可以在两个方向上遍历链表。
  2. 无环:链表的头节点和尾节点都指向null,表示链表的开始和结束。
  3. 带有表头和表尾指针:链表本身包含两个指针,一个指向头节点,一个指向尾节点,这样可以在O(1)时间复杂度内访问链表的头部和尾部。
  4. 长度计数器:链表还包含一个长度计数器,记录了链表包含的节点数。
  5. 多态:链表可以存储不同类型的数据,这主要通过将节点的值存储为void实现。
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
 
typedef struct list {
    listNode *head;
    listNode *tail;
    unsigned long len;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
} list;

链表在Redis中有广泛的应用,主要包括以下几个场景:

  • 列表数据类型:Redis的列表(List)数据类型就是用链表实现的。由于链表可以在O(1)时间复杂度内实现插入、删除、获取操作,因此非常适合用作列表类型。
  • 发布/订阅系统:Redis的发布/订阅系统使用链表保存所有订阅同一频道的客户端。当有新消息发布时,服务器只需遍历该链表,将消息发送给每个客户端。
  • 慢查询日志:Redis的慢查询日志功能使用链表保存执行时间超过一定阈值的命令,以便后续分析和优化。
  • Lua脚本队列:Redis使用链表实现了一个Lua脚本队列。由于Lua脚本可能需要较长时间来执行,这个队列可以确保Redis服务器在执行Lua脚本时不会阻塞其他操作。

Redis高可用、高性能

如何查找本地redis配置文件_1024程序员节_23

【2013年】Redis 哨兵模式(sentinel)

Redis 2.8版本还引入了Sentinel来实时监控Redis实例。是一个旨在帮助管理 Redis 实例的系统。它执行以下四个任务:监视(monitoring)、通知(notification)、自动故障转移(automatic failover)和配置提供者(configuration provider)。

Sentinel 本质上是主从模式

**Sentinel 本质上是主从模式,**与一般的主从模式不同的是,主节点的选举,不是从节点完成的,而是通过 Sentinel 来监控整个集群模式,发起主从选举。因此本质上 Redis Sentinel 有两个集群,一个是 Redis 数据集群,一个是哨兵集群。

故障转移过程

三个步骤:主观下线 -> 客观下线 -> 主节点故障

转移:

  • 首先 Sentinel 获取了主从结构的信息,而后向 所有的节点发送心跳检测,如果这个时候发现 某个节点没有回复,就把它标记为主观下线
  • 如果这个节点是主节点,那么 Sentinel 就询问 别的 Sentinel 节点主节点信息。如果大多数都 Sentinel 都认为主节点已经下线了,就认为主 节点已经客观下线
  • 当主节点已经客观下线,就要步入故障转移阶 段。故障转移分成两个步骤,一个是 Sentinel 要选举一个 leader,另外一个步骤是 Sentinel leader 挑一个主节点

如何查找本地redis配置文件_big data_24

Redis Sentinel 模式要注意有两个集群,一个是存放了 Redis 数据的集群,一个是监控这个数据集群的哨兵集群。于是就需要理解哨兵集群之间是如何监控的,如何就某件事达成协议,以及哨兵自身的容错。

【2015】Redis 集群模式(Cluster)

2015年,Redis3.0版本支持了集群模式。Redis集群是一种分布式数据库解决方案,通过分片管理数据,数据分为16384个槽位(0~16383区间),每个节点负责槽位的一部分。

如何查找本地redis配置文件_如何查找本地redis配置文件_25

Redis Cluster 集成了对等模式和主从模式。Redis Cluster 由多个节点组成,每个节点都可以是一个主从集群。Redis 将 key 映射为 16384 个槽(slot),均匀分配在所有节点上。

两种模式下的主从同步都有全量同步和增量同步两种(引导面试官询问两种同步模式细节),一般情况下,我们应该尽量避免全量同步(钓鱼,面试官接着就会问为什么,或者全量同步有啥缺点,或者如何避免)

一般而言,如果数据量和复杂并不大的时候,想要保证高可用,就采用 Redis Sentinel;如果负载很大,或者说触及了 Redis 单机瓶颈,那么应该采用 Redis Cluster 模式。

迁移(重分片)

重分片的时候,会触发槽迁移,也就是把一部分数据挪到另外一个部分。

这个步骤是渐进式的

在迁移过程中,一个槽的部分key能在源节点,一部分在目标节点。因此如果请求过来,打到源节点,源节点发现已经迁移了,就会返回
一个ask重定向的错误,这个错误会引导客户端直接去访问目标节点。

【2013】Redis 主从复制 (replication)

Redis采用主从复制模型,通过主从节点之间的同步,可以实现写扩展(写主节点)和读扩展(读从节点)。主从同步的作用主要包括:

  • (1) 数据冗余:主节点的数据可以同步到多个从节点,实现数据冗余,提高数据可靠性。
  • (2) 故障自动转移:主节点故障时,可以自动切换到从节点,继续提供服务,实现高可用(sentry或者集群模式下)。
  • (3) 负载均衡:主节点负责写,从节点负责读,实现读写分离,降低主节点压力。
  • (4) 升级无停机:升级时可先升级从节点,再切换主从角色。

所以,Redis通过主从同步机制,实现了数据的高可用和扩展性,是一个非常重要的特性。它保证了Redis集群的高可靠性和可用性

主从复制过程

当master与所属slave首次建立连接后会进行全量rdb+replication_buffer,后期(即使中间有断开连接,又重新恢复主从关系)优先使用增量同步数据,实在无法增量(比如:主的复制积压缓冲区repl_baklog_buffer爆满后,会重新落地rdb,然后主从就无法通过各自的offset偏移量计算出增量数据),才会走全量rdb。

增量传输计算规则: master_repl_offset 和 slave_repl_offset ,中间相差的这一部分,即为本次要增量传输的。

注:slave每次和master建立通信时,将会发送psync命令(包含复制的偏移量offset),请求partial resync(增量数据同步)。如果请求的offset不存在,那么执行全量的sync操作,相当于重新建立主从复制。

如何查找本地redis配置文件_big data_26

  • runID: 每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例;
  • offset: 同步偏移量。

主从复制过程大致可分为3个阶段:建立连接阶段、数据同步阶段、命令传播阶段

  • 第一阶段:建立链接、协商同步
  • 在执行replicaof命令后,slave会向master发送psync命令,携带master的runID(实例ID)和offset(复制进度)参数,因为是第一次请求复制,所以runID为?,offset为-1。
  • master收到psync命令后返回 fullresync命令,并携带master的runID和offset,slave保存。这一步的目的是为全量复制做准备。
  • 第二阶段:数据同步
  • 完成第一阶段工作后,master会执行bgsave命令生成RDB文件,并发送给slave。生成 BG SAVE 过程中的写命令也会被放入缓冲队列(repl_backlog_buffer和replication_buffer);
  • slave在收到RDB文件后,先清空本地数据,再加载新的RDB文件数据。最后主库会把执行过程中replication_buffer缓冲区中的的写命令,再发送给从库,完成全量复制
  • 第三阶段:命令传播(增量同步)
  • 在完成第一次全量复制后,master与slave会建立一个长连接,。
  • 同时,master也会通过这个长连接将repl_backlog_buffer数据传播给slave,从 节点执行这些命令, 来保证数据一致性。后面主节点通过长链接 源源不断发送新的命令;
  • 四.级联传播
    从节点也可以拥有从节点,也就是级联传播。从节点收到主节点执行的命令后,除了自身执行,也会将命令复制给它自己的从节点。
增量复制的过程

而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。

  • 当主从连接时,会将新的操作的命令写入 replication buffer 和 repl_backlog_buffer 缓冲区中
  • repl_backlog_buffer 是一个环形缓冲区,主库会记录当前的偏移量 master_repl_offset,记录自己写到的位置,而从库在上面也有对应的偏移量 slave_repl_offset,记录自己正在读到的偏移量。
  • 在恢复连接时,从库会通过 psync 命令将自己的偏移量 slave_repl_offset 发送给主库,主库会将 slave_repl_offset 和 master_repl_offset 之间的命令同步给从库即可。

举了例子: 主库和从库之间相差了 put d e 和 put d f 两个操作,在增量复制时,主库只需要把它们同步给从库,就行了。

因为 replication buffer 是一个环形的缓存,当主从库长期断开时,是有可能被覆盖掉旧的数据,这个时候是会重新发起全量复制,主库根据从库发送的 slave_repl_offset 来判断是增量还是全量的复制。

为什么全量复制使用 RDB 而不是使用 AOF 呢?
  • RDB 文件是经过压缩的二进制文件,AOF 文件是记录每一次的操作,包含对同一个 key 的多次冗余操作,文件比 RDB 要大的多,使用 RDB 可以减少带宽
  • RDB 是二进制数据,从库还原速度快。而 AOF 需要依次重放每一个命令,恢复速度慢。

全量、增量同步区别

全量同步的缺点
  • CUP 和 内存,缺页异常
  • IO负载
  • 网络负载
  • 失败会重新引发全量,循环往复

如何避免

  • 安全重启
  • 增大缓冲区
如果过程发生了网络中断或者阻塞,该如何解决?
  • 在 Redis 2.8 之前,从库只能和主库重新发起全量同步,对于较大的 RDB 文件,网络恢复时间较长;( 即便是抖动后断开又恢复网络连接,但此时 TCP 连接已经断开,数据肯定是需要重新同步了。那么很容易 陷入到无休止的全量同步之中。)
  • 从 Redis 2.8 开始,从库已支持增量同步,只会把断开的时候没有发生的写命令,同步给从库。

如何查找本地redis配置文件_big data_27

  • savle在恢复网络后,会发送 psync 命令给master,此时的 psync 命令里的 offset 参数不是 -1。
  • master收到该命令后,然后用 continue 响应命令告诉slave接下来采用增量复制的方式同步数据, 然后master将断网期间写入命令发送给slave,然后slave再执行这些命令。

注意点:断开重连并不一定是增量复制。如上图所示,repl backlog为环形结构,如果网络断开时间太长,写入命令如果超过1M,旧的命令就会被覆盖。因此如果master offset和slave offset相差的数据已被覆盖则会通过全量复制。因此,repl backlog可以适当配置大一些。

Redis缓冲区

replication_buffer: replication 顾名思义,就是复制的意思;
buffer是缓冲区的意思,两者合在一起replication_buffer就是复制缓冲区;

replication_backlog_buffer: backlog英文释义,是积压的意思; 三者合在一起 replication_backlog_buffer,就是复制积压缓冲区

如何查找本地redis配置文件_big data_28

Replication Buffer 复制缓冲区

Redis 是一种基于内存的键值存储数据库,它采用异步复制(Asynchronous
Replication)来实现数据的主从备份和数据的高可用性,主要通过两个缓冲区来实现复制功能,即 Replication Buffer 和
Repl Backlog Buffer。

Replication Buffer 在全量复制阶段和增量复制阶段都会出现

Replication Buffer是什么

Replication Buffer 是 Redis 主服务器中的一个缓冲区,主要用于保存写命令请求,它以固定大小的循环数组的形式存储,当数组被写满之后,新写入的元素会从数组头部覆盖老数据。当主服务器接收到一个写命令请求时,就会将该命令序列化成 RDB 格式或 AOF 格式,并存放到 Replication Buffer 中,等待复制给从服务器。

如何查找本地redis配置文件_1024程序员节_29

主从复制
第一阶段,全量rdb+ replication_buffer。
第二阶段(命令传播),主要是增量传输,此时replication_backlog_buffer出场。

  • 在全量复制过程中,主节点再向从节点传输RDB文件的同时,会继续接收客户端发送的写命令请求。
  • 这些写命令会先保存复制缓冲区中,等RDB文件传输完成后 ,再发送给从节点去执行。
  • 主节点会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
复制缓冲区溢出问题

在实际使用中,如果主服务器执行写入命令的速度大于从服务器复制命令的速度(从节点接收和加载RDB较慢),那么 Replication Buffer 就会被耗尽,这时主服务器会停止处理写入命令请求,直到有新的空间可用。因此,在进行大量写入操作时,需要注意调整 Replication Buffer 的大小以及增加从服务器数量等设置。

Repl Backlog Buffer 复制积压缓冲区(增量复制、主只有一个)

复制积压缓冲区:增量复制时使用的缓冲区。

如何查找本地redis配置文件_如何查找本地redis配置文件_30

如何查找本地redis配置文件_1024程序员节_31

  • 当主从全量rdb后,master会把rdb通信期间收到新的数据的操作命令,写入 replication buffer,同时也会把这些数据操作命令也写入 repl_backlog_buffer 这个缓冲区,它里面保存着最新传输的命令。
  • 一旦从节点发生网络闪断,再次于主节点恢复连接后,从节点会从复制积压缓冲区 读取断链期间主节点接收到的写命令,进而进行增量同步
    当从服务器进行复制时,它会向主服务器发送一个 PSYNC 命令,主服务器根据复制 ID 和 offset 等信息来确定从服务器的数据同步情况,并将未同步的数据发送给从服务器。
    主服务器通过 Repl Backlog Buffer 来保存已发送但尚未同步的数据,从而保证从服务器可以正确复制数据,并且可以从上一次同步失败的地方继续复制。
复制积压缓冲区的溢出问题
  • 复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满以后,会覆盖缓冲区中的旧命令。如果节点还没同步这些旧命令,就会造成主从节点间重新开始执行全量复制。
  • 为了应对复制冲压缓冲区的溢出问题,我们可以调整复制积压缓冲区的大小。

总述:复制缓冲区,主要用于首次rdb后的首次增量同步数据,复制积压缓冲区主要用于常规的增量数据同步。当无法完成增量同步时,就会全量同步,全量同步后的第一次增量,仍是使用复制缓冲区。

主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?

出现的阶段不一样:

  • repl backlog buffer 是在增量复制阶段出现,一个主节点只分配一个 repl backlog buffer;
  • replication buffer 是在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点,分配一个 replication buffer;

这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:

  • 当 repl backlog buffer 满了,因为是环形结构,会直接覆盖起始位置数据;
  • 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制。

Redis底层持久化实现

首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,所以提供了RDB和AOF两种持久化机制。

Redis 的持久化机制分成两种,RDB 和 AOF。

2.1 持久化 rdb

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

RDB快照的触发方式有很多,比如

  • 执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
  • 根据redis.conf文件里面的配置,自动触发bgsave
  • 主从复制的时候触发

如何查找本地redis配置文件_数据库_32

RDB 也是主从全量同步里的 RDB。 RDB 可以理解为是一个快照,直接把 Redis 内存中的数据以快照的形式保存下 来。因为这个过程很消耗资源,所以分成 SAVE 和 BG SAVE 两种。BG SAVE的核 心是利用 fork 和 COW 机制。

所以他是一个全量的方式来进行持久化的

2.1.1 Redis BG Save

利用fork系统调用,复制出来一个子进程,子进程尝试将数据写入文件。这个时候, 子进程和主进程是共享内存的,当主进程发生写操作,那么就会复制一份内存, 这就是所谓的 COW (copy on write)。

COW 的核心是利用缺页异常,操作系统在捕捉到缺页异常之后,发现他们共享内存了,就会复制出来一份。

如何查找本地redis配置文件_big data_33

2.2 持久化 aof

AOF持久化,它是一种近乎实时的方式,,AOF 是将 Redis 的命令逐条保留下来,而 后通过重放这些命令来复原。我们可以通过重写 AOF 来减少资源消耗。

就是客户端执行一个数据变更的操作,Redis Server就会把 Redis 的命令逐条保留下来,追加到aof缓冲区的末尾,

然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。

  • 逐条记录命令
  • AOF 刷新磁盘的时机
  • always: 每次都刷盘
  • everysec(默认): 每秒,这意味着一般情况下会 丢失一秒钟的数据。而实际上,考虑到硬盘阻塞(见后面**使用 everysec 输盘策略 有什么缺点),那么可能丢失两秒的数据。
  • no: 由操作系统决定

• 可以通过重写来合并 AOF 文件

如何查找本地redis配置文件_如何查找本地redis配置文件_34

2.2.1 AOF 刷盘对比

AOF 刷新磁盘的时机

  • always: 每次都刷盘
  • everysec: 每秒,这意味着一般情况下会丢失一 秒钟的数据。而实际上,考虑到硬盘阻塞(见 后面**使用 everysec 输盘策略有什么缺点), 那么可能丢失两秒的数据。
  • no: 由操作系统决定

MySQL redo log 刷盘:

  • 写到 log buffer , 每秒刷新;
  • 实时刷新;
  • 写到 OS cache, 每秒刷新

MySQL bin log 刷盘:

  • 系统自由判断
  • commit刷盘
  • 每N个事务刷盘

写入语义

  • 中间件写到日志缓存(程序内缓存)就认为写入了;
  • 中间件写入到系统缓存(page cache)就认为写入了;
  • 中间件强制刷新到磁盘(发起了 fsync)就认为写入了;
2.2.2 重写 AOF

重写 AOF 整体类似于 RDB。

另外,因为AOF这种指令追加的方式,会造成AOF文件过大,带来明显的IO性能问题,所以Redis针对这种情况提供了

AOF重写机制,也就是说当AOF文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进行压缩。

在这个过程中,Redis 还在源源不断执行命令, 这部分命令将会被写入一个 AOF 的缓存队列 里面。当子进程写完 AOF 之后,发一个信号给主进程,主进程负责把缓冲队列里面的数 据写入到新 AOF。而后用新的 AOF 替换掉老 的 AOF。

如何查找本地redis配置文件_数据库_35

2.3 混合持久化

Redis4.0 后大部分的使用场景都不会单独使用 RDB 或者 AOF 来做持久化机制,而是兼顾二者的优势混合使用。其原因是 RDB 虽然快,但是会丢失比较多的数据,不能保证数据完整性;AOF 虽然能尽可能保证数据完整性,但是性能确实是一个诟病,比如重放恢复数据。

Redis从4.0版本开始引入 RDB-AOF 混合持久化模式,这种模式是基于 AOF 持久化模式构建而来的,混合持久化通过 aof-use-rdb-preamble yes 开启。

将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF
日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF
全量文件重放,重启效率因此大幅得到提升。

如何查找本地redis配置文件_big data_36

那么 Redis 服务器在执行 AOF 重写操作时,就会像执行 BGSAVE 命令那样,根据数据库当前的状态生成出相应的 RDB 数据,并将这些数据写入新建的 AOF 文件中,至于那些在 AOF 重写开始之后执行的 Redis 命令,则会继续以协议文本的方式追加到新 AOF 文件的末尾,即已有的 RDB 数据的后面。

换句话说,在开启了 RDB-AOF 混合持久化功能之后,服务器生成的 AOF 文件将由两个部分组成,其中位于 AOF 文件开头的是 RDB 格式的数据,而跟在 RDB 数据后面的则是 AOF 格式的数据。

当一个支持 RDB-AOF 混合持久化模式的 Redis 服务器启动并载入 AOF 文件时,它会检查 AOF 文件的开头是否包含了 RDB 格式的内容

  • 如果包含,那么服务器就会先载入开头的 RDB 数据,然后再载入之后的 AOF 数据。
  • 如果 AOF 文件只包含 AOF 数据,那么服务器将直接载入 AOF 数据。

如何查找本地redis配置文件_1024程序员节_37

最后来总结这两者,到底用哪个更好呢?

推荐是两者均开启。

  • 如果对数据不敏感,可以选单独用 RDB。
  • 如果只是做纯内存缓存,可以都不用。

因此,基于对RDB和AOF的工作原理的理解,我认为RDB和AOF的优缺点有两个。

  • RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高
  • RDB文件默认采用压缩的方式持久化,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好

Redis 内存淘汰策略

内存淘汰策略

在 Redis 4.0 版本之前有 6 种策略,4.0 增加了 2种,主要新增了 LFU 算法。

下图为 Redis 6.2.0 版本的配置文件:

如何查找本地redis配置文件_如何查找本地redis配置文件_38

如何查找本地redis配置文件_如何查找本地redis配置文件_39

1、noevction(不进行数据淘汰)

默认情况下,Redis在使用的内存空间超过maxmemory值时,并不会淘汰数据,也就是设定的noeviction策略。对应到Redis缓存,也就是指,一旦缓存被写满了,不会删除任何数据,拒绝所有写入操作并返回客户端错误消息(error)OOM command not allowed when used memory,此时 Redis 只响应删和读操作;

2、在设置了过期时间的数据中进行淘汰

我们再分析下volatile-random、volatile-ttl、volatile-lru和volatile-lfu这四种淘汰策略。它们筛选的候选数据范围,被限制在已经设置了过期时间的键值对上。也正因为此,即使缓存没有写满,这些数据如果过期了,也会被删除。

例如,我们使用EXPIRE命令对一批键值对设置了过期时间后,无论是这些键值对的过期时间是快到了,还是Redis的内存使用量达到了maxmemory阈值,Redis都会进一步按照volatile-ttl、volatile-random、volatile-lru、volatile-lfu这四种策略的具体筛选规则进行淘汰。

  • volatile-ttl在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
    volatile-random就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
  • volatile-lru会使用LRU算法筛选设置了过期时间的键值对。
  • volatile-lfu会使用LFU算法选择设置了过期时间的键值对。

volatile-ttl和volatile-random筛选规则比较简单,而volatile-lru因为涉及了LRU算法,所以我会在分析allkeys-lru策略时再详细解释。volatile-lfu使用了LFU算法,它是在LRU算法的基础上,同时考虑了数据的访问时效性和数据的访问次数,可以看作是对淘汰策略的优化。

3、在所有数据中淘汰

相对于volatile-ttl、volatile-random、volatile-lru、volatile-lfu这四种策略淘汰的是设置了过期时间的数据,allkeys-lru、allkeys-random、allkeys-lfu这三种淘汰策略的备选淘汰数据范围,就扩大到了所有键值对,无论这些键值对是否设置了过期时间。它们筛选数据进行淘汰的规则是:

  • allkeys-random策略,从所有键值对中随机选择并删除数据;
  • allkeys-lru策略,使用LRU算法在所有数据中进行筛选。
  • allkeys-lfu策略,使用LFU算法在所有数据中进行筛选。

这也就是说,如果一个键值对被删除策略选中了,即使它的过期时间还没到,也需要被删除。当然,如果它的过期时间到了但未被策略选中,同样也会被删除。

Redis是如何淘汰key的?

Redis 会在 3种场景下对 key 进行淘汰,

  • 查询时发现 超过阈值
  • 懒惰删除:是在执行命令时,检查淘汰 key。
  • 定期删除:在定期执行 serverCron 时,检查淘汰 key;
Redis 在执行命令请求时,检查当前内存占用

Redis 在执行命令请求时。会检查当前内存占用是否超过 maxmemory 的数值,而超过内存阀值后的淘汰策略,是通过 maxmemory-policy 设置的如果超过,则按照设置的淘汰策略,进行删除淘汰 key 操作。

如果 Redis 开启了持久化和主从同步,那么 Redis 的过期处理要复杂 一些。

  • 在 RDB 之下,加载 RDB 会忽略已经过期的 key;(RDB 不读)
  • 在 AOF 之下,重写 AOF 会忽略已经过期的 key;(AOF 不写)
  • 主从同步之下,从服务器等待主服务器的删除命令;(从服务器啥也不 干)

定期执行淘汰(简单贪婪策略)

遍历每个数据库(就是redis.conf中配置的”database”数量,默认为16)

如何查找本地redis配置文件_redis_40


Redis 定期执行 serverCron 时,会对 DB 进行检测,清理过期 key。清理流程如下。

首先轮询每个 DB(默认16个),检查其 expire dict(过期字典),即带过期时间的过期 key 字典,从所有带过期时间的 key 中,随机选取 20 个样本 key,检查这些 key 是否过期,如果过期则清理删除。如果 20 个样本中,超过 5 个 key 都过期,即过期比例大于 25%,就继续从该 DB 的 expire dict 过期字典中,再随机取样 20 个 key 进行过期清理,持续循环,直到选择的 20 个样本 key 中,过期的 key 数小于等于 5,当前这个 DB 则清理完毕,然后继续轮询下一个 DB。

在执行 serverCron 时,如果在某个 DB 中,过期 dict 的填充率低于 1%,则放弃对该 DB 的取样检查,因为效率太低。

如果 DB 的过期 dict 中,过期 key 太多,一直持续循环回收,会占用大量主线程时间,所以 Redis 还设置了一个过期时间。这个过期时间根据 serverCron 的执行频率来计算,5.0 版本及之前采用慢循环过期策略,默认是 25ms,如果回收超过 25ms 则停止,6.0 非稳定版本采用快循环策略,过期时间为 1ms。

惰性删除

除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓 惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期 了就立即删除。*定时删除是集中处理,惰性删除是零散处理。

/*
 * Redis 对象
 */
typedefstruct redisObject {

    // 类型
    unsigned type:4;

    // 编码方式
    unsigned encoding:4;

    // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
    unsigned lru:LRU_BITS; // LRU_BITS: 24

    // 引用计数
    int refcount;

    // 指向底层数据结构实例
    void *ptr;

} robj;

Redis IO模型

Redis基于Reactor模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder)。

文件事件处理器由Socket、IO多路复用程序、文件事件分派器(dispather),事件处理器(handler)四部分组成,文件事件处理器的模型如下所示:

如何查找本地redis配置文件_如何查找本地redis配置文件_41


IO多路复用程序会同时监听多个socket,当被监听的socket准备好执行accept、read、write、close等操作时,与这些操作相对应的文件事件就会产生。IO多路复用程序会把所有产生事件的socket压入一个队列中,然后有序地每次仅一个socket的方式传送给文件事件分派器,文件事件分派器接收到socket之后会根据socket产生的事件类型调用对应的事件处理器进行处理。

文件事件处理器分为几种:

  • 连接应答处理器:用于处理客户端的连接请求;
  • 命令请求处理器:用于执行客户端传递过来的命令,比如常见的set、lpush等;
  • 命令回复处理器:用于返回客户端命令的执行结果,比如set、get等命令的结果;

Redis的客户端与服务端的交互

如何查找本地redis配置文件_big data_42

多路复用程序会监听不同套接字的事件

当某个事件,比如发来了一个请求,那么多路复用程序就把这个套接字丢过去套接字队列,事件分派器从 队列里边找到套接字,丢给对应的事件处理器处理。

如何查找本地redis配置文件_如何查找本地redis配置文件_43

如何查找本地redis配置文件_redis_44

单线程性能瓶颈与Redis 6.0多线程

Redis 慢的主要原因是单进程单线程模型。虽然一些重量级操作也进行了分拆,如 RDB 的构建在子进程中进行,文件关闭、文件缓冲同步,以及大 key 清理都放在 BIO 线程异步处理,但还远远不够。线上 Redis 处理用户请求时,十万级的 client 挂在一个 Redis 实例上,所有的事件处理、读请求、命令解析、命令执行,以及最后的响应回复,都由主线程完成,纵然是 Redis 各种极端优化,巧妇难为无米之炊,一个线程的处理能力始终是有上限的。

当前服务器 CPU 大多是 16 核到 32 核以上,Redis 日常运行主要只使用 1 个核心,其他 CPU 核就没有被很好的利用起来,Redis 的处理性能也就无法有效地提升。而 Memcached 则可以按照服务器的 CPU 核心数,配置数十个线程,这些线程并发进行 IO 读写、任务处理,处理性能可以提高一个数量级以上。

面对性能提升困境,虽然 Redis 作者不以为然,认为可以通过多部署几个 Redis 实例来达到类似多线程的效果。但多实例部署则带来了运维复杂的问题,而且单机多实例部署,会相互影响,进一步增大运维的复杂度。为此,社区一直有种声音,希望 Redis 能开发多线程版本。

因此,Redis 即将在 6.0 版本引入多线程模型。Redis 的多线程模型,分为主线程和 IO 线程。

因为处理命令请求的几个耗时点,分别是请求读取、协议解析、协议执行,以及响应回复等。所以 Redis 引入 IO 多线程,并发地进行请求命令的读取、解析,以及响应的回复。
而其他的所有任务,如事件触发、命令执行、IO 任务分发,以及其他各种核心操作,仍然在主线程中进行,也就说这些任务仍然由单线程处理。这样可以在最大程度不改变原处理流程的情况下,引入多线程。

Redis 6.0 版本中新引入的多线程模型,主要是指可配置多个 IO 线程,这些线程专门负责请求读取、解析,以及响应的回复。通过 IO 多线程,Redis 的性能可以提升 1 倍以上。

多线程方案优劣

虽然多线程方案能提升1倍以上的性能,但整个方案仍然比较粗糙。

首先所有命令的执行仍然在主线程中进行,存在性能瓶颈。然后所有的事件触发也是在主线程中进行,也依然无法有效使用多核心。

而且,IO 读写为批处理读写,即所有 IO 线程先一起读完所有请求,待主线程解析处理完毕后,所有 IO 线程再一起回复所有响应,不同请求需要相互等待,效率不高。最后在 IO 批处理读写时,主线程自旋检测等待,效率更是低下,即便任务很少,也很容易把 CPU 打满。

整个多线程方案比较粗糙,所以性能提升也很有限,也就 1~2 倍多一点而已。要想更大幅提升处理性能,命令的执行、事件的触发等都需要分拆到不同线程中进行,而且多线程处理模型也需要优化,各个线程自行进行 IO 读写和执行,互不干扰、等待与竞争,才能真正高效地利用服务器多核心,达到性能数量级的提升。

redis运行模式

单副本模式

Redis 单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。

优点:

  • 1、架构简单、部署方便
  • 2、高性价比,当缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务。
    3、高性能

缺点:

  • 1、不保证数据的可靠性
  • 2、当缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务。
  • 3、高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用memcached替代。

主从模式(多副本):

优点:

  • 1、高可靠性,一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行。另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题。
  • 2、读写分离策略,从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。
  • 3、负载均衡:可以通过将读操作分发到不同的从节点上实现负载均衡。
    缺点:
  • 1、故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其他从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐。
  • 2、主库的写能力受到单机的限制,可以考虑分片
  • 3、主库的存储能力受到单机的限制,可以考虑Pika
  • 4、原生复制的弊端在早期的版本也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求。建议升级到最新版本。

哨兵模式:

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用,这时候就需要哨兵模式登场了。哨兵模式是从Redis的2.6版本开始提供的,但是当时这个版本的模式是不稳定的,直到Redis的2.8版本以后,这个哨兵模式才稳定下来

Redis Sentinel是社区版本推出的原生高可用解决方案,Redis Sentinel部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群,其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群。可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个。

优点:

  • Redis Sentinel集群部署简单
  • 自动故障转移:哨兵可以监控主节点和从节点的状态,自动进行故障转移。
  • 高可用性:哨兵可以自动选择新的主节点,从而保持系统的可用性。

缺点:

  • 哨兵本身成为单点故障:如果哨兵集群出现问题,整个系统可能受到影响。
  • 故障转移需要时间:故障转移过程中可能会有一段时间的不可用性。

如何查找本地redis配置文件_redis_45


在上图过程中,哨兵主要有两个重要作用:

  • 第一:哨兵节点会以每秒一次的频率对每个 Redis 节点发送PING命令,并通过 Redis 节点的回复来判断其运行状态。
  • 第二:当哨兵监测到主服务器发生故障时,会自动在从节点中选择一台将机器,并其提升为主服务器,然后使用 PubSub 发布订阅模式,通知其他的从节点,修改配置文件,跟随新的主服务器。
主观下线和客观下线

哨兵节点发送ping命令时,当超过一定时间(down-after-millisecond)后,如果节点未回复,则哨兵认为主观下线。主观下线表示当前哨兵认为该节点已经下面,如果该节点为主数据库,哨兵会进一步判断是够需要对其进行故障切换,这时候就要发送命令(SENTINEL is-master-down-by-addr)询问其他哨兵节点是否认为该主节点是主观下线,当达到指定数量(quorum)时,哨兵就会认为是客观下线。

主从切换的步骤

当主节点客观下线时就需要进行主从切换,主从切换的步骤为:

  • 选出领头哨兵。
  • 领头哨兵所有的slave选出优先级最高的从数据库。优先级可以通过slave-priority选项设置。
  • 如果优先级相同,则从复制的命令偏移量越大(即复制同步数据越多,数据越新),越优先。
  • 如果以上条件都一样,则选择run ID较小的从数据库。
  • 选出一个从数据库后,哨兵发送slave no one命令升级为主数据库,并发送slaveof命令将其他从节点的主数据库设置为新的主数据库。
哨兵模式优缺点

1.优点

哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。

2.不足-问题

  • 是一种中心化的集群实现方案:始终只有一个Redis主机来接收和处理写请求,写操作受单机瓶颈影响。
  • 集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储。数据量过大时,主从同步严重影响master的性能。
  • Redis主机宕机后,哨兵模式正在投票选举的情况之外,因为投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
  • 主从模式或哨兵模式每个节点存储的数据都是全量的数据,数据量过大时,就需要对存储的数据进行分片后存储到多个redis实例上。此时就要用到Redis Sharding技术。

集群模式:

redis在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis
节点上存储不同的数据。cluster模式为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。

Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。Redis Cluster集群采用了P2P的模式,完全去中心化。

如何查找本地redis配置文件_big data_46

Redis Cluster集群具有如下几个特点:
  • 集群完全去中心化,采用多主多从;所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  • 客户端与 Redis 节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  • 每一个分区都是由一个Redis主机和多个从机组成,分片和分片之间是相互平行的。
  • 每一个master节点负责维护一部分槽,以及槽所映射的键值数据;集群中每个节点都有全量的槽信息,通过槽每个node都知道具体数据存储到哪个node上。

Redis Cluster采用虚拟哈希槽分区而非一致性hash算法,预先分配一些卡槽,所有的键根据哈希函数映射到这些槽内,每一个分区内的master节点负责维护一部分槽以及槽所映射的键值数据。

集群如何判断某个主节点挂掉?

首先要说的是,每一个节点都存有这个集群所有主节点以及从节点的信息。它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个主节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用从节点。

判断节点挂掉是通过投票完成的,投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉。

集群如何选举新的主节点?

选举的优先级依据依次是:网络连接正常->5秒内回复过INFO命令->10秒(down-after-milliseconds)内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。选出之后通过slaveif no ont将该从服务器升为新主服务器。

选举出新的主节点后: 通过slaveof ip port命令让其他从节点器复制该新主节点的信息。

优点:

  • 1、无中心架构
  • 2、自动分片:数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  • 3、可扩展性,可线性扩展到1000多个节点,节点可动态添加或删除。
  • 4、高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
  • 5、降低运维成本,提高系统的扩展性和可用性。

缺点:

1、Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
2、节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
3、数据通过异步复制,不保证数据的强一致性。
4、多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
5、Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。
6、key批量操作限制,如使用mset、mget目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于keys 不支持跨slot查询,所以执行mset、mget、sunion等操作支持不友好。
7、key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个key分布于不同的节点上时无法使用事务功能。
8、key作为数据分区的最小粒度,因此不能将一个很大的键值对象如hash、list等映射到不同的节点。
9、不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db 0。
10、复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
11、避免产生hot-key,导致主库节点成为系统的短板。
12、避免产生big-key,导致网卡撑爆、慢查询等。
13、重试时间应该大于cluster-node-time时间
14、Redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

redis cluster的数据迁移方案

整体流程

redis官方文档中提供的数据迁移办法是借助redis-trib脚本,其实严格来说,这个redis-trib并不是redis本体的一部分,它只是官方按照redis设计规范实现的一套脚本集合,帮助用户更方便的使用redis-cluster。 实际上,我们完全可以脱离这个脚本来使用cluster, 或者用其他方式实现这套逻辑,比如搜狐tv的redis运维工具cachecloud里,就用java实现了整套逻辑。

我们可以参考redis-trip或者cachecloud的代码来了解cluster数据迁移的流程,主要分为如下几步:

  • 设定迁移中的节点状态,比如要把slot x的数据从节点A迁移到节点B的话,需要把A设置成MIGRATING状态,B设置成IMPORTING状态。
    CLUSTER SETSLOT <slot> IMPORTING <node_id>CLUSTER SETSLOT <slot> MIGRATING <node_id>
  • 迁移数据,这一步首先使用CLUSTER GETKEYSINSLOT 命令获取该slot中所有的key, 然后每个key依次用MIGRATE命令转移数据。
  • 数据转移完毕之后,正式将slot指派给新的节点B1

可用性

在整个迁移中,会出现对于单个key的阻塞情况,原因是MIGRATE命令是原子性的,在单个key的迁移过程中,对这个key的访问会被阻塞。但是,一般来说,一个key的数据不会特别大,所以绝大多数情况下瞬间都能完成,所以一般不会真正影响使用。而其他任何情况都不会造成集群的不可用,如果出现了,比如出现slot级的不可用,说明client端的处理存在某些问题。接下来,本文也会介绍一些client端使用的注意事项。

一致性 Hash

一致性 hash 是将数据按照特征值映射到一个首尾相接的 hash 环上,同时也将节点(按照 IP 地址或者机器名 hash)映射到这个环上。

对于数据,从数据在环上的位置开始,顺时针找到的第一个节点即为数据的存储节点。

余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing 中,只有在园(continuum)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。

如何查找本地redis配置文件_如何查找本地redis配置文件_47

数据倾斜问题

一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。

如何查找本地redis配置文件_redis_48

此时必然造成大量数据集中到 Node A 上,而只有极少量会定位到 Node B 上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。

虚拟节点

具体做法可以在服务器 IP 或主机名的后面增加编号来实现。
例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算
“Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点。
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到
“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到 Node A 上。这样就解决了服务节点少时数据倾斜的问题。

如何查找本地redis配置文件_数据库_49

redis管理

redis异步线程

除了主进程,Redis 还会 fork 一个子进程,来进行重负荷任务的处理。Redis fork 子进程主要有 3 种场景。

除了主进程,Redis 还会 fork 一个子进程,来进行重负荷任务的处理。Redis fork 子进程主要有 3 种场景。

  • 收到 bgrewriteaof 命令时,Redis 调用 fork,构建一个子进程,子进程往临时 AOF文件中,写入重建数据库状态的所有命令,当写入完毕,子进程则通知父进程,父进程把新增的写操作也追加到临时 AOF 文件,然后将临时文件替换老的 AOF 文件,并重命名。
  • 收到 bgsave 命令时,Redis 构建子进程,子进程将内存中的所有数据通过快照做一次持久化落地,写入到 RDB 中。
  • 当需要进行全量复制时,master 也会启动一个子进程,子进程将数据库快照保存到 RDB 文件,在写完 RDB 快照文件后,master 就会把 RDB 发给 slave,同时将后续新的写指令都同步给 slave。

Redis 集群管理

Redis 的集群管理有 3 种方式。

  • client 分片访问,client 对 key 做 hash,然后按取模或一致性 hash,把 key 的读写分散到不同的 Redis 实例上。
  • 在 Redis 前加一个 proxy,把路由策略、后端 Redis 状态维护的工作都放到 proxy 中进行,client 直接访问 proxy,后端 Redis 变更,只需修改 proxy 配置即可。
  • 直接使用 Redis cluster。Redis 创建之初,使用方直接给 Redis 的节点分配 slot,后续访问时,对 key 做 hash 找到对应的 slot,然后访问 slot 所在的 Redis 实例。在需要扩容缩容时,可以在线通过 cluster setslot 指令,以及 migrate 指令,将 slot 下所有 key 迁移到目标节点,即可实现扩缩容的目的。

Redis高效数据结构

string: SDS简单动态字符串

SDS简单动态字符串介绍

https://www.51cto.com/article/700992.html

Redis 的SDS字符串是动态字符串,用于存储二进制数据的一种结构, 具有动态扩容的特点,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

redis为什么不直接使用C字符串,而要自定义简单动态字符串?

C字符串与Redis的SDS比起来有以下不足:

  • 获取字符串长度的时间复杂度为 n
  • API是不安全的可能造成缓冲区溢出
  • 只能保存文本数据

Redis6.x VS Redis3.0.0 的 SDS

Redis6.x VS Redis3.0.0 的数据结构定义与 相差比较大,但是核心思想不变。先从简单版本(Redis3.x)开始吧~

redis 3.0.0
struct sdshdr {
    //记录buf数组中已使用字节的数量
    //等于SDS所保存字符串的长度
    unsigned int len;

    //记录buf数组中未使用字节的数量
    unsigned int free;

    //char数组,用于保存字符串
    char buf[];
};

如下图所示为字符串"Aobing"在Redis中的存储形式:

如何查找本地redis配置文件_如何查找本地redis配置文件_50

  • len 为6,表示这个 SDS 保存了一个长度为5的字符串;
  • free 为0,表示这个 SDS 没有剩余空间;
  • buf 是个char类型的数组,注意末尾保存了一个空字符’\0’。

buf 尾部自动追加一个’\0’字符并不会计算在 SDS 的len中,这是为了遵循 C 字符串以空字符串结尾的惯例,使得 SDS 可以直接使用一部分string.h库中的函数,如strlen

#include <stdio.h>
#include <string.h>

int main()
{
    char buf[] = {'A','o','b','i','n','g','\0'};
    printf("%s\n",buf);             // Aobing
    printf("%lu\n",strlen(buf));    // 6
    return 0;
}
数据结构优化

目前我们似乎得到了一个结构不错的 SDS ,但是我们能否继续进行优化呢?

在 Redis3.x 版本中不同长度的字符串占用的头部是相同的,如果某一字符串很短但是头部却占用了更多的空间,这未免太浪费了。所以我们将 SDS 分为三种级别的字符串:

  • 短字符串(长度小于32),len和free的长度用1字节即可;
  • 长字符串,用2字节或者4字节;
  • 超长字符串,用8字节。

共有五种类型的SDS(长度小于1字节、1字节、2字节、4字节、8字节)

我们可以在 SDS 中新增一个 type 字段来标识类型,但是没必要使用一个 4 字节的int类型去做!可以使用 1 字节的char类型,通过位运算(3位即可标识2^3种类型)来获取类型。

如下所示为短字符串(长度小于32)的优化形式:

如何查找本地redis配置文件_big data_51


低三位存储类型,高5位存储长度,最多能标识的长度为32,所以短字符串的长度必定小于32。

无需free字段了,32-len即为free

Redis6.x中优化
// 注意:sdshdr5从未被使用,Redis中只是访问flags。
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低3位存储类型, 高5位存储长度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用 */
    uint8_t alloc; /* 总长度,用1字节存储 */
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 已使用 */
    uint16_t alloc; /* 总长度,用2字节存储 */
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* 已使用 */
    uint32_t alloc; /* 总长度,用4字节存储 */
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 已使用 */
    uint64_t alloc; /* 总长度,用8字节存储 */
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];
};

数据结构和我们分析的差不多嘛!也是加一个标识字段而已,并且不是int类型,而是1字节的char类型,使用其中的3位表示具体的类型。

同时,Redis 中也声明了5个常量分别表示五种类型的 SDS ,与我们分析的也不谋而合。

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
Redis SDS内存不对齐

在 Redis6.x 的源码中 SDS 的结构体为struct __attribute__ ((__packed__)) 与struct有较大的差别,这其实和我们熟知的对齐填充有关。

在 C 语言中,结构体成员的排列和对齐方式依赖于编译器和平台,目的是为了提高访问内存的效率。 __attribute__ ((__packed__)) 是 GCC 的一个扩展,它告诉编译器不要对结构体进行任何填充,这样所有的成员将紧凑排列。

typedef struct{
     char  c1;
     short s; 
     char  c2; 
     int   i;
} s;

若此结构体中的成员都是紧凑排列的,假设c1的起始地址为0,则s的地址为1,c2的地址为3,i的地址为4。下面用代码论证一下我们的假设。

#include <stdio.h>

typedef struct
{
    char c1;
    short s;
    char c2;
    int i;
} s;

int main()
{
    s a;
    printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n",
           (unsigned int)(void *)&a.c1 - (unsigned int)(void *)&a,
           (unsigned int)(void *)&a.s - (unsigned int)(void *)&a,
           (unsigned int)(void *)&a.c2 - (unsigned int)(void *)&a,
           (unsigned int)(void *)&a.i - (unsigned int)(void *)&a);
    return 0;
}
// 结果为:c1 -> 0, s -> 2, c2 -> 4, i -> 8


char c1 占用 1 个字节。
short s 通常占用 2 个字节,但为了对齐,它可能会被放置在 2 字节边界上。
char c2 占用 1 个字节,但为了对齐 int i,它可能会被放置在 4 字节边界上。
int i 通常占用 4 个字节,并且通常会被放置在 4 字节边界上。
编译器对齐的方式是为了确保每个成员都按照其类型的对齐要求放置,这样访问内存更高效。假设 c1 的起始地址为 0,那么成员的内存布局可能如下:

c1 的地址为 0
s 的地址为 2(而不是 1,因为 short 通常要求 2 字节对齐)
c2 的地址为 4(而不是 3,因为 int 通常要求 4 字节对齐)
i 的地址为 8(因为 int 通常要求 4 字节对齐)
这意味着编译器会在 c1 和 s 之间插入一个填充字节,以及在 s 和 c2 之间插入两个填充字节。

(4) sds 更改对齐方式

注意:我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。

如果我们一定要手动更改对齐方式,一般可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
  • 使用伪指令#pragma pack():取消自定义字节对齐方式。
  • 另外,还有如下的一种方式(GCC特有语法):
  • __attribute((aligned (n))):让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
  • __attribute__ ((packed)):取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

将上述示例代码的结构体更改如下(取消对齐),再次执行,可以发现取消对齐后和我们的假设就一致了。

(5) Redis为什么不对齐呢?

综上所述我们知道了对齐填充可以提高 CPU 的数据读取效率,作为 IO 频繁的 Redis 为什么选择不对齐呢?

我们再次回顾 Redis6.x 中的 SDS 结构:

如何查找本地redis配置文件_如何查找本地redis配置文件_52

有个细节各位需要知道,即 SDS 的指针并不是指向 SDS 的起始位置(len位置),而是直接指向buf[],使得 SDS 可以直接使用 C 语言string.h库中的某些函数,做到了兼容,十分nice~。

如果不进行对齐填充,那么在获取当前 SDS 的类型时则只需要后退一步即可flagsPointer = ((unsigned char*)s)-1;相反,若进行对齐填充,由于 Padding 的存在,我们在不同的系统中不知道退多少才能获得flags,并且我们也不能将 sds 的指针指向flags,这样就无法兼容 C 语言的函数了,也不知道前进多少才能得到 buf[]。

SDS相对C字符串的好处

如何查找本地redis配置文件_redis_14

使用它主要有以下好处:

  • 读取字符串长度快:获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)
  • 杜绝缓冲区溢出:SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求
  • 二进制安全:SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束
  • 减少内存重新分配次数:对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略

这些好处也就解释了为什么Redis要使用SDS来实现字符串了。

如何查找本地redis配置文件_redis_54

https://zhuanlan.zhihu.com/p/684099907

C语言传统字符串获取长度的时间复杂度为 n

C语言传统字符串
C语言传统字符串是以空字符结尾的字符数组。例如:

char str[] = "hello\0";
strlen(str);

1.常数复杂度获取长度

由于C字符串不记录自身的长度,所以为了获取一个字符串的长度程序strlen必须遍历这个字符串,直至遇到’0’为止,整个操作的时间复杂度为O(N)。而我们使用SDS封装字符串则直接获取len属性值即可,时间复杂度为O(1)。。

实际上这种做法,在很多地方都很常见,例如C++中的标准容器,如vector获取其大小,string获取其长度。

redis中的简单动态字符串定义如下:

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; //记录buf数组中已使用的字节数量
    uint64_t alloc; //分配的buf数组长度,不包括头和空字符结尾
    unsigned char flags; //标志位,标记当前字节数组是 sdshdr8/16/32/64 中的哪一种,占 1 个字节。
    char buf[];//真正存储字符串的地方
};

定义的这些字段有以下一些好处:

  • 用单独的变量 len 和 free,可以方便地获取字符串长度和剩余空间;
  • 内容存储在动态数组 buf 中,SDS 对上层暴露的指针指向 buf,而不是指向结构体 SDS。因此,上层可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数,-
  • 同时也能通过 buf 地址的偏移,方便地获取其他变量;
  • 读写字符串不依赖于 \0,保证二进制安全。

比如我们设置的key=“xiaoxu”、value=“code”,存储情况如下图所示:

如何查找本地redis配置文件_如何查找本地redis配置文件_55

二进制安全性

♂️ 什么是二进制安全性?

二进制安全是指一种数据处理或传输的方式,其中对待数据的处理不会受到数据中包含的二进制数据的影响。在计算机科学和编程中,这个术语通常与字符串的处理有关。

C语言字符串和Redis SDS的二进制安全性问题对比

  • C 语言中字符串是以遇到的第一个空字符 \0 来识别是否到末尾,因此其只能保存文本数据,不能保存图片,音频,视频和压缩文件等二进制数据,否则可能出现字符串不完整的问题,所以其是二进制不安全。
  • Redis SDS(简单动态字符串)。而SDS使用len属性的值判断字符串是否结束,所以不会受’\0’的影响,保证了二进制安全。
预分配空间减少内存分配次数

实际上,在创建新的sds的时候,它并不仅仅申请要使用的内存,而是额外申请了一些空间,以避免下次修改的时候又需要重新申请内存,减少性能损耗。

比如说,你有一个字符数组:

char str[] = "hello";

现在你想存储helloworld,怎么办?原先的空间已经确定了,没有办法存储这么多字符串,你只能重新申请空间,然后还要把原先的hello拷贝到新申请的空间中去。如果有频繁地修改字符串,就会导致系统中频繁的内存申请,释放,拷贝,这样还能有高效的redis吗?

因此在redis中,如果有这样的情况,分配新的空间的时候,会预分配一些空间,以备下次使用。

3.惰性释放空间
而正因如此,出现字符串缩短的时候,也没有必要直接释放内存,只需要更新字符串,记录当前使用的长度即可,你说,下次字符串又增长的时候,不就又用上了吗?减少分配。

杜绝缓冲区溢出

字符串的拼接操作是使用十分频繁的,在C语言开发中使用char *strcat(char *dest,const char *src)方法将src字符串中的内容拼接到dest字符串的末尾。由于C字符串不记录自身的长度,所有strcat方法已经认为用户在执行此函数时已经为dest分配了足够多的内存,足以容纳src字符串中的所有内容,而一旦这个条件不成立就会产生缓冲区溢出,会把其他数据覆盖掉,Dangerous~。

ziplist实现原理

ziplist是一种连续,无序的数据结构。压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。

Redis中的hash,List,Zset这几种类型的数据在某些情况下会使用ziplist来存储。

ziplist是一个经过特殊编码的双向链表(占用一大块内存),设计的目标是为了提高存储效率,ziplist可以用于存储字符串或者整数,其中整数是按照二进制表示进行编码的,而不是编码成字符串序列。

ziplist不是普通的双向链表, 普通的双向链表每一项都占用独立的一块内存,各项之间用地址指针连接起来,这会造成大量的内存碎片,而且地址指针也占用额外的内存。 ziplist是将表中每一项放在前后连续的地址空间中,一个ziplist整体占用一大块内存,它是一个表(list), 但其实不是一个链表

另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,即对于大的整数,就多用一些字节来存储,对于小的整数就少用一些字节存

如何查找本地redis配置文件_数据库_56

值得注意的是,这个压缩列表的内存空间是连续的。这也是压缩列表的主要特点,空间连续,避免内存碎片,节省内存。

ziplist的特点

  • 压缩列表ziplist结构本身就是一个连续的内存块,由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串。
  • 压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。

quicklist实现原理

Redis对外暴露的List数据类型,底层实现用的就是quicklist

quicklist的实现是一个双向链表,链表的每一个节点都是一个ziplist, 为什么quicklist要这样设计呢? 其实也是一个空间和时间的折中

.双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。 首先它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片

ziplist由于是一整块连续内存,所以存储效率很高,但是它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能

这样设计的问题, 到底一个quicklist节点包含多长的ziplist合适?这是一个找平衡的问题:

.每个quicklist节点上的ziplist越短,则内存碎片越多,极端情况是一个ziplist只包含一个数据项,这就退化成了普通的双向链表

.每个quicklist节点上的ziplist越长,则为一个ziplist分配大块连续内存的难度就越大,有可能出现内存里有很多小块的内存空间,但却找不到一块足够大的空闲空间分给ziplist。极端情况是整个quicklist只有一个节点,这就退化成了一个ziplist了

skiplist 跳跃列表

跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率。

跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

跳表(skip
List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

跳跃列表缺点:跳表的效率比链表高,但是跳表需要额外存储多级索引,所以需要的更多的内存空间。

跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集(见右边的示意图)。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。

如何查找本地redis配置文件_redis_57

skiplist插入

skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。

skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。

Redis事务

Redis 在形式上看起来也差不多,MULTI、EXEC、DISCARD这三个指令构成了 redis 事务处理的基础:

  • MULTI:用来组装一个事务,从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令依次执行。
  • EXEC:用来执行一个事务
  • DISCARD:用来取消一个事务

redis事务分2个阶段:组队阶段、执行阶段

  • 组队阶段:只是将所有命令加入命令队列
  • 执行阶段:依次执行队列中的命令,在执行这些命令的过程中,不会被其他客户端发送的请求命令插队或者打断。