文章目录
一、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.高效数据结构
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 4.0 vs 5.0 vs 6.0 vs 7.0 功能对比
版本 | 时间 | 主要特性 | 说明 |
---|---|---|---|
Redis 4.0 | 2017 | 模块系统(Modules API) | 开放扩展接口,衍生出 RedisJSON、RedisGraph 等。 |
异步删除(UNLINK, FLUSHASYNC) | 删除大 key 不再阻塞主线程。 | ||
PSYNC2 | 改进的复制协议,更可靠的主从同步。 | ||
小功能 | SWAPDB 、MEMORY 命令等。 | ||
Redis 5.0 | 2018 | Streams(流) | 新数据结构,支持消息队列,消费组,类似 Kafka。 |
Sorted Set 新命令 | ZPOPMIN 、ZPOPMAX 等。 | ||
集群管理命令 | CLUSTER MYID 等更完善。 | ||
Redis 6.0 | 2020 | 多线程 I/O | 网络读写多线程,命令执行仍单线程;高并发场景性能提升明显。 |
ACL(访问控制列表) | 支持多用户、多权限、命令级与 key 级安全控制。 | ||
RESP3 协议 | 新协议,支持更丰富的返回类型(map、set、bool)。 | ||
客户端缓存一致性(tracking) | 支持追踪 key 的修改,客户端本地缓存自动更新。 | ||
改进的复制与持久化 | 更稳定高效。 | ||
Redis 7.0 | 2022 | 事务增强 | 支持 事务命令队列(事务中可以 abort)。 |
新的复制协议(PSYNC3) | 断线后更快恢复同步。 | ||
多线程扩展 | 不仅 I/O,多线程覆盖更多场景(如集群槽迁移)。 | ||
函数(Functions API) | 可以持久化的 Lua 函数,替代部分脚本场景。 | ||
ACL v2 | 更细粒度权限管理,支持命令子类别。 | ||
更灵活的集群 | 支持副本迁移、集群工具增强。 | ||
数据结构增强 | Set、Sorted Set、Stream 新命令,性能优化。 |
演进总结
- Redis 4.0(2017):补齐基础能力 → 模块系统、异步删除、复制优化。
- Redis 5.0(2018):新增 Streams,Redis 具备消息队列能力。
- Redis 6.0(2020):大飞跃 → 性能(多线程 I/O)、安全(ACL)、协议(RESP3)、客户端缓存一致性。
- Redis 7.0(2022):全面升级 → 事务增强、复制协议 PSYNC3、多线程扩展、函数 API、ACL v2、集群增强,更贴近企业级和云原生需求。
Redis IO模型
在 Redis 中,事件触发机制 是整个事件驱动模型的核心,它负责监听和处理各种 I/O 事件(如客户端连接、读写请求)和定时事件(如过期键清理、定期持久化)。
Redis基于Reactor模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder)。基于 I/O 多路复用(如 epoll、kqueue、select)来实现高效的事件处理。
文件事件处理器由Socket、IO多路复用程序、文件事件分派器(dispather),事件处理器(handler)四部分组成,文件事件处理器的模型如下所示:
IO多路复用程序会同时监听多个socket,当被监听的socket准备好执行accept、read、write、close等操作时,与这些操作相对应的文件事件就会产生。IO多路复用程序会把所有产生事件的socket压入一个队列中,然后有序地每次仅一个socket的方式传送给文件事件分派器,文件事件分派器接收到socket之后会根据socket产生的事件类型调用对应的事件处理器进行处理。
文件事件处理器分为几种:
- 连接应答处理器:用于处理客户端的连接请求;
- 命令请求处理器:用于执行客户端传递过来的命令,比如常见的set、lpush等;
- 命令回复处理器:用于返回客户端命令的执行结果,比如set、get等命令的结果;
Redis的客户端与服务端的交互
多路复用程序会监听不同套接字的事件
当某个事件,比如发来了一个请求,那么多路复用程序就把这个套接字丢过去套接字队列,事件分派器从 队列里边找到套接字,丢给对应的事件处理器处理。
单线程性能瓶颈与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 线程。
1. 网络 I/O 模块
操作 | 执行模式 | 详细说明 |
---|---|---|
请求读取 | 🔴 多线程 | 多个 I/O 线程并行地从所有客户端套接字读取数据。 |
协议解析 | 🔴 多线程 | 多个 I/O 线程并行解析读取到的数据,将其解析为 Redis 命令。 |
响应回复 | 🔴 多线程 | 多个 I/O 线程并行地将执行结果写入缓冲区,并发送回客户端。 |
2. 核心数据处理模块
操作 | 执行模式 | 详细说明 |
---|---|---|
命令执行 | ⚪ 单线程 | 主线程顺序执行所有解析好的命令(如 SET , GET 等)。这是保证原子性的关键。 |
惰性删除 | ⚪ 单线程 | 在访问键时检查并删除过期键,由主线程执行。 |
定期删除 | ⚪ 单线程 | 定期采样和删除过期键的逻辑仍在主线程中完成。 |
3. 内存管理与后台任务模块
操作 | 执行模式 | 详细说明 |
---|---|---|
内存分配/释放 | 🟡 混合 (后台) | 实际的内存操作由分配器处理,但触发指令在主线程。 |
惰性释放 (Lazy Free) | 🔴 多线程 (后台) | 对于 UNLINK 、FLUSHDB ASYNC 等操作,大键的内存释放由后台线程异步处理,避免主线程阻塞。 |
异步关闭文件 | 🔴 多线程 (后台) | 关闭文件描述符等操作可以放入后台线程处理。 |
4. 持久化模块
操作 | 执行模式 | 详细说明 |
---|---|---|
RDB 生成 | ⚪ 单线程(后台进程) | SAVE 或 BGSAVE 的最终数据同步和文件写入过程均由(子)进程或主线程处理。 |
AOF 日志写入 | ⚪ 单线程 | 命令执行后,将追加写入 AOF 缓冲区的操作仍在主线程。刷盘策略可配置。 |
AOF 重写 | ⚪ 单线程 | BGREWRITEAOF 由子进程执行,不会阻塞主线程,但其触发和同步逻辑在主线程。 |
5. 复制与模块系统模块
操作 | 执行模式 | 详细说明 |
---|---|---|
全量同步 | 🟡 混合 | 主节点生成 RDB 是单线程,但发送 RDB 文件给从节点使用了 I/O 多线程。 |
增量同步 | ⚪ 单线程 | 主节点将写命令传播到从节点,命令的执行和传播仍在主线程逻辑中。 |
模块命令执行 | ⚪ 单线程 | 自定义模块实现的命令,其执行逻辑仍在主线程中。 |
多线程方案优劣
虽然多线程方案能提升1倍以上的性能,但整个方案仍然比较粗糙。
首先所有命令的执行仍然在主线程中进行,存在性能瓶颈。然后所有的事件触发也是在主线程中进行,也依然无法有效使用多核心。
而且,IO 读写为批处理读写,即所有 IO 线程先一起读完所有请求,待主线程解析处理完毕后,所有 IO 线程再一起回复所有响应,不同请求需要相互等待,效率不高。最后在 IO 批处理读写时,主线程自旋检测等待,效率更是低下,即便任务很少,也很容易把 CPU 打满。
整个多线程方案比较粗糙,所以性能提升也很有限,也就 1~2 倍多一点而已。要想更大幅提升处理性能,命令的执行、事件的触发等都需要分拆到不同线程中进行,而且多线程处理模型也需要优化,各个线程自行进行 IO 读写和执行,互不干扰、等待与竞争,才能真正高效地利用服务器多核心,达到性能数量级的提升。
Redis实现原理
字典表
redis单机服务端有16个数据库,每个数据库都有一个字典结构,这个字典里存着两个hash表(为了之后的扩缩容),而这个hash表里有一个dictEntry 组成的数组,里面存放的就是所有的键值对。这个dictEntry还有指向下一个节点的指针,就是为了在hash冲突的情况,采用拉链法扩展出一个链表。
/*
* 字典
*/
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的位置,比如:
渐进式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的链表实现比较简单,但具有五个主要特性:
- 双端:链表节点包含prev和next两个指针,分别指向前一个节点和后一个节点,这样可以在两个方向上遍历链表。
- 无环:链表的头节点和尾节点都指向null,表示链表的开始和结束。
- 带有表头和表尾指针:链表本身包含两个指针,一个指向头节点,一个指向尾节点,这样可以在O(1)时间复杂度内访问链表的头部和尾部。
- 长度计数器:链表还包含一个长度计数器,记录了链表包含的节点数。
- 多态:链表可以存储不同类型的数据,这主要通过将节点的值存储为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高可用、高性能
【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 Sentinel 模式要注意有两个集群,一个是存放了 Redis 数据的集群,一个是监控这个数据集群的哨兵集群。于是就需要理解哨兵集群之间是如何监控的,如何就某件事达成协议,以及哨兵自身的容错。
【2015】Redis 集群模式(Cluster)
2015年,Redis3.0版本支持了集群模式。Redis集群是一种分布式数据库解决方案,通过分片管理数据,数据分为16384个槽位(0~16383区间),每个节点负责槽位的一部分。
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集群的高可靠性和可用性
Redis主从复制
Redis主从同步分为全量同步和增量同步两种模式,通过PSYNC协议实现:
复制标识控制
replication ID:40位唯一字符串,标识主节点数据版本
offset偏移量:主从各自维护,记录已同步数据量(类似binlog位置)复制积压缓冲区
环形缓冲区存储最近的写命令(默认1MB)
通过repl-backlog-size参数调整,建议设置为「网络延迟×写入流量」的2倍
全量同步(首次连接或无法增量同步时)
触发条件:当从节点首次连接主节点,或主从的replication ID不一致时
执行流程:
- 从节点发送PSYNC ? -1命令请求同步
- 主节点生成RDB快照(bgsave)并缓存期间的写命令
- RDB文件传输完成后,主节点发送缓存命令(repl_backlog)
- 从节点清空旧数据,加载RDB文件后执行缓存命令
当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操作,相当于重新建立主从复制。
- 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 ID一致,且从节点offset在复制积压缓冲区(replication backlog) 范围内
执行流程:
- 从节点发送PSYNC
- 主节点校验offset后,发送积压缓冲区内缺失的命令。(主库会将 slave_repl_offset 和 master_repl_offset 之间的命令同步给从库即可)
- 从节点执行增量命令完成数据同步
因为 replication buffer 是一个环形的缓存,当主从库长期断开时,是有可能被覆盖掉旧的数据,这个时候是会重新发起全量复制,主库根据从库发送的 slave_repl_offset 来判断是增量还是全量的复制。
为什么全量复制使用 RDB 而不是使用 AOF 呢?
- RDB 文件是经过压缩的二进制文件,AOF 文件是记录每一次的操作,包含对同一个 key 的多次冗余操作,文件比 RDB 要大的多,使用 RDB 可以减少带宽
- RDB 是二进制数据,从库还原速度快。而 AOF 需要依次重放每一个命令,恢复速度慢。
全量、增量同步区别
全量同步的缺点
- CUP 和 内存,缺页异常
- IO负载
- 网络负载
- 失败会重新引发全量,循环往复
如何避免
- 安全重启
- 增大缓冲区
如果过程发生了网络中断或者阻塞,该如何解决?
- 在 Redis 2.8 之前,从库只能和主库重新发起全量同步,对于较大的 RDB 文件,网络恢复时间较长;( 即便是抖动后断开又恢复网络连接,但此时 TCP 连接已经断开,数据肯定是需要重新同步了。那么很容易 陷入到无休止的全量同步之中。)
- 从 Redis 2.8 开始,从库已支持增量同步,只会把断开的时候没有发生的写命令,同步给从库。
-
savle在恢复网络后,会发送 psync 命令给master,此时的 psync 命令里的 offset 参数不是 -1。
-
master收到该命令后,然后用 continue 响应命令告诉slave接下来采用增量复制的方式同步数据, 然后master将断网期间写入命令发送给slave,然后slave再执行这些命令。
注意点:断开重连并不一定是增量复制。如上图所示,repl backlog为环形结构,如果网络断开时间太长,写入命令如果超过1M,旧的命令就会被覆盖。因此如果master offset和slave offset相差的数据已被覆盖则会通过全量复制。因此,repl backlog可以适当配置大一些。
Redis缓冲区 repl_backlog_buffer、replication_buffer
Redis 缓冲队列:repl_backlog_buffer 和 replication_buffer 详解
Redis 使用两种不同的缓冲区来处理主从复制(replication)过程中的数据传输,它们各自有不同的作用和特点。
1. repl_backlog_buffer (复制积压缓冲区) 主节点只有一个
replication_backlog_buffer: backlog英文释义,是积压的意思; 三者合在一起 replication_backlog_buffer,就是复制积压缓冲区
作用
• 主从断连后的数据恢复:当从节点与主节点断开连接后重新连接时,用于提供部分重同步(partial resynchronization)
• 增量复制:保存最近主节点执行的写命令,允许从节点只获取断开期间丢失的命令而不是全量同步
关键特性
• 环形缓冲区:固定大小的循环缓冲区,写满后会覆盖最旧的数据
• 主节点维护:仅由主节点维护
• 全局共享:所有从节点共享同一个repl_backlog_buffer
• 配置参数:通过repl-backlog-size
配置(默认1MB),repl-backlog-ttl
配置存活时间(默认3600秒)
工作原理
-
主节点执行写命令后,除了发送给已连接的从节点,还会写入repl_backlog_buffer
-
从节点断开后重连时,会发送自己的复制偏移量(replication offset)
-
主节点检查请求的偏移量是否仍在backlog中:
• 如果在:发送从该偏移量之后的所有命令(部分重同步)• 如果不在:触发全量同步
2. replication_buffer (复制客户端缓冲区),主节点有多个
作用
• 命令传输缓冲:主节点为每个从节点单独分配的缓冲区,用于暂存要发送给该从节点的命令数据
• 流量控制:当从节点处理速度跟不上主节点时,缓冲数据避免直接丢弃
关键特性
• 每个从节点独立:主节点为每个连接的从节点单独维护一个replication_buffer
• 动态调整:大小不固定,会根据需要增长(受系统内存限制)
• 配置参数:通过client-output-buffer-limit
中的slave
类别配置限制
工作原理
- 主节点执行写命令后,会将命令写入所有从节点的replication_buffer
- 从节点通过网络连接读取这些命令并执行
- 如果从节点读取速度慢,缓冲区会积压数据
- 当缓冲区超过限制时,主节点可能断开该从节点的连接
两者对比
特性 | repl_backlog_buffer | replication_buffer |
---|---|---|
作用范围 | 全局(所有从节点共享) | 每个从节点独立 |
缓冲区类型 | 固定大小的环形缓冲区 | 动态增长的线性缓冲区 |
主要用途 | 断线后部分重同步 | 正常复制时的命令传输 |
配置参数 | repl-backlog-size | client-output-buffer-limit |
溢出处理 | 覆盖旧数据 | 可能断开从节点连接 |
生命周期 | 持久存在(直到配置的TTL) | 随从节点连接创建/销毁 |
实际应用建议
-
repl_backlog_buffer调优:
• 在网络不稳定的环境中增大repl-backlog-size
• 根据业务写入量调整,公式参考:
backlog_size = 平均写入速率 × 最大断连时间 × 2
-
replication_buffer监控:
• 监控client_replica_buffer
指标• 合理设置
client-output-buffer-limit
避免主节点内存耗尽• 对于慢从节点,考虑升级其配置或优化网络
-
两者协同工作:
• 正常情况:数据通过replication_buffer传输• 断线重连:优先尝试使用repl_backlog_buffer进行部分重同步
• 如果backlog不足,则退化为全量同步
Redis底层持久化实现
首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,所以提供了RDB和AOF两种持久化机制。
Redis 的持久化机制分成两种,RDB 和 AOF。
2.1 持久化 rdb
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
RDB快照的触发方式有很多,比如
- 执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
- 根据redis.conf文件里面的配置,自动触发bgsave
- 主从复制的时候触发
RDB 也是主从全量同步里的 RDB。 RDB 可以理解为是一个快照,直接把 Redis 内存中的数据以快照的形式保存下 来。因为这个过程很消耗资源,所以分成 SAVE 和 BG SAVE 两种。BG SAVE的核 心是利用 fork 和 COW 机制。
所以他是一个全量的方式来进行持久化的
2.1.1 Redis BG Save
利用fork
系统调用,复制出来一个子进程,子进程尝试将数据写入文件。在没有 COW 机制的情况下,fork() 会将父进程的内存数据完整复制一份给子进程,这会消耗大量内存和时间。
而 COW 机制允许父子进程在 fork() 后共享同一块物理内存,只有当某个进程需要修改内存数据时,才会触发内存页的复制操作。
COW 的核心是利用缺页异常,操作系统在捕捉到缺页异常之后,发现他们共享内存了,就会复制出来一份。
具体过程如下:
- 内存共享:fork() 后,子进程的虚拟内存空间指向父进程的物理内存,此时父子进程共享内存。
- 写操作触发复制:当父进程或子进程尝试修改某块内存时,操作系统会检测到内存页是只读的,从而触发页异常中断(page-fault)。中断处理程序会为修改的进程复制该内存页,使其拥有独立的副本。
- 按需复制:只有被修改的内存页才会被复制,未修改的内存页仍然共享。
2.2 持久化 aof
AOF持久化,它是一种近乎实时的方式,,AOF 是将 Redis 的命令逐条保留下来,而 后通过重放这些命令来复原。我们可以通过重写 AOF 来减少资源消耗。
就是客户端执行一个数据变更的操作,Redis Server就会把 Redis 的命令逐条保留下来,追加到aof缓冲区的末尾,
然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。
- 逐条记录命令
- AOF 刷新磁盘的时机
- always: 每次都刷盘
- everysec(默认): 每秒,这意味着一般情况下会 丢失一秒钟的数据。而实际上,考虑到硬盘阻塞(见后面**使用 everysec 输盘策略 有什么缺点),那么可能丢失两秒的数据。
- no: 由操作系统决定
• 可以通过重写来合并 AOF 文件
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。
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 服务器在执行 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 数据。
最后来总结这两者,到底用哪个更好呢?
推荐是两者均开启。
- 如果对数据不敏感,可以选单独用 RDB。
- 如果只是做纯内存缓存,可以都不用。
因此,基于对RDB和AOF的工作原理的理解,我认为RDB和AOF的优缺点有两个。
- RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高
- RDB文件默认采用压缩的方式持久化,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好
Redis 内存淘汰策略
内存淘汰策略
在 Redis 4.0 版本之前有 6 种策略,4.0 增加了 2种,主要新增了 LFU 算法。
下图为 Redis 6.2.0 版本的配置文件:
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 定期执行 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。
过期字典实现
过期字典是Redis每个库中都有的一个独立哈希表,与存储键值对的主字典平行存在。它的结构特点如下:
typedef struct redisDb {
dict *dict; // 该DB的主键空间
dict *expires; // 该DB的过期字典
int id; // 数据库ID
// ...其他字段
} redisDb;
存储内容:
- 键:与主字典共享相同的键对象(不重复存储)
- 值:64位整数,表示键的过期时间(Unix时间戳,毫秒精度)
expires字典:
| 键对象"user:1001" | 1672531200000 (2023-01-01 00:00:00) |
| 键对象"session:abc" | 1672617600000 (2023-01-02 00:00:00) |
惰性删除
除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓 惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。
Redis会为每个key中额外增加内存空间用于存储每个key的使用时间, 大小是3字节
/*
* 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运行模式
单副本模式
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 节点发送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 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)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。
数据倾斜问题
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。
此时必然造成大量数据集中到 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管理
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事务
Redis 在形式上看起来也差不多,MULTI、EXEC、DISCARD这三个指令构成了 redis 事务处理的基础:
- MULTI:用来组装一个事务,从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令依次执行。
- EXEC:用来执行一个事务
- DISCARD:用来取消一个事务
redis事务分2个阶段:组队阶段、执行阶段
- 组队阶段:只是将所有命令加入命令队列
- 执行阶段:依次执行队列中的命令,在执行这些命令的过程中,不会被其他客户端发送的请求命令插队或者打断。
面试题
Redis集群数据是如何分布的? 如何决定一个key存放在哪个节点上?
数据分布的核心机制:哈希槽分区
-
槽位划分
Redis集群将数据空间划分为16384个固定数量的哈希槽(0-16383),每个节点负责管理一部分槽位。例如,3个节点的集群可能分配如下:
- 节点A:0-5460
- 节点B:5461-10922
- 节点C:10923-16383。
-
Key到槽位的计算
通过CRC16算法对key计算哈希值,再对16384取模,确定槽位:slot = CRC16(key) % 16384
例如,key为"user:1001"的槽位计算后若为5000,则会被分配到节点A。
-
槽位分配的灵活性
槽位与节点的映射关系可通过集群管理工具动态调整。扩容时,槽位会从现有节点迁移到新节点,而无需停机。
决定key存放节点的流程
客户端请求路由
客户端连接任意集群节点,若该节点不负责目标槽位,会返回MOVED重定向指令,包含正确节点的地址。
优化方案:Smart客户端(如Jedis)本地缓存槽位映射表,直接定位目标节点,减少重定向。
节点间的数据迁移
扩缩容时,槽位及其数据会在节点间迁移。例如,新增节点D后,集群可能从节点A迁移槽位0-1000到D,此过程对客户端透明。
异常处理
若目标节点不可用,集群会触发故障转移,由从节点接管槽位,并更新路由信息。