Redis - 8 ( 10000 字 Redis 入门级教程 )

一:集群

1.1 基本概念

上述哨兵模式提升了系统的可用性,但存储数据的核心依然是主节点和从节点。所有数据都需要存储在单个主节点和从节点中,当数据量过大接近或超出机器的物理内存时,可能会导致严重问题。虽然硬件成本不断降低,部分企业的服务器内存已经达到 TB 级别,但在当今"大数据"时代,1TB 内存往往不足以满足需求。我们的解决方法很简单:增加机器!"大数据"的核心思想就是一台机器无法搞定时,用多台机器来分担。基于这一思路,Redis 引入了集群模式(Cluster),通过多组主从节点存储数据全集的不同部分,从而构成一个更大的整体,即 Redis 集群。例如,如果整个数据全集为 1TB,使用三组主从节点存储数据,那么每组只需存储全集的三分之一即可。

主从节点组 数据内容 占总数据比例
Master1, Slave11, Slave12 保存相同的数据 占总数据的 1/3
Master2, Slave21, Slave22 保存相同的数据 占总数据的 1/3
Master3, Slave31, Slave32 保存相同的数据 占总数据的 1/3

这三组机器存储的数据彼此不同,每个从节点都是对应主节点的备份。当主节点发生故障时,其对应的从节点会补位成为新的主节点。每个红框部分可以被称为一个分片(Sharding)。如果全量数据进一步增加,只需通过增加更多的分片即可解决存储需求问题,从而实现数据的水平扩展。

1.2 数据分片算法

Redis Cluster 的核心思想是通过多组机器存储数据的不同部分,因此关键问题在于如何确定数据的分片位置。具体来说,给定一个数据(即某个具体的 key),需要明确它应该存储在哪个分片,以及在读取时应该从哪个分片获取数据。针对这一问题,业界主要有三种主流的实现方式。

1.2.1 哈希求余

假设有 N 个分片,使用 [0, N-1] 的序号进行编号。对于给定的 key,首先计算其 hash 值,然后对 N 取模(% N),得到的结果即为对应的分片编号。例如,当 N 为 3,给定 key 为 "hello",通过计算其 hash 值(例如使用 MD5 算法)得到结果为 bc4b2a76b9719d91,再对 3 取模,结果为 0,因此将 "hello" 这个 key 存储在 0 号分片上。同样,在需要读取某个 key 的记录时,也通过计算其 hash 值并对 N 取模,就能快速找到对应的分片编号。实际系统中计算 hash 的方式可能不是 MD5,但核心思想是一致的。

该方法的优点是简单高效且数据分布均匀,但缺点在于扩容时会引发较大的数据迁移成本。当分片数量 N 改变时,原有的映射规则被破坏,需要节点之间相互传输和重新排列数据,以满足新的映射规则,导致数据搬迁量大,开销较高。例如,当 N 为 3 时,[100, 120] 这 21 个 hash 值均匀分布在 3 个分片上(假设 hash 值为简单整数,便于观察);但当新增一个分片,N 从 3 增加到 4 时,由于 key % 3 和 key % 4 的结果不同,大量 key 需要重新映射到新机器,增加了数据搬迁量。

1.2.2⼀致性哈希算法

为了解决扩容时大量数据搬迁的问题,业界提出了一致性哈希算法。与传统的取模方式不同,一致性哈希算法通过改进 key 映射到分片序号的方式,显著降低了扩容或缩容时的数据搬迁量,仅需重新映射少部分数据,从而实现更高效的扩容和数据分布。

  1. 将数据空间 [0, 2^32-1] 映射到一个圆环上,数据按照顺时针方向递增。
  1. 假设当前存在三个分片,则将这三个分片映射到圆环上的指定位置。
  1. 假设有一个 key,其 hash 值为 H,那么该 key 的映射规则是:从 H 所在位置开始,沿圆环顺时针查找,找到的第一个分片即为该 key 所属的分片。
  1. 这相当于将圆环按照 N 个分片的位置划分为 N 个管辖区间,key 的 hash 值落在哪个区间内,就归该区间对应的分片管理。
  1. 在这种情况下,如果需要扩容一个分片,可以直接在圆环上新增一个分片的位置,而原有分片的位置保持不变即可。

1.2.3 哈希槽分区算法 (Redis 使用)

为了解决上述问题(搬迁成本高和数据分布不均),Redis Cluster 引入了哈希槽(hash slots)算法。该算法将整个哈希空间划分为 16384 个槽位(即 [0, 16383]),并将这些槽位较为均匀地分配给各个分片。每个分片的节点都会记录自己持有的哈希槽范围,假设当前有三个分片,一种可能的分配方式是:

  • 0 号分⽚: [0, 5461] 共 5462 个槽位
  • 1 号分⽚: [5462, 10923] 共 5462 个槽位
  • 2 号分⽚: [10924, 16383] 共 5460 个槽位

如果需要扩容,比如新增一个 3 号分片,可以通过重新分配原有槽位来实现扩展。例如,从每个现有分片中分出一部分槽位分配给新分片。这种方式不仅能够保持数据的均匀分布,还能最大程度减少数据迁移量。在实际使用 Redis 集群分片时,无需手动指定哪些槽位分配给某个分片,只需指定某个分片应持有的槽位数量,Redis 会自动完成槽位的分配以及相关 key 的搬运工作。

  • 0 号分⽚: [0, 4095] 共 4096 个槽位
  • 1 号分⽚: [5462, 9557] 共 4096 个槽位
  • 2 号分⽚: [10924, 15019] 共 4096 个槽位
  • 3 号分⽚: [4096, 5461] + [9558, 10923] + [15019, 16383] 共 4096 个槽位

1.3 基于 Docker 集群搭建

接下来将基于 Docker 搭建一个 Redis 集群,其中每个节点都对应一个容器。集群的拓扑结构如下:

1.3.1 创建目录和配置

  1. 创建 redis-cluster ⽬录,内部创建两个文件:

    redis-cluster/
    ├── docker-compose.yml
    └── generate.sh

  2. generate.sh 内容如下:

    创建 9 个 Redis 节点配置文件

    for port in (seq 1 9); do # 创建对应节点的目录 mkdir -p redis{port}/
    # 创建 Redis 配置文件
    touch redis{port}/redis.conf # 写入配置文件内容 cat << EOF > redis{port}/redis.conf
    port 6379 # Redis 节点对外服务的端口
    bind 0.0.0.0 # 允许外部访问
    protected-mode no # 关闭保护模式
    appendonly yes # 开启 AOF 持久化
    cluster-enabled yes # 启用集群模式
    cluster-config-file nodes.conf # 集群节点的配置文件
    cluster-node-timeout 5000 # 集群节点通信超时时间(毫秒)
    cluster-announce-ip 172.30.0.10${port} # 每个节点的对外 IP 地址(注意 IP 动态变化)
    cluster-announce-port 6379 # 节点对外服务的 Redis 端口
    cluster-announce-bus-port 16379 # 节点的集群总线通信端口
    EOF
    done

    创建第 10 和第 11 个 Redis 节点配置文件(注意 IP 格式不同)

    for port in (seq 10 11); do # 创建对应节点的目录 mkdir -p redis{port}/
    # 创建 Redis 配置文件
    touch redis{port}/redis.conf # 写入配置文件内容 cat << EOF > redis{port}/redis.conf
    port 6379 # Redis 节点对外服务的端口
    bind 0.0.0.0 # 允许外部访问
    protected-mode no # 关闭保护模式
    appendonly yes # 开启 AOF 持久化
    cluster-enabled yes # 启用集群模式
    cluster-config-file nodes.conf # 集群节点的配置文件
    cluster-node-timeout 5000 # 集群节点通信超时时间(毫秒)
    cluster-announce-ip 172.30.0.1${port} # 每个节点的对外 IP 地址(注意 IP 动态变化)
    cluster-announce-port 6379 # 节点对外服务的 Redis 端口
    cluster-announce-bus-port 16379 # 节点的集群总线通信端口
    EOF
    done

  3. 执行 bash generate.sh 命令后生成如下目录:

    redis-cluster/
    ├── docker-compose.yml # Docker Compose 配置文件,用于定义集群的服务
    ├── generate.sh # 用于批量生成 Redis 配置文件的脚本
    ├── redis1/ # Redis 节点 1 的配置目录
    │ └── redis.conf # Redis 节点 1 的配置文件
    ├── redis2/ # Redis 节点 2 的配置目录
    │ └── redis.conf # Redis 节点 2 的配置文件
    ├── redis3/ # Redis 节点 3 的配置目录
    │ └── redis.conf # Redis 节点 3 的配置文件
    ├── redis4/ # Redis 节点 4 的配置目录
    │ └── redis.conf # Redis 节点 4 的配置文件
    ├── redis5/ # Redis 节点 5 的配置目录
    │ └── redis.conf # Redis 节点 5 的配置文件
    ├── redis6/ # Redis 节点 6 的配置目录
    │ └── redis.conf # Redis 节点 6 的配置文件
    ├── redis7/ # Redis 节点 7 的配置目录
    │ └── redis.conf # Redis 节点 7 的配置文件
    ├── redis8/ # Redis 节点 8 的配置目录
    │ └── redis.conf # Redis 节点 8 的配置文件
    ├── redis9/ # Redis 节点 9 的配置目录
    │ └── redis.conf # Redis 节点 9 的配置文件
    ├── redis10/ # Redis 节点 10 的配置目录
    │ └── redis.conf # Redis 节点 10 的配置文件
    ├── redis11/ # Redis 节点 11 的配置目录
    │ └── redis.conf # Redis 节点 11 的配置文件

  4. 每个 redis.conf 文件的内容几乎相同,唯一的区别在于每个配置文件中的 cluster-announce-ip 设置不同,以区分每个节点的 IP 地址。

    port 6379 # Redis 服务端口,用于客户端连接
    bind 0.0.0.0 # 绑定所有网络接口,允许外部访问
    protected-mode no # 关闭保护模式,适用于集群模式
    appendonly yes # 开启 AOF(Append Only File)持久化
    cluster-enabled yes # 启用集群模式
    cluster-config-file nodes.conf # 存储集群节点配置的文件,Redis 会自动生成和管理
    cluster-node-timeout 5000 # 集群节点之间通信的超时时间(毫秒)
    cluster-announce-ip 172.30.0.101 # 节点对外广播的 IP 地址,用于集群间通信
    cluster-announce-port 6379 # 节点对外广播的服务端口
    cluster-announce-bus-port 16379 # 节点用于集群总线通信的端口

1.3.2 编写 docker-compose.yml

首先创建网络,并将网段分配为 172.30.0.0/24。接下来配置每个节点,包括配置文件映射、端口映射以及容器的固定 IP 地址。设置固定 IP 地址是为了便于后续的观察和操作。端口映射可以选择配置或不配置,配置的目的是方便通过宿主机的 IP 加映射的端口进行访问,而通过容器自身的 IP 地址加默认端口 6379 也同样可以访问。

version: '3.7'
networks:
  mynet:
    ipam:
      config:
        - subnet: 172.30.0.0/24

services:
  redis1:
    image: 'redis:5.0.9'                # 使用 Redis 5.0.9 官方镜像
    container_name: redis1              # 容器名称,方便管理和识别
    restart: always                     # 设置容器在失败后自动重启
    volumes:
      - ./redis1/:/etc/redis/           # 挂载主机目录 ./redis1 到容器内的 /etc/redis,用于加载配置文件
    ports:
      - 6371:6379                       # 将宿主机的 6371 端口映射到容器的 Redis 服务端口 6379
      - 16371:16379                     # 将宿主机的 16371 端口映射到容器的集群总线端口 16379
    command: redis-server /etc/redis/redis.conf  # 启动容器时执行的命令,加载挂载的 Redis 配置文件
    networks:
      mynet:                            # 指定容器所属的网络
        ipv4_address: 172.30.0.101      # 为容器分配固定的 IP 地址,便于集群通信和管理

  redis2:
    image: 'redis:5.0.9'
    container_name: redis2
    restart: always
    volumes:
      - ./redis2/:/etc/redis/
    ports:
      - 6372:6379
      - 16372:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.102

  redis3:
    image: 'redis:5.0.9'
    container_name: redis3
    restart: always
    volumes:
      - ./redis3/:/etc/redis/
    ports:
      - 6373:6379
      - 16373:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.103

  redis4:
    image: 'redis:5.0.9'
    container_name: redis4
    restart: always
    volumes:
      - ./redis4/:/etc/redis/
    ports:
      - 6374:6379
      - 16374:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.104

  redis5:
    image: 'redis:5.0.9'
    container_name: redis5
    restart: always
    volumes:
      - ./redis5/:/etc/redis/
    ports:
      - 6375:6379
      - 16375:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.105

  redis6:
    image: 'redis:5.0.9'
    container_name: redis6
    restart: always
    volumes:
      - ./redis6/:/etc/redis/
    ports:
      - 6376:6379
      - 16376:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.106

  redis7:
    image: 'redis:5.0.9'
    container_name: redis7
    restart: always
    volumes:
      - ./redis7/:/etc/redis/
    ports:
      - 6377:6379
      - 16377:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.107

  redis8:
    image: 'redis:5.0.9'
    container_name: redis8
    restart: always
    volumes:
      - ./redis8/:/etc/redis/
    ports:
      - 6378:6379
      - 16378:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.108

  redis9:
    image: 'redis:5.0.9'
    container_name: redis9
    restart: always
    volumes:
      - ./redis9/:/etc/redis/
    ports:
      - 6379:6379
      - 16379:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.109

  redis10:
    image: 'redis:5.0.9'
    container_name: redis10
    restart: always
    volumes:
      - ./redis10/:/etc/redis/
    ports:
      - 6380:6379
      - 16380:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.110

  redis11:
    image: 'redis:5.0.9'
    container_name: redis11
    restart: always
    volumes:
      - ./redis11/:/etc/redis/
    ports:
      - 6381:6379
      - 16381:16379
    command: redis-server /etc/redis/redis.conf
    networks:
      mynet:
        ipv4_address: 172.30.0.111

编写完后启动容器:

docker-compose up -d

1.3.3 构建集群

# 使用 redis-cli 创建 Redis 集群
redis-cli --cluster create \
  172.30.0.101:6379 \  # 节点 1 的 IP 和端口
  172.30.0.102:6379 \  # 节点 2 的 IP 和端口
  172.30.0.103:6379 \  # 节点 3 的 IP 和端口
  172.30.0.104:6379 \  # 节点 4 的 IP 和端口
  172.30.0.105:6379 \  # 节点 5 的 IP 和端口
  172.30.0.106:6379 \  # 节点 6 的 IP 和端口
  172.30.0.107:6379 \  # 节点 7 的 IP 和端口
  172.30.0.108:6379 \  # 节点 8 的 IP 和端口
  172.30.0.109:6379 \  # 节点 9 的 IP 和端口
  --cluster-replicas 2 # 为每个主节点分配 2 个从节点

执行创建命令后,各容器会自动加入集群,此时客户端连接集群中的任意节点,就相当于连接了整个集群。客户端连接时需要使用 -c 选项,否则当操作的 key 不属于当前节点时,无法完成请求。而使用 -c 选项可以自动将请求重定向到对应的节点。此外,通过执行 cluster nodes 命令,可以查看整个集群的状态和节点分布情况。

1.3.4 故障判定

集群中的所有节点会通过定期发送心跳包来进行通信。

步骤 描述
1 节点 A 向节点 B 发送 ping 包,节点 B 会返回一个 pong 包。ping 和 pong 除了消息类型不同外,其他内容相同,包含集群配置信息(如节点 ID、所属分片、节点角色是主节点还是从节点、从节点的主节点 ID、持有的哈希槽位图等)。
2 每个节点每秒钟会随机向部分节点发送 ping 包,而不是向所有节点发送,避免当节点数量较多时心跳包过多。例如,在 9 个节点的集群中,如果每个节点都向其他所有节点发送心跳包,则会产生 72 组通信,且通信量随节点数按 N^2 增长。
3 当节点 A 向节点 B 发送 ping 包,但未能收到 B 的 pong 响应时,A 会尝试重置与 B 的 TCP 连接。如果重置后仍然失败,A 会将 B 标记为 PFAIL 状态(即主观下线)。
4 A 将 B 标记为 PFAIL 后,会通过 Redis 内置的 Gossip 协议与其他节点通信,确认 B 的状态。每个节点维护自己的"下线列表",但由于网络视角不同,各节点的下线列表可能有所不同。
5 如果 A 发现多数节点(超过集群总节点数的一半)也认为 B 是 PFAIL,则 A 会将 B 标记为 FAIL(即客观下线),并将此消息同步到其他节点,其他节点接收后也会将 B 标记为 FAIL。

至此,节点 B 被彻底判定为故障节点。在某些情况下,节点的故障可能会导致整个集群进入宕机状态(称为 fail 状态)。以下三种情况会导致集群宕机:第一,某个分片的所有主节点和从节点都故障;第二,某个分片的主节点故障,但没有从节点进行替补;第三,超过半数的主节点故障。集群的核心原则是确保每个哈希槽(slots)都能正常工作,保证数据的存取功能不受影响。

1.3.5 故障迁移

当节点 B 故障,并且节点 A 将 B 的 FAIL 消息同步给集群中的其他节点时,根据 B 的角色处理方式不同:如果 B 是从节点,则无需进行故障迁移;如果 B 是主节点,则由 B 的从节点(如 C 和 D)触发故障迁移。故障迁移的过程是将从节点提升为主节点,以继续支持 Redis 集群的正常运行。

步骤 描述
1 从节点会首先判断自己是否具有参选资格。如果从节点与主节点通信中断的时间超过设定阈值(数据差异过大),则失去竞选资格。
2 具有参选资格的从节点(如 C 和 D)会进入休眠状态一段时间。休眠时间由以下公式计算:500ms 基础时间 + [0, 500ms] 的随机时间 + 排名 * 1000ms。排名基于数据同步偏移量(offset),offset 值越大,排名越靠前(休眠时间越短)。
3 当某个从节点(如 C)的休眠时间结束后,C 会向集群中的所有节点发起拉票请求。只有主节点才有投票资格。
4 主节点会将自己的票投给 C(每个主节点只有 1 票)。当 C 获得的票数超过主节点总数的一半时,C 会被提升为新的主节点。C 随后会执行 slaveof no one,并指示 D 执行 slaveof C,完成主从切换。
5 C 会将自己成为主节点的信息同步给集群中的其他节点,所有节点都会更新其本地保存的集群结构信息。

1.3.6 集群扩容

扩容是开发中常见的场景之一。随着业务的发展,现有集群可能无法容纳日益增长的数据,这时可以通过加入更多的节点来扩展集群,增加存储容量和计算能力。本质上,分布式系统的核心思想就是通过引入更多的机器和硬件资源来满足不断增长的需求。

  1. 把新的主节点加⼊到集群

前面已经将 redis1 至 redis9 重新组建为集群,接下来需要将 redis10 和 redis11 加入集群。其中,将 redis10 配置为主节点,redis11 配置为其从节点。

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
  1. 重新分配 slots

    redis-cli --cluster reshard 172.30.0.101:6379

在执行 reshard 操作时,提供的地址可以是集群中的任意节点地址。执行后将进入交互式操作,Redis 会提示用户输入以下信息:需要迁移的 slots 数量(此处填写 4096);接收这些 slots 的目标节点(填写 172.30.0.110 的集群节点 ID);以及需要从哪些节点迁移 slots(填写 all,表示从集群中的所有节点进行搬运)。

  1. 给新的主节点添加从节点

目前集群已新增主节点,初步实现了扩容目标。但为了提升集群的高可用性,还需要为新的主节点添加从节点,以确保主节点宕机时,从节点能够顶替其角色。操作完成后,从节点会成功添加到集群中,保障了集群的容错能力。

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 \  # 将新节点加入到 Redis 集群
  --cluster-slave \                                                 # 指定新加入的节点为从节点
  --cluster-master-id [172.30.0.110 节点的 nodeId]                  # 指定该从节点的主节点 ID(需要填写主节点 172.30.0.110 的 nodeId)
相关推荐
✎﹏ℳ๓₯㎕4 分钟前
vue使用keep-alive实现页面前进刷新,后退缓存
前端·vue.js·缓存
东方不败之鸭梨的测试笔记7 分钟前
需求上线,为什么要刷缓存?
缓存
梁萌7 分钟前
缓存-文章目录
缓存
代码代码快快显灵7 分钟前
Redis的数据结构(基本)
数据库·redis·缓存
一条小小yu9 分钟前
从零开始手写缓存之如何实现固定缓存大小
java·spring·缓存
SG.xf41 分钟前
企业级Nosql数据库和Redis集群
数据库·redis·nosql
鸿永与1 小时前
『SQLite』安装与基本命令语法
数据库·sqlite
鸿永与1 小时前
『SQLite』常见数据类型(动态类型系统)
java·数据库·sqlite
Dann Hiroaki1 小时前
文献分享:BGE-M3——打通三种方式的嵌入模型
数据库·人工智能·深度学习·自然语言处理·全文检索·bert
河南查新信息技术研究院2 小时前
在科技查新中怎样判定其项目的新颖性?
数据库·科技·全文检索