您的当前位置:首页正文

Redis 篇-初步了解 Redis 持久化、Redis 主从集群、Redis 哨兵集群、Redis 分片集群

2024-11-01 来源:个人技术集锦

        1.0 分布式缓存概述

        2.0 Redis 持久化

        2.1 RDB 持久化

        2.1.1 RDB 的 fork 原理

        2.2 AOF 持久化

        2.3 RDB 与 AOF 之间的区别

        3.0 Redis 主从集群

        3.1 搭建主从集群

        3.2 主从的全量同步原理

        3.3 主从的增量同步原理

        4.0 Redis 哨兵集群

        4.1 搭建哨兵集群

        4.2 RedisTemplate 连接哨兵

        5.0 Redis 分片集群

        5.1 搭建分片集群

        5.2 散列插槽

        5.3 集群伸缩

        5.4 故障转移

        5.5 RedisTemp 访问分片集群


        1.0 分布式缓存概述

        分布式缓存是一种用于提高系统性能和可扩展性的技术,它通过在多个节点上存储数据副本来减少数据访问延迟和负载。分布式的出现是为了解决在单点 Redis 的问题。

        在单点 Redis 存在的问题:

        数据丢失问题:通过实现 Redis 数据持久化来解决当前问题。

        并发能力问题:通过搭建主从集群,实现读写分离来解决当前问题。

        存储能力问题:搭建分片集群,利用插槽机制实现动态扩容。

        故障恢复问题:利用 Redis 哨兵,实现健康检测和自动恢复。

        2.0 Redis 持久化

        因为 Redis 存储的数据都是在内存中的,所以当 Redis 出现宕机或者断电等情况导致停止服务,那么 Redis 中的数据就会直接丢失掉。

        让 Redis 中的数据可以持久化,也就是将 Redis 中的相关的数据保存在磁盘中。因此,Redis 持久化常见有两种形式:RDB 持久化、AOF 持久化。

        这样就算 Redis 服务出现了宕机,不需要担心数据丢失,可以从磁盘中重新读取之前的数据再进行服务。

        2.1 RDB 持久化

        RDB 全称 Redis Database Backup file(Redis 数据备份文件),也叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。

        1)通过执行 Save 命令,Redis 中的数据就会被重新创建一个 dump.rdb 文件记录,来替换原先的文件。

        通过时间,可以看出来已经替换成功了。证明 save 可以用来将 Redis 内存数据进行拷贝到磁盘中,来保证数据持久化。

        但是使用 save 命令会有一个弊端:save 命令由 Redis 主进程来执行 RDB,因此会阻塞所有命令。通过 save 命令来进行数据持久化,一般常用于将当前 Redis 服务器清除之前将数据保证起来,通常 Redis 服务正在运行的时候,是不会直接用 save 命令来保证数据持久化的,因为会阻塞其他请求,从而影响效率。

        在主动将 Redis 服务进行停机之前会自动执行 save 命令,将数据进行保存。重新启动该 Redis 服务之后,会自动从 RDB 文件中获取数据,重新将数据放到内存中。

        2)使用 bgsave 命令开启子进程执行 RDB,避免主进程受到影响。

Redis 内部由触发 RDB 的机制,可以在 Redis.conf 文件中找到格式如下:

        #3600 秒内,如果至少由一个 key 被修改,则执行 bgsave,如果是 save"" 则表示禁用 RDB

        符合条件就会自动触发,每隔一段时间自动触发。这就保证了在 Redis 服务过程中也可以将数据进行持久化。

RDB 的其他配置也可以在 Redis.conf 文件中设置:

        设置是否压缩 RDB 文件,建议不开启,压缩也会消耗 cpu,磁盘不值钱。

        设置 RDB 文件名称。

        2.1.1 RDB 的 fork 原理

        bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。完成 fork 后子进程读取内存数据并写入到 RDB 文件。

fork 采用的是 copy-on-write 技术:

        当主进程执行读操作时,访问共享内容。

        当主进程执行写操作时,则会拷贝一份数据,执行写操作。

        简单来说,主进程在进行 fork 子进程的时候会阻塞其他请求,因此需要加快 fork 子进程过程。主进程通过页表来操作内存,因为通过拷贝页表,子进程也就可以读取内存数据了。fork 过程结束之后,主进程解除阻塞状态,可以正常响应请求,而子进程则将内存数据进行读操作,写入磁盘中。在子进程写入磁盘过程中,主进程有可能会进行写操作,所有为了防止脏读的情况,在写数据的时候,将内存的数据进行拷贝,再来进行写操作。

        这也说明了当子进程写入数据的过程中,主进程写操作的请求越多,拷贝的数据也就越多,可能会导致数据溢出。

RDB 的缺点:

        RDB 执行间隔时间长,两次 RDB之间写入数据有丢失风险。

        fork 子进程、压缩、写出 RDB 文件都比较耗时。

        2.2 AOF 持久化

        AOF 全称为 Append Only File(追加文件)。Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件。

        当 Redis 服务需要重新启动,那么直接来读取 AOF 文件,执行所有命令。这就保证了数据持久化。

AOF 持久化,默认是关闭的:

        将 no 改为 yes,则启动 AOF 持久化,通过设置 appendfilename 来修改 AOP 文件名。

AOF 的命令记录的频率:

        AOF 也是异步的将命令写入到磁盘中。

# If unsure, use "everysec".

# 表示每执行一次命令,立即记录到AOF文件
# appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每个1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放到AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
# appendfsync no

        因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才意义。通过执行 bgrewriteaof 命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果。

当然不需要我们手动去敲命令,Redis 也会在触发阈值时自动去重写 AOF 文件:

        在配置文件中可以看到:

# AOF文件比上次文件增长超过百分之百则触发重写
auto-aof-rewrite-percentage 100

# AOF文件体积最小 64 mb以上才触发重写
auto-aof-rewrite-min-size 64mb

        2.3 RDB 与 AOF 之间的区别

        1)持久化方式:

        RDB:定时对整个内存做拷贝。

        AOF:记录每一次执行的命令。

        2)数据完整性:

        RDB:不完整,两次备份之间会丢失。

        AOF:相对完整,取决于刷盘策略。

        3)文件大小:

        RDB:会有压缩,文件体积小。

        AOF:记录命令,文件体积大。

        4)宕机恢复速度:

        RDB:很快。

        AOF:慢。

        5)数据恢复优先级:

        RDB:低,因为数据完整性不如 AOF。

        AOF:高,因为数据完整性更高。

        6)系统资源占用:

        RDB:高,大量CPU 和内存消耗。

        AOF:低,但是 AOF 重写时会占用大量 CPU 和内存资源。

        7)使用场景:

        RDB:可以容忍数分钟的数据丢失,追求更快的启动速度。

        AOF:对数据安全性要求较高。

        3.0 Redis 主从集群

        在主从集群架构中,主节点(Master)负责处理所有的写操作,而从节点(Slave)则负责处理读操作。这样可以将读写请求分开,从而提高系统的并发处理能力。

        通过搭建主从集群实现读写分离,来解决高并发能力问题。简单来说,就是通过搭建多个从节点来提供"只读"的服务。

        3.1 搭建主从集群

        使用一台云服务器搭建一个主节点和两个从节点。

root@iZ2ze0lv9jcc14x57qtu1dZ:~/temp# cp /opt/redis-6.2.5/redis.conf 7001
root@iZ2ze0lv9jcc14x57qtu1dZ:~/temp# cp /opt/redis-6.2.5/redis.conf 7002
root@iZ2ze0lv9jcc14x57qtu1dZ:~/temp# cp /opt/redis-6.2.5/redis.conf 7003

        接着分别将文件中 redis.conf 配置进行修改,比如:端口号、RDB 文件的存放路径。

        再来修改每个实例的声明 IP

        为了避免将来混乱,需要在 redis.conf 文件中指定每一个实例的绑定 ip 信息:

        启动三个 redis 服务:

        成功启动 redis 服务。

        最后来开启主从关系:

        将 7001 作为 master 主节点、而 7002、7003 作为 slave 从节点。

        其中有临时和永久两种模式:

                1)修改配置文件(永久生效)

                        在 redis.conf 中添加一行配置:

REPLICAOF <masterip> <masterport>

                2)使用 redis-cli 客户端连接到 redis 服务,执行 replicaof 命令(重启后会失效)

                接下来使用临时模式来实现一下:

        此时,已经将 7002 配置成 7001 的从节点。

        因此,不能进行写操作了,读写已经完成分离,主节点负责写操作,而从节点负责读操作。

         需要注意的是,如果主节点已经配置密码了,那么在连接主节点的时候,在从节点的配置文件中设置主节点密码:

         配置完之后,查看连接情况:

        现在已经 7002、7003 两个 slave 节点连接到 7001 master 节点上了。在 7001 中可以读和写,而 7002、7003 只能进行读操作。

        那么考虑一个问题,当 7001 完成写操作之后,7002 可以成功获取数据,数据同步的原理是什么呢?

        因为 7002、7003 自动完成了主从的全量同步或者是增量同步,详细讲解如下:

        3.2 主从的全量同步原理

        先来了解两个概念:

        1)Replication Id:简称 replid,是数据集的标记,id 一致则说明是同一数据集。每一个 master 都有唯一的 replid,slave 则会继承 master 节点的 replid 。

        2)offset:偏移量,随着记录在 rep_baklog 中的数据增多而逐渐增大。slave 完成同步也会记录当前同步的 offset 。如果 slave 的 offset 小于 master 的 offset,说明 slave 数据落后于 master,需要更新。

        当 slave 从节点执行完 replicaof 命令建立连接时,会发送 replid、offset 给 master 节点,接着 master 主节点会判断发送过来的 replid 是否跟当前的 master 的 replid 是否一致,

        如果一致,则说明已经同步过的 slave 从节点,则只需要将在 offset 之后的 repl_baklog 命令发送给 slave 从节点执行接收到的命令即可; 

        如果是不一致,则说明有可能是第一次来建立连接的,那么 master 先会返回主节点的 replid 和 offset ,slave 将版本信息进行保存,master 会 fork 子进程来执行 bgsave,生成 RDB 文件并且发送给 slave ,slave 先清空本地数据,再来加载 RDB 文件。

        在 slave 加载 RDB 文件的时候,master 也会不断接收写操作的请求命令,这些命令会先保存到 repl_baklog 文件中,等待 slave 加载完成之后,master 发送 repl_baklog 中的命令到 slave 节点中,而 slave 节点接收到命令就会执行。

        完成以上的步骤之后,slave 就完成了数据同步了。

        3.3 主从的增量同步原理

        增量同步一般都是用于 slave 重启后同步。

        slave 重启之后,给 master 发送 replid、offset,当 master 判断 replid 与当前 master 的 replid 一致,那么就不会触发 RDB 发送了,而是 master 去 repl_baklog 中获取 offset 后的命令,并且发送到 slave 中,slave 执行命令,这就是增量同步原理。

        需要注意:repl_baklog 大小有上限,写满后会覆盖最早的数据。如果 slave 断开时间过久,导致数据被覆盖,则无法实现增量同步,只能再次全量同步。

        需要知道的是,执行全量同步消耗的资源是非常大的,因此需要优化全量同步或者减少全量同步发生:

        1)在 master 中配置 repl-diskless-sync yes 启用无磁盘复制,避免全量同步的磁盘 IO 。

        2)Redis 单节点上的内存占用不要太大,减少 RDB 导致的过多磁盘 IO 。

        3)适当提高 repl_baklog 的大小、发现 slave 宕机时尽快实现故障,尽可能避免全量同步。

        4)限制 master 上的 slave 节点数量,如果实现是太多 slave,则可以采用主-从-从链式结构。减少 master 压力。

        4.0 Redis 哨兵集群

        思考:slave 节点宕机恢复后可以找 master 节点同步数据,那 master 节点宕机怎么办?

        这时候就需要用到哨兵了。

        Redis 提供了哨兵机制来实现主从集群的自动故障恢复,哨兵的主要功能:

        1)监控:哨兵会不断检查主从集群中的 master 和 slave 是否按预期工作。

        Sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping 命令:

        主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。

        客观下线:若超过指定数量的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 sentinel 实例数量的一半。

一旦发现 master 故障,sentinel 需要在 slave 中选择一个作为新的 master,选择依据:

        首先会判断 slave 与 master 节点断开时间长短,如果超过指定值(down-after-milliseconds*10)则会排除该 slave 节点。

        然后判断 slave 节点的 slave-priority 值,越小优先级越高,如果是 0 则永不参与选举。

        如果 slave-prority 一样,则判断 slave 节点的 offset 值,越大说明数据越新,优先级越高。

        最后是判断 slave 节点的运行 id 大小,越小优先级越高。

        2)自动故障恢复:如果 master 故障,哨兵会将一个 slave 提升为 master 。当故障实例恢复后也以新的 master 为主。

        故障转移步骤:

        sentinel 给备选的 slave 节点发送 slaveof no one 命令,让该节点成为 master。

        sentinel 给所有其他 slave 发送 slaveof 新选举的 master 的 IP 和 Port 命令,让这些 slave 成为新的 master 的从节点,开始从新的 master 上进行全量同步。

        最后,sentinel 将故障节点标记为 slave ,当故障节点恢复后会自动成为新的 master 的 slave 节点。

        3)通知:哨兵充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端。

        4.1 搭建哨兵集群

        搭建三个哨兵节点,哨兵的配置文件:

解释:

        port 27003:是当前 sentinel 实例的端口。

        sentinel monitor mymaster 8.152.162.150 7001 2:指定 master 节点信息:

                mymaster:主节点名称,自定义。

                8.152.162.159 7001:主节点的 IP 和 端口号。

                2:选举 master 时的 quorum 值。

需要注意:

        如果之前设置密码了,那么在 sentinel.conf 配置文件中添加如下配置:

##如果配置了密码打开下面的注释写上密码 格式: sentinel auth-pass mymaster 12345678
# sentinel auth-pass <master-name> <password>

        最后启动三个哨兵:

redis-sentinel s1/sentinel.conf
redis-sentinel s2/sentinel.conf
redis-sentinel s3/sentinel.conf

        现在三个 sentinel 已经开始运作了。

测试:

        现在手动将 7001 master 主节点停机,测试哨兵是如何进行故障转移:

        现在 master 已经停机了。

        哨兵也很快发现了 7001 停止服务了。

         接着重新选举新的 master,此时是 7002 被选中称为主节点。

        再接着哨兵会广播给其他 slave 节点,重新进行全量同步,这时候的主节点为 7002

        最后,将 7001 标记为 slave,当 7001 重启之后,是一个 slave,并且进行全量同步。

        4.2 RedisTemplate 连接哨兵

        在 sentinel 集群监管下的 redis 主从集群,其节点会因为自动故障转移而发生变化,Redis 的客户端必须感知这种变化,即使更新连接信息。Spring 的 RedisTemplate 底层利用 lettuce 实现了节点的感知和自动切换。

        1)在 pom 文件中引入 redis 的 starter 依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        2)然后在配置文件 application.yml 中指定 sentinel 相关信息:

spring:
  data:
    redis:
      password: ******
      host: 8.152.162.159
      lettuce:
        pool:
          max-active: 10
          max-idle: 10
          min-idle: 1
          time-between-eviction-runs: 10s
      database: 0
      port: 6379
      sentinel:
        master: mymaster
        nodes:
          - 8.152.162.159:27001
          - 8.152.162.159:27002
          - 8.152.162.159:27003
logging:
  level:
    io.lettuce.core: debug
  pattern:
    dateformat: MM-dd HH:mm:SSS

        3)配置主从读写分离

    @Bean
    public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }

        这里的 ReadFrom 是配置 Redis 的读取策略,是一个枚举,包括下面选择:

        MASTER:从主节点读取

        MASTER_PREFERRED:优先从 master 节点读取,master 不可用才读取 replica

        REPLICA:从 slave 节点读取

        RREPLICA_PREFERRED:优先从 slave 节点读取,所有的 slave 都不可用才读取 master。

做一个测试:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Text {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @GetMapping("get/{key}")
    public String getKey(@PathVariable String key){
        return stringRedisTemplate.opsForValue().get(key);
    }
    
    @GetMapping("set/{key}/{value}")
    public String setKey(@PathVariable String key,@PathVariable String value){
        stringRedisTemplate.opsForValue().set(key,value);
        return "OK";
    }

}

        当发送请求之前,会尝试与 sentinel 节点建立连接,再由哨兵节点去订阅 redis 主从节点,主要是从 slave 节点来获取数据。

        5.0 Redis 分片集群

        主从和哨兵可以解决高可用、高并发读的问题。但是依然会有两个问题没有解决:

        1)海量数据存储问题。

        2)高并发写的问题。

        使用分片集群可以解决上述问题,分片集群特征:

        1)集群中有多个 master,每个 master 保存不同数据。

        2)每个 master 都可以有多个 salve 节点

        3)master 之间通过 ping 监测彼此健康状态。

        4)客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

        5.1 搭建分片集群

         redis.conf 配置文件如下:

        不同节点的端口号需要进行调整,日志存放的位置也要进行调整。

        接着创建每个节点之间在集群中的关系:

命令说明:

        redis-cli --cluster 或者 ./redis-trib.rb:代表集群操作命令。

        create:代表创建集群。

        --replicas 1 或者 --cluster-replicas 1:指定集群中每个 master 的副本个数为 1,此时节点总数÷(replicas + 1)得到的就是 master 的数量。因此节点列表中的前 n 个就是 master,其他节点都是 slave 节点,随机分配得到不同的 master 。

查看集群状态:

redis-cli -p 7001 cluster nodes

        5.2 散列插槽

        Redis 的散列插槽(Hash Slots)是 Redis 集群模式下用于数据分布和路由的一种机制。Redis 集群将数据分散到多个节点上,以实现高可用性和可扩展性。

        Redis 集群总共有 16384 个散列槽。每个键在存储时会被映射到这 16384 个槽中的一个。Redis 使用 CRC16 散列算法来计算键的散列值,并通过取模运算将其映射到 0 到 16383 的范围内。例如,slot = CRC16(key) % 16384

        每个 Redis 节点负责一部分散列槽。通过将键映射到散列槽,Redis 可以确定该键应该存储在哪个节点上。在集群中,客户端在执行命令时会根据键的散列槽来确定目标节点。

        5.3 集群伸缩

        Redis 集群支持动态伸缩,允许用户根据需求增加或减少节点。

添加 7004 节点:

给 7004 分配插槽:

        5.4 故障转移

        利用 cluster failover 命令可以手动让集群中的某个 master 宕机,切换到执行 cluster failover 命令的这个 slave 节点,实现无感知的数据迁移。

流程如下:

7002 从节点转换为 master 的命令:

        5.5 RedisTemp 访问分片集群

        RedisTemplate 底层同样基于 lettuce 实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:

        1)引入 redis 的 starter 依赖

        3)配置读写分离

        与哨兵模式相比,其中只有分片集群的配置方式只是有一点点差异,如下:

显示全文