Redis主从复制&哨兵&集群搭建解析

Redis集群三种模式

redis群集有三种模式,分别是主从同步/复制哨兵模式Cluster

主从复制

哨兵

Redis 主从模式不具备自动容错和恢复功能,如果主节点宕机,Redis 集群将无法工作,此时需要人为干预,将从节点提升为主节点。
哨兵机制作用主要是监控主从节点,当主节点挂掉,通过内部投票机制,从 从节点当中选出一个主节点,这样可以避免人工成本。

虽然主从+哨兵采用了多节点,但是他们存在的目的主要是解决容灾问题,而并非性能问题。

Redis性能存在什么问题?

Redis 内存太大会导致 rdb文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实例内存往往都是受限的。
单个 Redis 实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作压力会非常大。
单台redis内存容量限制,如何进行扩容?继续加内存、加硬件么?

Cluster集群

Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分的更为精细,每个节点负责其中一部分槽位,槽位的信息存储于每个节点中。

当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要查找某个 key 时,可以直接定位到目标节点。

调整槽位:Redis Cluster 提供了工具 redis-trib 可以让运维人员手动调整槽位的分配情况,它使用Ruby 语言进行开发,通过组合各种原生的 Redis Cluster 指令来实现。把新的主节点加⼊到集群会引起槽位调整。

握手命令:

cluster meet 127.0.0.1 7001    //与ip为127.0.0.1,端口为 7001的节点握手
cluster nodes      //显示当前集群的节点信息

槽位重新平衡:

redis-client --cluster rebalance

槽分配命令:

127.0.0.1:7000> cluster addslots 0 1 2 3 ... 5000 //将0~5000的槽分配个7000节

1、通知目标节点准备导入属于槽slot的键值对。redis-trip向目标节点发送命令 (导入:import) 会修改目标节点的clusterState.importing_slots_from数组

 cluster setslot <slot> importing <source_id>    //slot为槽的编号,source_id为源节点id

2、通知源节点准备迁移属于槽slot的键值对。redis-trip向源节点发送命令 (迁移:migrate) 会修改源节点的clusterState.migrating_slots_to数组

cluster setslot <slot> migrating <target_id>    //target_id为目标节点id

3、从源节点处获取属于槽slot的键值对。redis-trip向源节点发送命令

cluster getkeysinslot <slot> <count>            //count为最多获取count个键值对

4、将获得的键值对从源节点迁移到目标节点。对于获得的每一个键值对,redis-trip都向源节点发送一个migrate命令

 migrate <target_id> <target_port> <key_name> 0 <timeout>    //目标节点id、端口、键名、超时时间设置

5、重复3、4两步操作,直到属于槽slot的所有键值对都成功从源节点迁移至目标节点

6、全部迁移成功后,将槽slot指派给目标节点。redis-trip向集群中任意一个节点发送命令,将slot指派给目标节点,

 cluster setslot <slot> NODE <target_id>         //正式指派槽slot给界目标id的节点
然后这个信息会通过消息发送到整个集群,最终所有节点都会知道这个消息

(指派成功后,通过消息发送,通知整个集群中的节点,然后节点们根据消息,对节点内的clusterState结构和clusterNode结构进行更新?)

hash槽存满了怎么办?
注意:hash槽不是用来存储数据的,时用来计算该把数据存储在哪个服务器里。相当于仓库的门,

Redis集群的槽位扩展主要涉及在现有集群中添加新节点,并将部分哈希槽(slot)从现有节点迁移到新节点,以实现集群的扩展。以下是一般步骤:

  • 准备新节点:
    在新的服务器上安装Redis,并确保该Redis实例可以与其他集群节点通信。配置新节点的Redis实例,使其与集群中的其他节点具有相似的配置(如内存限制、密码等)。
  • 启动新节点:
    启动新的Redis实例,并确保它处于集群模式。
  • 加入集群:
    使用redis-cli --cluster add-node <new_node_ip>:<new_node_port> <existing_node_ip>:<existing_node_port>命令将新节点添加到集群中。其中,<new_node_ip>和<new_node_port>是新节点的IP地址和端口,<existing_node_ip>和<existing_node_port>是集群中已存在节点的IP地址和端口。
  • 迁移槽位:
    • 使用redis-cli --cluster reshard <existing_node_ip>:<existing_node_port>命令迁移槽位。在命令执行过程中,需要指定要迁移的槽位数量、源节点和目标节点。
    • Redis集群的槽位迁移是自动的,它会将指定槽位上的所有数据从源节点迁移到目标节点。
  • 验证集群状态:
    使用redis-cli --cluster check <existing_node_ip>:<existing_node_port>命令检查集群状态,确保所有槽位都已正确分配,并且集群正常工作。
  • 注意事项:
    • 在扩展Redis集群时,要确保新节点与现有节点之间的网络连接稳定,并且具有足够的带宽来传输数据。
    • 在迁移槽位时,要确保集群中的其他操作不会受到干扰,可以考虑在低峰时段进行槽位迁移。
    • 如果集群中存在大量的数据,槽位迁移可能需要一些时间来完成。在此期间,集群的性能可能会受到影响,因此需要进行适当的监控和管理。

通过以上步骤,您就可以成功扩展Redis集群的槽位并添加新节点了。请注意,以上步骤仅供参考,具体操作可能会因Redis版本和集群配置而有所不同。

计算key字符串对应的映射值,redis采用了crc16函数而后与0x3FFF取低16位的方法。crc16以及md5都是比较经常使用的根据key均匀的分配的函数,就这样,用户传入的一个key咱们就映射到一个槽上,而后通过gossip协议,周期性的和集群中的其余节点交换信息,最终整个集群都会知道key在哪个槽上。

为什么是16384个槽位?

  • 节点之间通过⼼跳包通信. ⼼跳包中包含了该节点持有哪些 slots. 这个是使⽤位图这样的数据结构 表⽰的. 表⽰ 16384 (16k) 个 slots, 需要的位图⼤⼩是 2KB. 如果给定的 slots 数更多了, ⽐如 65536 个了, 此时就需要消耗更多的空间, 8 KB 位图表⽰了. 8 KB, 对于内存来说不算什么, 但是在频繁的⽹ 络⼼跳包中, 还是⼀个不⼩的开销的
  • Redis 集群⼀般不建议超过 1000 个分⽚. 所以 16k 对于最⼤ 1000 个分⽚来说是⾜够⽤ 的, 同时也会使对应的槽位配置位图体积不⾄于很⼤

Redis–集群Cluster(槽指派、重新分片)

Redis Cluster故障迁移


Twemproxy 、codis待学习

Redis 集群一致性保证

Redis集群不能保证强一致性。一些已经向客户端确认写成功的操作,会在某些不确定的情况下丢失。产生写操作丢失的第一个原因,是因为主从节点之间使用了异步的方式来同步数据。

一个写操作是这样一个流程:

  • 客户端向主节点B发起写的操作
  • 主节点B回应客户端写操作成功
  • 主节点B向它的从节点B1,B2,B3同步该写操作

从上面的流程可以看出来,主节点B并没有等从节点B1,B2,B3写完之后再回复客户端这次操作的结果。所以,如果主节点B在通知客户端写操作成功之后,但同步给从节点之前,主节点B故障了,其中一个没有收到该写操作的从节点会晋升成主节点,该写操作就这样永远丢失了。

如果真的需要,Redis集群支持同步复制的方式,通过WAIT 指令来实现,这可以让丢失写操作的可能性降到很低。但就算使用了同步复制的方式,Redis集群依然不是强一致性的,在某些复杂的情况下,比如从节点在与主节点失去连接之后被选为主节点,不一致性还是会发生。

主从复制:主从复制是高可用redis的基础,哨兵和Cluster集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。

  • 缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制。

哨兵:在主从复制的基础上,哨兵实现了自动化的故障恢复。
Redis 主从模式不具备自动容错和恢复功能,如果主节点宕机,Redis 集群将无法工作,此时需要人为干预,将从节点提升为主节点。
哨兵机制作用主要是监控主从节点,当主节点挂掉,通过内部投票机制,从 从节点当中选出一个主节点,这样可以避免人工成本。

  • 缺陷:写操作无法负载均衡;存储能力受到单机的限制;哨兵无法对从节点进行自动故障转移,在读写分离场景下,从节点故障会导致读服务不可用,需要对从节点做额外的监控、切换操作。

Cluster集群:通过集群,Rdis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。

Redis集群方案

Redis复制相关配置和参数介绍

Redis的复制配置主要包括以下几个参数:

  • bind:指定主节点的IP地址,从节点会连接到该IP地址上。
  • port:指定主节点的端口号。
  • requirepass:指定主节点的认证密码,从节点需要提供正确的密码才能连接到主节点。
  • slaveof:指定从节点连接的主节点的IP地址和端口号。
  • masterauth:指定从节点连接主节点的认证密码。

Redis复制过程

  • 主节点将写操作写入内存缓冲区,并将写操作发送给从节点。
  • 从节点接收到写操作后,将其写入自己的内存中。
  • 从节点周期性地向主节点发送SYNC命令,主节点接收到SYNC命令后会执行BGSAVE命令生成RDB快照文件。
  • 主节点将RDB快照文件发送给从节点,从节点接收到RDB快照文件后会将其加载到自己的内存中。
  • 从节点将主节点发送过来的写操作的增量数据进行合并,从而保持与主节点的数据同步。

主从复制

主从复制原理

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(Master),后者称为从节点(Slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点 (或没有从节点),但一个从节点只能有一个主节点。

主从复制作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis
    数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通
    过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从复制流程

  1. 若启动一个Slave机器进程,则它会向Master机器发送一个“sync command”命令,请求同步连接。
  2. 无论是第一次连接还是重新连接,Master机器都会启动一个后台进程,将数据快照保存到数据文件中(执行rdb操作),同时 Master 还会记录修改数据的所有命令并缓存在数据文件中。
  3. 后台进程完成缓存操作之后,Master机器就会向Slave机器发送数据文件,Slave端机器将数据文件保存到硬盘上,然后将其加载到内存中,接着Master机器就会将修改数据的所有操作一并发送给Slave端机器。若Slave出现故障导致宕机,则恢复正常后会自动重新连接。
  4. Master机器收到Slave端机器的连接后,将其完整的数据文件发送给Slave端机器,如果Mater同时收到多个Slave发来的同步请求,则Master 会在后台启动一个进程以保存数据文件,然后将其发送给所有的Slave端机器,确保所有的 Slave端机器都正常。

主从配置示例

主节点配置文件 redis.conf

bind 127.0.0.1
port 6379
requirepass foobared

从节点配置文件 redis.conf

bind 127.0.0.1
port 6380
requirepass foobared
slaveof 127.0.0.1 6379
masterauth foobared

启动主节点

redis-server redis.conf

启动从节点

redis-server redis.conf

验证复制是否成功

redis-cli -h 127.0.0.1 -p 6379
auth foobared
SET key1 value1
redis-cli -h 127.0.0.1 -p 6380
auth foobared
GET key1

主从节点的角色转换

  • 当主节点发生故障或下线时,可以将从节点提升为新的主节点,实现故障转移。
  • 通过在从节点上执行slaveof no one命令,可以将从节点转变为主节点。
  • 在角色转换后,原来的主节点恢复正常后可以作为新的从节点连接到新的主节点上。

同步复制&异步复制

Redis的主从复制分为同步复制(synchronous replication)和异步复制(asynchronous replication)。

  • 同步复制:在进行写操作时,主节点会将数据先保存到自身的内存中,然后再通过网络发送给所有从节点,并等待所有从节点都确认接收成功才返回结果。这样可以保证数据的完全一致性,但由于需要等待所有从节点的确认,导致了写入速度变慢。

  • 异步复制:与同步复制不同,异步复制只需要主节点将数据发送给从节点,而无需等待从节点的确认。因此,写入速度更高效,但也容易造成数据不一致的情况。当主节点故障切换或者重新连接上线时,如果没有及时处理未被从节点确认的数据,就会导致部分数据丢失。

# 开启同步复制模式
replica-serve-stale-data yes
repl-diskless-sync no
repl-disable-tcp-nodelay no
repl-backlog-size 1mb
repl-timeout 60
# 开启异步复制模式
replica-serve-stale-data yes
repl-diskless-sync yes
repl-disable-tcp-nodelay yes
repl-backlog-size 1mb
repl-timeout 60

replica-serve-stale-data参数用来控制从节点在主节点断开连接期间是否能提供服务;
repl-diskless-sync参数用来指定是否使用磁盘持久化机制进行同步复制;
repl-disable-tcp-nodelay参数用来关闭TCP_NODELAY选项,加快传输速度;
repl-backlog-size参数用来限制从节点的复制积压大小;
repl-timeout参数用来设置超时时间。

Wait指令

Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一致性(不严格)。wait 指令是Redis3.0版本以后才出现的。

> set key value
oK
>wait 1 0
(integer)1

wait提供两个参数,第一个参数是从库的数量N,第二个参数是时间t,以毫秒为单位。它表示等待wait 指令之前的所有写操作同步到N个从库(也就是确保N个从库的同步没有滞后),最多等待时间t。
如果时间t=0,表示无限等待直到N个从库同步完达成一致。假设此时出现了网络分区,wait指令第二个参数时间t=0,主从同步无法继续进行, wait指令会永远阻塞,Redis 服务器将丧失可用性。

Redis 集群模式

Redis 集群模式是通过将多个 Redis 实例组成一个集群来提供高可用性和性能的解决方案。Redis 集群使用的是分布式哈希槽算法,通过对 key 进行哈希映射到不同的 Redis 实例上,实现了数据的分布式存储和负载均衡。

集群搭建

修改主机的 Redis 配置文件 redis.conf,在文件末尾添加以下内容:

# 启动集群模式
cluster-enabled yes
cluster-config-file nodes.conf
# 集群节点的超时时限,节点发送对其它节点的PING命令的时候如果超过这个时间还没有响应则标记另外一个为故障状态 默认:15000毫秒
cluster-node-timeout 5000

这段配置用于启用集群模式,并指定了存储集群信息的文件和节点失效的超时时间。

修改从机的 Redis 配置文件 redis.conf,在文件末尾添加以下内容:

slaveof <master-ip> <master-port>

将 < master-ip> 和 < master-port> 替换为主机的 IP 地址和端口号,这段配置用于指定从机的主机。

分别在每台虚拟机上启动 Redis 实例:

redis-server /path/to/redis.conf

然后,在主机的某个 Redis 节点上执行以下命令,以创建 Redis 集群:

redis-cli --cluster create <node1-ip>:<node1-port> <node2-ip>:<node2-port> <node3-ip>:<node3-port> --cluster-replicas 1

将 < nodeX-ip> 和 < nodeX-port> 替换为各个节点的 IP 地址和端口号,–cluster-replicas 参数表示每个主节点对应的从节点数量。执行完命令后,Redis 集群就创建完成了。
在创建时会询问是否设置成列出来的配置,输入yes回车即可,槽位分配和主从关系都是由Cluster计算出来的,后续可以通过命令调整

要使用 Redis 集群,我们需要使用 redis-cli 或者其他 Redis 客户端进行连接。连接方式与单机 Redis 相同,只不过需要使用 -c 参数来启用 Redis 集群模式,例如:

redis-cli -c -h <cluster-ip> -p <cluster-port>

Redis 集群模式核心原理

在 Redis 集群中,数据被分为 16384 个哈希槽,每个哈希槽对应一个 Redis 实例。当我们向 Redis 写入数据时,Redis 会对 key 进行哈希,然后将哈希值与 16383 取模得到的值就是要存储的哈希槽。根据哈希槽与 Redis 实例的映射关系,Redis 就知道了要将数据存储在哪个实例上。

当我们从 Redis 读取数据时,Redis 需要先确定要查询的 key 对应的哈希槽,然后才能知道要从哪个实例中读取数据。如果集群中的某一个节点失效了,那么相应的哈希槽就无法提供服务了。此时,Redis 会将失效的节点上的哈希槽迁移到其他节点上,保证数据的高可用性。

Redis 中使用哈希槽算法来实现分布式存储。哈希槽算法的基本思想是将所有数据按照 key 进行哈希计算,得到一个哈希值,然后将这个哈希值对槽总数取模,最终将数据存储到对应的哈希槽中。

集群搭建原博客

集群的相关操作命令示例

集群中的槽位迁移与重新分配

哨兵搭建

哨兵在主从复制的基础上,哨兵引入了主节点的自动故障转移。

分别在之前安装的三个redis配置文件下创建sentinel.conf配置文件

port 26379 #哨兵端口号
daemonize yes #守护线程
logfile "./sentinel.log"
dir /tmp
#master-name redisMaster #哨兵名称
#sentinel monitor [master-group-name] [ip] [port] [quorum]
#sentinel monitor 自定义的主机哨兵名字 主机ip 主机端口 判定主机宕机的哨兵数
sentinel monitor redisMaster 127.0.0.1 6379 2
#最多可以有多少个从Redis实例同时对新的master进行同步
sentinel parallel-syncs redisMaster 1 
#默认5000ms没有响应 认为主机宕机
sentinel down-after-milliseconds redisMaster 5000
# 转移新的master超时时间ms,超时也会继续转移,但是不会遵循parallel-syncs的配置
sentinel failover-timeout redisMaster 15000
#如果有密码 设置master和slaves验证密码,主机和从机密码必须一致
sentinel auth-pass redisMaster 123456

启动方式 :

redis-server redis.conf 启动redis (集群对应启动多个)
redis-server sentinel.conf --sentinel 启动哨兵(启动各自对应的)
redis-cli -p port 启动客户端(port对应redis端口号)

Windows 使用 redis-server.exe ./sentinel.conf --sentinel 启动三个哨兵

Windows哨兵搭建示例

主从及哨兵搭建示例

同步相关知识点

原文

主从同步机制详解-知乎

节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。
运行ID的主要作用是用来唯一识别Redis节点。从节点保存主节点的运行ID识别自己正在复制的是哪个主节点。当运行ID变化后从节点将做全量复制。
Redis关闭再启动后,运行ID会随之改变,可以使用debug reload命令重新加载RDB并保持运行ID不变,从而有效避免不必要的全量复制。debug reload命令会阻塞当前Redis节点主线程,阻塞期间会生成本地RDB快照并清空数据之后再加载RDB文件。因此对于大数据量的主节点和无法容忍阻塞的应用场景,谨慎使用。

psync命令

从节点使用psync命令完成部分复制和全量复制功能,命令格式:

psync {runId} {offset}

参数含义如下:

  • runId:从节点所复制主节点的运行id。
  • offset:当前从节点已复制的数据偏移量。

psync命令运行流程:

1)从节点(slave)发送psync命令给主节点,参数runId是当前从节点保存的主节点运行ID,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1。

2)主节点(master)根据psync参数和自身数据情况决定响应结果:
如果回复+FULLRESYNC{runId}{offset},那么从节点将触发全量复制流程。
如果回复+CONTINUE,从节点将触发部分复制流程。
如果回复+ERR,说明主节点版本低于Redis2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。

同步过程

在从节点执行slaveof命令后,复制过程便开始运作。复制的完整流程如下所示:

1)保存主节点(master)信息。执行slaveof后从节点只保存主节点的地址信息便直接返回,这时建立复制流程还没有开始。

2)从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。从节点会建立一个socket套接字,专门用于接受主节点发送的复制命令。

3)发送ping命令。连接建立成功后从节点发送ping请求进行首次通信,ping请求主要目的如下:

  • 检测主从之间网络套接字是否可用。
  • 检测主节点当前是否可接受处理命令。
  • 如果发送ping命令后,从节点没有收到主节点的pong回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连。

4)权限验证。如果主节点设置了requirepass参数,则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。

5)同步数据集。主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。Redis在2.8版本以后采用新复制命令psync进行数据同步,原来的sync命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量同步和部分同步。

6)命令持续复制。当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

同步类型

Redis在2.8及以上版本使用psync命令完成主从数据同步。

psync命令运行需要以下组件支持:

  • 主从节点各自复制偏移量
  • 主节点复制积压缓冲区
  • 主节点运行id。
    runID 就是用来标识各个服务器的唯一性的,这样在同步的时候就不会发生错乱了。

复制偏移量
主从服务器分别会维护一个复制偏移量:参与复制的主从节点都会维护自身复制偏移量。主节点(master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在info relication中的master_repl_offset指标中。
从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。
主服务器每次向从服务器 传播N个字节的数据后 ,就会将自己的偏移量加N;
从服务器每次收到主服务器发送过来的N个字节后,就会将自己的偏移量加N。

通过对比主从服务器的复制偏移量,可以知道主从是否一致:
如果主从复制偏移量一致:说明主从服务器状态一致;
如果主从复制偏移量不同:说明主从服务器不一致。不一致就需要进行部分重同步或者完整重同步了。

复制积压缓冲区
复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区。
当主服务器传播命令时,它不仅会将命令发送给从服务器,还会写入复制积压缓冲区。因此,复制积压缓冲区里
保存了最近一部分传播的命令 。并且复制复制积压缓冲区 还保存了每个字节内容对应的偏移量 。

由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息保存在主节点的info replication中:

127.0.0.1:6379> info replication
# Replicationrole:master...
repl_backlog_active:1 //  开启复制缓冲区
repl_backlog_size:1048576 //  缓冲区最大长度
repl_backlog_first_byte_offset:7479 //  起始偏移量,计算当前缓冲区可用范围
repl_backlog_histlen:1048576 //  已保存数据的有效长度。

当从服务器断线重连之后,从服务器会通过PSYNC带上自己的复制偏移量offset,主服务器收到之后会和自己的复
制偏移量比较,并执行不同的操作:

  • 如果 offset偏移量之后(offset+1开始)的数据仍然在复制积压缓冲区中 ,那么主服务器会对从服务器进行部分重同步操作 ;
  • 如果 offset偏移量之后的数据不在复制积压缓冲区中 ,那么会进行 完整重同步操作 。

增量同步/部分复制

Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。

因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer
中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis 的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉 了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 —— 快照同步。

部分复制使用 psync {runId} {offset} 命令实现。当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。

部分复制的流程:

1)当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接。

2)主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。

3)当主从节点网络恢复后,从节点会再次连上主节点。

4)当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。

5)主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。

6)主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

快照同步/全量复制

快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。
从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。

在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。 所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。

全量复制的完整运行流程:

1)从发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1。

2)主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应。

3)从节点接收主节点的响应数据保存运行ID和偏移量offset。

4)主节点执行bgsave保存RDB文件到本地。

5)主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件。对于数据量较大的主节点,RDB文件从创建到传输完毕消耗的总时间超过repl-timeout所配置的值(默认60秒),从节点将放弃接受RDB文件并清理已经下载的临时文件,导致全量复制失败。Redis支持无盘复制,生成的RDB文件不保存到硬盘而是直接通过网络发送给从节点,通过repl-diskless-sync参数控制,默认关闭。无盘复制适用于主节点所在机器磁盘性能较差但网络带宽较充裕的场景。

6)对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为client-output-buffer-limit slave256MB64MB60,如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。

7)从节点接收完主节点传送来的全部数据后会清空自身旧数据。

8)从节点清空数据后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时。对于线上做读写分离的场景,从节点也负责响应读命令。如果此时从节点正出于全量复制阶段或者复制中断,那么从节点在响应读命令可能拿到过期或错误的数据。对于这种场景,Redis复制提供了slave-serve-stale-data参数,默认开启状态。如果开启则从节点依然响应所有命令。对于无法容忍不一致的应用场景可以设置no来关闭命令执行,此时从节点除了info和slaveof命令之外所有的命令只返回“SYNC with master in progress”信息。

9)从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。

全量复制时间开销主要包括:

  • 主节点bgsave时间。
  • RDB文件网络传输时间
  • 从节点清空数据时间
  • 从节点加载RDB的时间
  • 可能的AOF重写时间

Redis同步过程

无盘复制

主节点在进行快照同步时,会进行很重的文件 IO 操作,特别是对于非 SSD 磁盘存储时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。
所以从 Redis 2.8.18 版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。

心跳

主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。

主从心跳判断机制:

1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点连接状态为flags=S。

2)主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period控制发送频率。

3)从节点在主线程中每隔1秒发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:

  • 实时监测主从节点网络状态
  • 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
  • 实现保证从节点的数量和延迟性功能,通过min-slaves-to-write、min-slaves-max-lag参数配置定义。

主节点根据replconf命令判断从节点超时时间,体现在info replication统计中的lag信息中,lag表示与从节点最后一次通信延迟的秒数,正常延迟应该在0和1之间。如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。

同步过程中数据一致性问题

在redis主从复制过程中,由于网络异常、文件写入异常等原因,从节点无法与主节点或其他从节点进行同步,进而会导致数据不一致。

造成影响 :

  • 延迟
    1. 服务发起了一个写请求,Master写入完成,开始向Slave同步。
    2. 服务又发起了一个读请求,此时同步未完成,读到Slave的一个不一致的脏数据
    3. 数据库主从同步最后才完成。
  • 宕机

任何数据冗余,必将引发一致性问题。

解决方案

为了避免这种情况的发生,我们可以通过增加从节点的数量,使得从节点的数据保持一致。
当redis集群中某一台节点宕机时,查看是否还有剩余宕机的情况。为了保证数据的一致性,我们需要对redis集群中一台出现的宕机节点进行修复,该节点恢复正常服务。

redis集群数据不一致:为了保证数据的一致性,我们需要采用数据同步机制,确保在主节点宕机或者出现故障时,从节点能够自动切换到备用节点上,保证数据的可用性。

可以通过配置主节点的min-slaves-to-write参数,确保在至少有一定数量的从节点在线时,才执行写操作。

在Redis的配置文件中,可以通过repl-diskless-sync-delay选项来设置主从同步的间隔时间(单位是秒)。如果设置为0,则表示数据将实时同步,没有延迟。如果设置为正值,则表示数据会在指定的秒数后写入到磁盘,然后同步给从服务器。

# 在redis.conf中设置
repl-diskless-sync-delay 5

Redis 集群方案

  • twemproxy,大概概念是,它类似于一个代理方式, 使用时在本需要连接 redis 的地方改为连接 twemproxy, 它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。
    缺点: twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。

  • codis, 目前用的最多的集群方案, 基本和 twemproxy 一致的效果, 但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点。

  • Redis cluster3.0 自带的集群, 特点在于他的分布式算法不是一致性 hash, 而是 hash槽的概念, 以及自身支持节点设置从节点,具体看官方文档介绍。

SpringBoot中主从分离

读写分离简单实现:

使用lettuce实现:

POM:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--springboot中的redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- lettuce pool 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- 使用jackson作为redis数据序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.4</version>
        </dependency>
		<!-- SpringBoot测试包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

添加配置文件application.yml

spring:
  #redis配置信息
  redis:
    sentinel:
      # 主节点名称 在哨兵配置文件中配置的
      master: mymaster
      # 哨兵ip地址和端口 有多个哨兵就填多个,只填一个其实也能使用
      nodes:
        - 10.0.20.13:26379
        - 10.0.20.13:26380
        - 10.0.20.13:26381
    ## Redis数据库索引(默认为0)
    database: 0
    ## Redis服务连接密码(默认为空)
    password: 123456
    ## 连接超时时间(毫秒)
    timeout: 5000
    lettuce:
      pool:
        ## 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        ## 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        ## 连接池中的最大空闲连接
        max-idle: 8
        ## 连接池中的最小空闲连接
        min-idle: 1
      shutdown-timeout: 1000ms

# 打印lettuce debug日志,方便查看读写分离效果
logging:
  pattern:
    console: '%date{yyyy-MM-dd HH:mm:ss.SSS} | %highlight(%5level) [%green(%16.16thread)] %clr(%-50.50logger{49}){cyan} %4line -| %highlight(%msg%n)'
  level:
    root: info
    io.lettuce.core: debug
    org.springframework.data.redis: debug

编写配置文件

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.ReadFrom;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.HashSet;

@Configuration
public class RedisConfig{

    /**
     * 配置读写分离
     */
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties) {
        // 配置哨兵节点以及主节点
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(
                redisProperties.getSentinel().getMaster(), new HashSet<>(redisProperties.getSentinel().getNodes())
        );

        // 配置读写分离
        LettucePoolingClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
                .poolConfig(getPoolConfig(redisProperties.getLettuce().getPool()))
                // ReadFrom.REPLICA_PREFERRED 优先读取从节点,如果从节点不可用,则读取主节点
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .build();

        return new LettuceConnectionFactory(redisSentinelConfiguration, lettuceClientConfiguration);
    }
    private GenericObjectPoolConfig<?> getPoolConfig(RedisProperties.Pool properties) {
        GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(properties.getMaxActive());
        config.setMaxIdle(properties.getMaxIdle());
        config.setMinIdle(properties.getMinIdle());
        if (properties.getTimeBetweenEvictionRuns() != null) {
            config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns().toMillis());
        }
        if (properties.getMaxWait() != null) {
            config.setMaxWaitMillis(properties.getMaxWait().toMillis());
        }
        return config;
    }

    /**
     * retemplate相关配置,配置自定义序列化规则为jackson
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();
        return template;
    }
}

编写测试类测试是否连接成功以及读写分离效果:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes =  LettuceApplication.class)
public class LettuceTest {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Test
    public void t1(){
        String key = "key1";
        System.out.println("插入数据到redis");
        redisTemplate.opsForValue().set(key,"value1");
        Object value = redisTemplate.opsForValue().get(key);
        System.out.println("从redis中获取到值为 "+value);
        Boolean delete = redisTemplate.delete(key);
        System.out.println("删除redis中值 "+delete);
    }
}

lettuce基础配置示例

SpringBoot集成Redis主从架构实现读写分离(哨兵模式)

使用Lettuce实现动态切换

redis读写分离之lettuce配置示例

Redis的过期策略以及内存淘汰机制

Redis采用的是定期删除+惰性删除策略。

定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是惰性删除派上用场,也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了,如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高,那么就应该采用内存淘汰机制。

在redis.conf中有一行配置

# 设置最大内存,比如100mb
maxmemory 100mb
 
# 设置淘汰策略为allkeys-lru
maxmemory-policy allkeys-lru

该配置就是配内存淘汰策略,共有以下六种:

  1. noeviction:不进行淘汰,当内存不足以容纳新写入数据时,新写入操作会报错。

  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(移除最不常使用的键,荐使用)

  3. volatile-lru:尝试回收最少使用的键, 但仅限于在过期集合的键(只对设置了过期时间的键进行LRU淘汰),使得新添加的数据有空间存放。

  4. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

  5. volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键(随机移除设置了过期时间的键)。

  6. volatile-ttl:回收在过期集合的键,并且优先回收存活时间TTL较短的键,使得新添加的数据有空间存放(移除即将过期的键)。

美团面试题:怎么保证redis中的key都是热点数据;答案:使用allkeys-lru的过期策略。

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值