disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
      • 基础与特性
        • 心跳检测
      • 数据的主从复制
        • 复制的旧版实现
        • 复制的新版实现
        • 主从复制时数据丢失问题
      • 哨兵 sentinel
        • 主要功能
        • 哨兵选举
      • 高可用架构 RedisCluster
        • 去中心化集群架构
        • 集群的底层数据结构
        • 槽和机器的关系
        • 重新分片
        • ASK、MOVED、ASKING
        • 复制与故障转移
        • gossip 协议
        • 消息
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2022-10-12
非关系型数据库
目录

Redis 集群

主从复制指将主 Radis 的数据复制到其他从属 Radis 中,数据的复制只能从主机复制到从机,并且从机不可以写入数据

主从复制的好处是读写分离已经容灾后快速恢复,主机读数据,从机写数据,因为大多数对数据库的操作都是读取数据,这么做可以减少服务器压力

# 基础与特性

首先要明确这台 reids 是否支持集群化,查看配置的 cluster-enabled 属性,如果该属性为 yes,则表示支持集群

redis 使用 cluster meet IP 端口命令来连接另外一台支持集群的 redis 机器,使用 cluster node 命令查看该节点的集群状态,如果这个节点只有它自己,也是可以被称为集群的

集群的特性有很多,比如从机下可以挂从机,薪火相传就是指从机下可以挂从机这一特性,缺点是某个中间节点的从机挂掉后后续的从机不能读取数据

使用命令 slaveof no one 来实现主从机的切换,当主机挂了之后这个从机自动成为主机,主机一般被称作 master,从机一般被称作 slave

我们可以得出,如何复制数据以及主机挂掉之后从机如何成为主机这两个问题比较主要。基于这个问题,我们来介绍一下 redis 的三种模式:主从模式、哨兵模式、cluster。主从模式和哨兵都可以实现读写分离,但是 cluster 模式不行,除非客户端额外维护从机地址

# 心跳检测

在互联网多个机器沟通的时候,心跳检测十分重要,毕竟是用来确认对方是否活着的重要依据,在几乎所有的集群搭建中,底层都实现了心跳检测机制。redis 的心跳检测主要做两件事:

1,检测主从服务器的网络状态:主机每一秒都会发送检测请求,并且将该值保存,如果发现 lag 的值超过1秒的话,主从连接大概率出现问题了

2,检测命令丢失:即使对方存活的情况下也无法保证对方是否接受到了命令,命令可能由于网络等等情况丢失。因此 redis 的心跳检测还会携带从机的复制偏移量,如果主机发现返回的复制偏移量小于当前的复制偏移量,主机就知道从机应该是丢失数据了,此时会采取一定的补救措施

心跳检测在主从复制中会用到,从节点会主动向主节点汇报自己的偏移量。在哨兵模式中也会用到,哨兵节点之间通过 Gossip 协议交换信息,达成对主节点状态的共识。哨兵与 Redis 节点之间的心跳检测,则是通过定期发送 PING 命令监控所有 Redis 节点(主节点和从节点)的存活状态

# 数据的主从复制

# 复制的旧版实现

需要复制的时候向被认定为从机的 redis 机器输入 slaveof 指令即可

slaveof 127.0.0.1 6379
1

主从复制的原理是从机连接主机时会发送一个 sync(同步)命令,主机接受后会发送存放在主机中的所有数据给从机(全量复制),在执行修改后也会发生数据给从机(增量复制)

全量复制是执行 bgsave 命令,使用子线程生成一个当前数据的 RDB 文件(全量同步用 RDB,因为 RDB 是紧凑的内存快照,体量比 AOF 小的多),并且将这个文件发送给从机。但是生成并且发送数据的时候也有可能在主机中生成数据,因此主机会用一个缓冲区记录所有的写数据。在从机载入 RDB 文件后还会读取主机缓冲区中的命令,此时主从复制完成

在主机写数据时候需要向从机发送写数据的指令,保证主从一致,这种行为叫命令传播,简单来说就是先全量后增量的变更数据

但是旧版全量复制的时候有个缺点,就是在主从机断链的并且恢复的时候,主机会重新向从机发送 RDB 文件,这个过程是非常消耗资源的。因此 redis 将该动作升级了一下

# 复制的新版实现

为了处理重新链接时消耗过多的问题,我们可能想到让从机从断裂处开始读取数据

新版使用 psync 命令来代替 sync 命令,其中关于部分重同步的底层实现更变成了在从机断掉之后会选择性的进行 RDB 复制或者是只将部分数据复制,注意,redis 集群间的只会通过异步复制的方式同步数据

在主机向从机发送增量更新命令时主从服务器都会维护一个复制偏移量,该值记录了主机已经向从机发送了多少字节,主机每次向从机发送 N 字节数据在主服务器中该值就增加 N 字节。从服务器中的复制偏移量记录了当前最新数据的偏移量。这样发生网络中断时该值不会更新,我们就知道从机是从什么时候开始中断的

复制积压缓冲区是一个固定长度的先进先出队列,主服务器用它来记录之前发送的语句偏移量与字符对应关系,如果从机重新链接后给出的复制偏移量小于缓冲区的最小值,就执行 RDB 全量复制,如果大于该值说明缓冲区中存放了复制需要的语句,无需进行 RDB

额外聊一个过期问题,从机如果内存满了,数据过期了,不会删除数据。只会给数据一个特殊标识,做逻辑删除。只有等到主键的删除命令(主机检测数据过期后,会向从节点发送 DEL 命令),才会真正的删除数据。因此可能会出现主机内存够用,从机内存不够的问题。解决该问题可以使用去中心化集群架构,以及从节点配置激进的删除策略 allkeys-lru(淘汰所有key,优先保证可用性)

# 主从复制时数据丢失问题

1,异步复制导致的数据丢失:因为数据从主机复制到从机需要一定时间,当数据没有完全复制到从机时,主机就挂了,此时数据会丢失

在主机开启持久化之后数据依然会丢失,因为集群检测到 master 发生故障,会重新选举新的 master。新的 master 没有收到这份数据,那么旧 master 重新上线后里面的数据就会被刷新掉,此时数据还是会丢失,低版本中只能在发生延迟后禁止主机写入来防止这种情况,高版本中可以设置同步复制,即可以设置等待至少 x 个副本确认,最多等 y 秒

2,脑裂导致的数据丢失:假设我们有一个 redis 集群,正常情况下 client 会向 master 发送请求,然后同步到 salve,sentinel 集群监控着集群,在集群发生故障时进行自动故障转移。此时,由于某种原因,比如网络原因,集群出现了分区,master 与 slave 节点之间断开了联系,sentinel 监控到一段时间没有联系认为 master 故障,然后重新选举,将 slave 切换为新的 master。但是 master 可能并没有发生故障,只是网络产生分区,此时 client 任然在旧的 master 上写数据,导致数据丢失

脑裂:由于某些原因导致集群被分成多个部分,原来的 master 正常提供服务,但是哨兵认为它挂了,选出一个新的 master,导致同一时间有多个 master 向外提供服务

发生了这两种情况主机都是可以探测到的,思路就是探测到之后进行一些补救措施,比如主机禁止写入

min-slaves-to-write 1
min-slaves-max-lag 10
1
2

min-slaves-to-write 默认情况下是0,min-slaves-max-lag 默认情况下是10。以上面配置为例,这两个参数表示至少有1个 salve 的与 master 的同步复制延迟不能超过10s,一旦所有的 slave 复制和同步的延迟达到了10s(没有确认到从机接受到数据了),那么此时 master 就不会接受任何请求。redis 采用异步复制的操作,因此不能配置为等到所有的从机都接受到数据后,才给客户端发回响应

关于脑裂也是如此,如果当前主机控制的从机少于一定数量时(min-slaves-max-lag = 10代表少于10时),不会写入数据

注意,redis 不像 mysql 一样可以设置半同步或者全同步,只有至少要有 x 个从节点连接,并且从节点延迟不超过 y 秒,这种设置。当没有足够低延迟的从节点时,主节点会拒绝写操作。如果想要实现全同步这样的效果,我们需要执行 wait 命令,WAIT 命令用于阻塞当前客户端,直到所有先前的写入命令成功传输并且至少由指定数量的从节点确认,执行 WAIT 命令时,可以指定需要确认的从节点数量和超时时间,如果在超时时间内未达到指定的从节点数量,命令仍会返回已确认的从节点数量

# 哨兵 sentinel

从机挂了主机可以检查到并且自动处理问题,那主机挂了服务就真挂了,如何保证服务的高可用性呢

# 主要功能

我们可以配置一主二从三哨兵来处理这个问题,哨兵也是一个 redis 服务,哨兵是 redis 高可用的解决方案。而为了哨兵的高可用一般需要配置哨兵集群,哨兵的功能是监视所有的 redis 集群,在主机挂了之后,自动配置第二个主机,如果故障转移发生了,他还要通知 client 客户端新的 master 地址

哨兵集群也会有相互发送心跳检测的功能。哨兵与普通 redis 的连接有两个,一个是命令连接(用于发送命令),一个是订阅连接(发送订阅模式中,主机在发送数据后本地不保存数据,万一数据丢失则无法再次发送,因此在哨兵里解决了这一问题)。哨兵与哨兵之间的连接只有命令连接

成为哨兵意味着失去大多数的命令解析能力,服务器在命令表中没有载入 set、del 等普通命令,下面是 redis 5.0.9 哨兵可以执行的所有命令

struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

哨兵是按照如下过程检测 radis 主机是否挂了的:

  • 主观下线:哨兵给主机发信息,如果不回信息,哨兵会主观认为主机挂了
  • 客观下线:很多哨兵(超过一半的哨兵)都发现主机挂了,进行投票之后换了一个主机,称为原来主机的客观下线

# 哨兵选举

在大多数哨兵认定主机客观下线之后,就需要进行性的哨兵选举了,哨兵们选出一个主哨兵,来执行选举主机、故障转移等操作。哨兵的选举采用的是 Raft 算法,Raft 是一个用户管理日志一致性的协议

大致按照如下顺序:

  • 每个做主观下线的 sentinel 节点向其他 sentinel 节点发送命令,要求将自己设置为领导者
  • 接收到的 sentinel 可以同意或者拒绝
  • 如果该 sentinel 节点发现自己的票数已经超过半数并且超过了 quorum
  • 如果此过程选举出了多个领导者,那么将等待一段时重新进行选举。选出 Leader 后 Leader 会从从机中选举出合适的丛机进行故障转移

重点强调一下 raft,他其实重点侧重于在一群机器中选择出一个老大,而且他最初的目的是管理日志一致性,即这个老大原本应该会受到请求,并且把请求作为日志条目加入到它的日志中,然后并行的向其他服务器发起请求以复制日志条目。当这条日志被复制到大多数服务器上,Leader 将这条日志应用到它的状态机并向客户端返回执行结果。redis 只借鉴了它前半部分,舍弃了后半部分 在这里插入图片描述 新的主机选出来之后,我们还要干几件事,选出主机,他会根据以下规则选择:

1,过滤无效节点,排除已下线或网络不通的从节点。排除数据太旧(复制偏移量落后)的从节点 2,优先级排序:优先选择 slave-priority 值最小的从节点(配置文件设置)。若优先级相同,选择复制偏移量(repl_offset)最大的从节点(数据最新)。若仍相同,选择 Run ID 字典序最小的从节点(唯一随机因素)

让已下线的主机下的所有从机复制新的主机的内容,保证数据一致性;将旧的主服务器挂到这个新主机下,变成新主机的从机,这样在下次上线的时候就不用配置了。这些都可以通过固定命令完成

这里可能存在个问题,这种事情为什么要哨兵做,像 zk 中主从复制就是集群内部自己完成的。主节点挂掉后,集群内部自己用 zab 选举出主机,redis 为什么要额外建立个哨兵机制?

答案是他们的侧重点不一样,redis 是 AP 架构,复制是异步的,如果让集群内部选举可能会选出一个偏移量较低的节点作为主机器,导致数据丢失。但是这其实也可以处理,如果保证集群内部的心跳检测就可以了,不过 redis 没有实现集群内部的心跳检测,而是将哨兵作为独立进程,避免将复杂逻辑嵌入 Redis 核心,哨兵的 ping 命令是个时间事件

# 高可用架构 RedisCluster

redis 使用集群分摊压力、实现扩容,获得更多的内存空间,redis 实现了自己的集群连接方式保证了一定的高可用性

之前我们会使用主从复制加哨兵模式来保证服务的高可用,配合上 redis 的其他特性(薪火相传等),这个集群会运行的很好,其中的原理在上面都已经讲过了,但是,都什么年代了还在用传统集群

# 去中心化集群架构

redis 可以采用去中心化集群(每个 redis 都有从机,与其他 redis 可以相互通信,每个 redis 都可以被访问),而数据的分配一般使用数据分片(槽指派)来实现

所谓数据分片,就是一共有16384个 slot(槽),数据库中的每个键都能通过算法(使用公式 CRC16(key) % 16384,然后对16384取模来计算)算出需要操作的数据属于这16384个哈希槽的哪一个

如果16384个槽没有被占满,即集群中的 redis 服务器们没有负责全部的槽,此时的集群服务被称作下线状态,相反,如果 redis 占满了所有的槽,此时服务处于上线状态,redis 默认使用这种 hash 槽算法,可以让用户自定义哪些槽放到哪些机器上去

而集群中哪些节点放到哪些槽上,是用户手动设置,或者使用 redis-cli --cluster create 创建集群时,工具会自动均匀分配槽。如果集群中有新机器加入,或者有机器退出时,需要使用 redis-cli --cluster reshard 指令重新分配槽

额外说一下一致性 hash 算法,步骤是先计算 key 的 hash 值,用上一步的值 % (2^32)用于确保 key 能映射到环上的某一个点(避免映射到环外)

key 落到圆环上以后,就会按照顺时针寻找距离自己最近的一个节点。这种算法是为了减少节点变更时,数据迁移的数量

而集群中的每个节点负责处理一部分哈希槽,这些节点又叫 redis 分片,比如:

  • 节点 A 负责处理0号至8000号哈希槽
  • 节点 B 负责处理8001号至16384号哈希槽

而如果想要添加或者删除节点的话,将其他节点的槽放入或者增加就行了,槽中的数据也会一并转移

redis 的这种设计说明了它是一个高可用的集群架构,数据分布在多个节点上,即使部分节点出现故障,其他正常节点仍然可以提供服务,客户端可以继续访问集群中的数据,不会因为个别节点的问题而导致整个系统不可用。同时增量数据会自动写入其他节点,Redis 集群在默认情况下采用最终一致性模型。在数据写入时,数据会被异步复制到其他节点,这可能导致在复制过程中,不同节点上的数据存在短暂的不一致性,因此它是 AP 的设计

我们既然要保证高可用,就要考虑到这过程中的各种问题,假如突然有一台机器宕机了,如何将该机器的槽以及数据快速迁移到其他机器上,并且保证该过程中用户的访问是正常的呢?

简单来说:

1,集群节点(包括主节点和从节点)通过 Gossip 协议 互相通信,定期交换状态信息(如存活状态、槽分配等)。每个节点默认每秒随机选择部分节点发送 PING,检测对方是否存活 2,若节点 A 在 cluster-node-timeout(默认 15 秒)内未收到节点 B 的响应,会将其标记为 PFAIL(可能失效)。若多数主节点确认节点 B 为 PFAIL,则升级为 FAIL(确认失效),触发故障转移(这是哨兵的思路,去中心化集群部署中每个机器都把哨兵的活干了,所以不需要哨兵了) 3,当主节点被确认为 FAIL 时,其对应的从节点会发起选举(基于 Raft 算法变种)。获胜的从节点会升级为新主节点,接管原主节点的槽

# 集群的底层数据结构

redis 集群使用三个数据结构来维持

redis 集群由多个节点组成,为了让某个机器了解集群中其他机器的状态,必须在服务器内部维持一个表示其他机器的数据结构,即 clusterNode

typedef struct clusterNode {
    mstime_t ctime; /* 创建节点的时间 */
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* 节点标识 */
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
    int numslaves;  /* Number of slave nodes, if this is a master */
    struct clusterNode **slaves; /* 从节点 */
    struct clusterNode *slaveof; /* 父节点 */
    ......
}
1
2
3
4
5
6
7
8
9
10
11
12

clusterNode 的 link 属性是一个 clusterLink 结构,表示与连接节点相关的信息,clusterLink 结构既然是表示连接的,那与 redisClient 类似,也有输入缓冲区与输出缓冲区,表示了向其他节点发送消息的相关属性

typedef struct clusterLink {
    mstime_t ctime;             /* 连接开始时间 */
    int fd;                     /* 代表了套接字 */
    sds sndbuf;                 /* 输出缓冲区 */
    sds rcvbuf;                 /* 输入缓冲区 */
    struct clusterNode *node;   /* 与这个连接相关联的节点 */
} clusterLink;
1
2
3
4
5
6
7

我们使用 clusterState 结构来表示在当前节点的视角下,整个集群是什么样子的,上面两个数据结构都是一对一的关系,这个结构则代表比较整体的概念

typedef struct clusterState {
    clusterNode *myself;  /* 自己 */
    uint64_t currentEpoch;
    int state;            /* 集群当前状态,可能是 CLUSTER_OK, CLUSTER_FAIL, ... */
    int size;             /* 至少处理一个槽的节点的数目 */
    dict *nodes;          /* 名称为键,clusterNode 为值的字典 -> clusterNode structures */
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
    ......
}
1
2
3
4
5
6
7
8
9

来看看它们的具体应用:

cluster meet 命令借鉴(抄袭)了 TCP 的三次握手,该命令的执行就是两台 redis 的握手过程,首先发送方创建一个 clusterNode 结构,将该结构添加到 clusterState.nodes 字典中,然后发送一条 MEET 消息,那一台 redis 接受后也会执行相同的操作(创建结构,添加字典,发送 MEET),A 接受会会返回一条 PONG 消息,握手结束

之后,B 会向集群中通过 gossip 协议向集群中其他节点发送 A(发送方)的 MEET 请求,最终,集群中所有的机器都会认识 A

# 槽和机器的关系

除此之外节点还会在接受到槽更新命令的时候在集群中还会发送节点的槽指派信息,其他的机器接受后,会改变clusterNode 以及 clusterState 中的 slots相关信息

也就是说,槽是由 clusterNode 以及 clusterState 中的槽数组配合储存的,在每个 node 节点中储存的是比特数组,使用1来表示该槽由该机器负责。该机器对应的 slots 只会记录该机器负责的槽,其他机器负责的槽记录为0

    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
1

但是这么做会导致很多操作会消耗巨大的时间复杂度,比如查询某个槽应该由哪个节点负责,我们就不得不遍历所有节点的所有槽。因此我们还需要使用 clusterState 一起记录信息

    clusterNode *slots[CLUSTER_SLOTS];
1

clusterState 里的 slots 记录了集群中节点的指针,所有槽都可以通过指针很快的找到对应关系,而没有节点负责的槽会指向 null

了解了槽的分配方式,我们下一步应该去了解节点是如何判断数据属于哪个槽的,我们知道 redis 是储存键值对的数据库。因此由相应的 hash 函数将数据转换为数字,我们可以用数字来判断槽的位置。redis 中使用以下的 hash 算法

def slot_number(key):
	return CRC16(key) & 16383
1
2

16384的二进制为100000000000000,16383则为多个连续的1,将利用 CRC16 算法算出来的数据进行按位与运算,可以保证数据在槽数组中一定有相应位置。因此节点判断某个数据是否在自己负责的槽中,只需要根据算出来的结果与 clusterState 进行比较即可

如果该位置是其他节点的 clusterNode 指针,则根据相应的 clusterNode 内容拿到对应的 IP 以及端口,向用户返回 MOVED 即可,随后用户的客户端会自动根据返回的结果访问相应的节点

# 重新分片

我们先来看看普通的重新分片操作,在某个机器运行良好的情况下将这台机器上的一部分槽分配给另外一条机器(如果机器挂了,还是老老实实让从机上线吧)。redis 通过 redis-trib 这个集群管理软件负责执行的,redis-trib.rb 是官方提供的 Redis Cluster 的管理工具,无需额外下载,默认位于源码包的 src 目录下,但因该工具是用 ruby 开发的,所以需要准备相关的依赖环境(trib 是三边形的意思)

机器收到指令后会指行以下的操作:

1,redis-trid 会向目标节点发送 cluster setslot 槽 importing 源节点 IP 这个命令,让目标准备接受数据,然后向源节点发送 cluster setslot 槽 migrating 目标 IP 命令让源节点准备发送数据(migrate 迁移) 2,redis-trid 发送命令获取最多 x 个键,然后向源节点发送命令,原子的将数据迁移到目标节点中。重复这一过程,直到所有的数据迁移完毕 3,redis-trid 向其他节点发送消息,让其他节点中的数据中的槽指派给目标节点

cluster setslot importing 命令会让目标节点在 clusterState 中的 importing_slots_from 发生变化。如果该属性的槽指向不为 null,而是一个 clusterNode,代表现在该节点的这些槽正在接受目标节点的数据迁移

    clusterNode *importing_slots_from[CLUSTER_SLOTS];
1

cluster setslot migrating 命令会让源节点的 migrating_slots_to 属性发生变化,如果该属性的槽指向不为 null,而是一个 clusterNode,代表现在该节点的这些槽正在向目标节点迁移数据

    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
1

在迁移的过程中如果用户访问正在迁移的键怎么办呢?由于数据要么在源节点要么在目标节点,我们可以根据这个特性来解决问题

由于此时其他节点的槽还并没有改变,因此请求会打到源节点,在迁移数据时维持高可用的做法就是,节点会判断 key 在不在自己的数据库中,如果没找到,去用该 key 对应的槽查找 migrating_slots_to 数组中对应的下标,如果该下标不为 null,会发送 ASK 错误给用户,而 ASK 中带有目标节点的 IP 与端口。如果下标为 null,那就是没有该数据了

# ASK、MOVED、ASKING

我们知道如果某个槽不由 A 节点处理,A 节点会返回给客户端一个 MOVED 命令,该命令指向了负责该区域的节点,但是在槽重新分配的时候,我们可能遇到更多的奇怪情况,因此 redis 做了一些特殊处理

比如 redis 是先转移数据再转移槽的,假如这时候 A 节点向 B 节点转移数据,槽还在 A,数据已经在 B 了。那么会发生如下情况:

客户端访问 A 以使用某个键的值,redis 会先检测该数据的槽是否由 A 负责,如果是,并且在 A 中没有找到这个数据的时候,A 会检测自己 clusterState.migrating_slots_to[i] 中的值,看对应的槽是否在进行迁移,如果是,节点会发送一个 ASK 命令指引客户端去访问正确的节点

为什么不发送 MOVED 命令呢,因为它们有不同的使用范围,MOVED 是数据分片完成之后的类似永久重定向的命令,而 ASK 只是一种暂时的措施,只在数据迁移时临时发挥作用保证数据的正确性

ASK 与 MOVED 还有一个区别,ASK 在访问正在导入槽的节点的时候会向这个节点先发送 ASKING 命令,再发送语句执行命令。ASKING 命令唯一的用处就是让该集群去检查自己的 clusterState.importing_slots_from[i],如果该节点正在导入槽 i,则破格执行该命令。因为此时,这个节点还没有负责槽 i,只是在导入数据而已

# 复制与故障转移

在做高可用系统的时候,不可能只有一个机子负责一部分的槽,如果该机器挂了,我们应该使用一定的错误检测手段,以及发现故障之后的维护高可用的故障转移手段

联系之前学习过的哨兵集群,我们可能想到所有的节点都配置上子节点,这样在主节点崩溃之后就可以快速替换了。而我们还可以使用哨兵来实现故障的检测与自动转移,但是,我们都用心跳检测来保证集群的检测了,还有必要配置哨兵浪费资源吗

redis 是这么做来进行检测与自动转移的:集群中每个节点都会定期的向集群中的其他节点发送 PING 消息,也就是心跳检测。如果在一定时间范围内,没有收到对方的回应,则该节点认定对方为疑似下线状态

集群中的状态可能是在线状态,疑似下线状态以及已下线状态。当其他节点收到某个节点将另外一个节点标记为疑似下线状态,还会在自己的某个字典中添加疑似下线节点的下线报告

如果集群中半数以上的节点都认为某个节点疑似下线,则会进入故障转移阶段。大体过程为,选择一个从节点成为主节点,获取获取所有的数据以及槽,改变集群中的数据结构

而从节点就用来复制主节点内容,以及在检测到主节点崩溃之后自己上线为主节点,用来的主节点重新上线后,会变成从节点。这里面的要点有这么几个

  • 一个主节点可能有很多从节点,如何确定是哪一个从节点应该替换主节点呢?
  • 在从节点复制主节点时,会发生什么,换句话说,从节点的相关信息在哪些节点的哪些数据结构中保存呢?

确定是哪一个从节点应该替换主节点跟配置纪元有关。当一个从节点发现自己复制的主节点处于已下线状态(接受到集群中的关于主节点的 FAIL 消息),就会开始进行投票选举,过程很简单:

1,集群的配置纪元(epoch,这里特指 current epoch,因为 redis 中还有一个纪元)是一个自增计数器,表示了选举的版本号。每进行一轮投票,该计数器加1,纪元的值会在投票消息中发给各个节点,这样解决了时间层次上的冲突

2,而在纪元修改完毕之后,该挂了的主节点下所有的从节点都会向集群中的每个主节点广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息以要求主节点投票。每个主节点在一个配置纪元内只会投一次票,投给第一个符合条件的从节点(这里的符合条件指的是会选择复制偏移量最大的从节点),当一个从节点收到了大于等于集群中一半机器的投票,该从节点会成为主节点,如果所有的从节点都没有收到一半的票数,则纪元加一重新开始投票

投票的过程和 Sentinel 方法类似,它们都参考了 Raft 算法的领头选举方法来实现。投票保证了该从节点与每个主节点都能联通,而只投给第一个其实保证了该从节点与其他主节点的联系速度最快

# gossip 协议

Redis 分片之间也通过 Gossip 协议进行通讯,Gossip 协议百度翻译成中文是八卦,简单点说是一种弱最终一致性算法,主要用于解决大规模去中心化 P2P 网络中的数据一致性问题

算法的灵感来自著名的六度分隔理论,即你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过六个人你就能够认识任何一个陌生人

因此,节点只需要向自己认识的节点发送消息就行了,gossip 协议的目标是把一些发生的事件传播到整个集群之中

该协议发送数据只干两件事:

1,随机选择 N 个尚未发送过消息的邻接节点发送消息 2,收到信息的节点等待一段时间,再重复上述步骤

# 消息

集群中的每个节点通过发送消息与接受消息来通信。你可能觉得这句话是废话,但是它非常重要,它规定了节点之间通信必须遵守的原则。节点之间互相发送的消息大体分为以下5种:

1,MEET 消息,该消息用于握手。发送者会向接收者发送 MEET 消息让接收者将自己拉进集群中。也就是说一台新机器进入集群的时候会使用到 gossip 协议

2,PING 消息,用于心跳检测。有以下两种情况节点会发送 PING 消息:一是 redis 每过一秒,节点会选择出集群中随机五个节点,向其中最长时间没有发送过 PING 消息的节点发送 PING。二是某个节点发现集群中有节点的最长时间没有发送过 PING 消息的时间已经超过自己的 cluster-node-timeout 设置时长的一半,节点会向其发送消息

3,PONG 消息,该消息主要用于回应 PING 消息与 MEET 消息,只有完整发送 PING 以及回传 PONG 才算一次完整的心跳。它还可以用来刷新集群中其他节点对自己的认知,比如故障转移完成后,可以直接广播一条 PONG 来告诉其他节点自己成为了主节点

4,FAIL 消息,当节点判断另外一个节点进入 FAIL 状态的时候,会向集群中广播 FAIL 消息,所有接受到的节点将会将这个下线的节点标记,如果他自己也确认这个节点下线了,则会标记这个节点下线

5,PUBLISH 消息,当节点接受到该命令时,会执行该命令并且广播该消息

#Redis
最后更新: 2/23/2026, 9:23:04 AM
ThreadLocal 原理以及使用
Redis 数据结构、对象与数据库

← ThreadLocal 原理以及使用 Redis 数据结构、对象与数据库→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式