Redis主从复制,哨兵机制、Redis集群

如果 Redis 的读写请求量很大,那么单个实例很有可能承担不了这么大的请求量,如何提高 Redis 的性能呢?你也许已经想到了,可以部署多个副本节点,业务采用读写分离的方式,把读请求分担到多个副本节点上,提高访问性能。要实现读写分离,就必须部署多个副本,每个副本需要实时同步主节点的数据。

这篇文章我们就来介绍一下 Redis 主从复制流程和原理,以及在复制过程中有可能产生的各种问题。
主要围绕着三个方面
Redis 主从复制、哨兵机制、Redis 集群

# 目标

  • 理解什么是 Redis 的主从复制、哨兵机制、官方集群方案 Cluster
  • 会搭建 Redis 主从、哨兵、Redis Cluster
  • 理解 Redis 主从存在的问题
  • 理解 Redis 主从的实现原理(全量同步,增量同步,部分同步的概念)
  • 理解 Redis 哨兵机制的实现原理
  • 理解 Cluster 的实现原理

# Redis 主从复制

生产上永远不可能用单机,所有的节点,包括应用节点,中间件节点,数据节点等都要保证高可用,最基本的要求。
主从必然存在延迟问题 ,Redis 按照分布式集群来讲,就不是强一致性的 CAP 结构,只保证了高可用 AP,没有保证强一致性 CP,所以必然存在延迟。
CAP:
一致性 (Consistency)
可用性 (Availability)
分区容忍性 (Partition tolerance)
如果业务需要主从机制必须强一致,那就要考虑适不适合选择 Redis。

# 什么是主从复制

如果单机情况下,机器重启,内存数据丢失,如何保证数据的高可用呢?持久化方案
如果单机,这台机器的硬件坏了,例如硬盘坏了,或者访问压力太大,服务器崩溃了,如何保证数据的高可用呢?主从复制
Redis 的主从机制:主负责读写,从一般只读不能写(客户端)。

和 Mysql 主从复制的原因一样,Redis 虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis 支持主从复制,Redis 的主从结构可以采用一主多从或者级联结构,Redis 主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。 如下图:

img

主从复制特点:

  • 1)采用异步复制;
  • 2)一个主 redis 可以含有多个从 redis;
  • 3)每个从 redis 可以接收来自其他从 redis 服务器的连接;
  • 4)主从复制对于主 redis 服务器来说是非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主 redis 仍然可以处理外界的访问请求;
  • 5)主从复制对于从 redis 服务器来说也是非阻塞的,这意味着,即使从 redis 在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从 redis 返回的是以前老的数据,如果你不想这样,那么在启动 redis 时,可以在配置文件中进行设置,那么从 redis 在复制同步过程中来自外界的查询请求都会返回错误给客户端;(虽然说主从复制过程中对于从 redis 是非阻塞的,但是当从 redis 从主 redis 同步过来最新的数据后还需要将新数据加载到内存中,在加载到内存的过程中是阻塞的,在这段时间内的请求将会被阻,对于大数据集,加载到内存的时间也是比较多的);
  • 6)主从复制提高了 redis 服务的扩展性,避免单个 redis 服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;
  • 7)为了避免主 redis 服务器写磁盘压力带来的开销,可以配置让主 redis 不在将数据持久化到磁盘,而是通过连接让一个配置的从 redis 服务器及时的将相关数据持久化到磁盘,不过这样会存在一个问题,就是主 redis 服务器一旦重启,因为主 redis 服务器数据为空,这时候通过主从同步可能导致从 redis 服务器上的数据也被清空;

# 主从配置

主 Redis 配置

  • 无需特殊配置

从 Redis 配置

  • 修改从服务器上的 redis.conf 文件:```slaveof <masterip> <masterport>
    表示当前【从服务器】对应的【主服务器】的 IP 是 192.168.10.135,端口是 6379。
    slaveof 192.168.10.135 6379 (5.0 之前的配置)
    replicaof 192.168.19.135 6379 (5.0 之后的配置)

# 实现原理

# 一。复制整体流程

复制整体步骤如下:

  • 从节点执行 slaveof 命令
  • 从节点只是保存了 slaveof 命令中主节点的信息,并没有立即发起复制
  • 从节点内部的定时任务发现有主节点的信息,开始使用 socket 连接主节点
  • 连接建立成功后,发送 ping 命令,希望得到 pong 命令响应,否则会进行重连
  • 如果主节点设置了权限,那么就需要进行权限验证;如果验证失败,复制终止。
  • 权限验证通过后,进行数据同步,这是耗时最长的操作,主节点将把所有的数据全部发送给从节点。( 全量同步
  • 当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性增量同步 )。

img

# 二。数据间的同步

上面说的整体复制过程,其中有一个步骤是 “同步数据集”,这个就是现在讲的‘数据间的同步’。

# redis 同步有 2 个命令:

syncpsync ,前者是 redis 2.8 之前的同步命令,后者是 redis 2.8 为了优化 sync 新设计的命令。我们会重点关注 2.8 的 psync 命令。

# psync 命令需要 3 个组件支持:

主从节点各自复制偏移量
主节点 复制积压缓冲区
主节点运行 ID

# 主从节点各自复制偏移量:

  • 参与复制的主从节点都会维护自身的复制偏移量。
  • 主节点在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在 info replication 中的 masterreploffset 指标中。
  • 从节点每秒钟上报自身的的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。
  • 从节点在接收到主节点发送的命令后,也会累加自身的偏移量,统计信息在 info replication 中。
  • 通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。

# 主节点复制积压缓冲区:

  • 复制积压缓冲区是一个保存在主节点的一个固定长度的先进先出的队列。默认大小 1MB。
  • 这个队列在 slave 连接时创建。这时主节点响应写命令时,不但会把命令发送给从节点,也会写入复制缓冲区。
  • 他的作用就是 用于部分复制复制命令丢失的数据补救 。通过 info replication 可以看到相关信息。

# 主节点运行 ID:

  • 每个 redis 启动的时候,都会生成一个 40 位的运行 ID。

  • 运行 ID 的主要作用是用来识别 Redis 节点。如果使用 ip+port 的方式,那么如果主节点重启修改了 RDB/AOF 数据,从节点再基于偏移量进行复制将是不安全的。所以,当运行 id 变化后,从节点将进行全量复制。也就是说,redis 重启后,默认从节点会进行全量复制。

  • 如何在重启时不改变运行 ID 呢?

    • 可以通过 debug reload 命令重新加载 RDB 并保持运行 ID 不变。从而有效的避免不必要的全量复制。
    • 他的缺点则是:debug reload 命令会阻塞当前 Redis 节点主线程,因此对于大数据量的主节点或者无法容忍阻塞的节点,需要谨慎使用。一般通过故障转移机制可以解决这个问题。

# psync 命令的使用方式:

命令格式为 psync {runId}

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

# psync 执行流程:

img

  • 1、客户端向服务器发送 SLAVEOF 命令,让当前服务器成为 Slave;
  • 2、当前服务器根据自己是否保存 Master runid 来判断是否是第一次复制,如果是第一次同步则跳转到 3,否则跳转到 4;
  • 3、向 Master 发送 PSYNC ? -1 命令来进行完整同步;
  • 4、向 Master 发送 PSYNC runid offset;
  • 5、Master 接收到 PSYNC 命令后 首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;
  • 6、 如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步

流程说明:从节点发送 psync 命令给主节点,runId 就是目标主节点的 ID,如果没有默认为 -1,offset 是从节点保存的复制偏移量,如果是第一次复制则为 -1.

主节点会根据 runid 和 offset 决定返回结果:

  • 如果回复 +FULLRESYNC {runId} {offset} ,那么从节点将触发全量复制流程。
  • 如果回复 +CONTINUE,从节点将触发部分复制。
  • 如果回复 +ERR,说明主节点不支持 2.8 的 psync 命令,将使用 sync 执行全量复制。

# 三。全量复制

全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的的阶段。触发全量复制的命令是 sync 和 psync。之前说过,这两个命令的分水岭版本是 2.8, redis 2.8 之前使用 sync 只能执行全量同步,2.8 之后同时支持全量同步和部分同步

流程如下:

img

介绍一下上图步骤:

  • 发送 psync 命令(spync ? -1)
  • 主节点根据命令返回 FULLRESYNC
  • 从节点记录主节点 ID 和 offset
  • 主节点 bgsave 并保存 RDB 到本地
  • 主节点发送 RBD 文件到从节点
  • 从节点收到 RDB 文件并加载到内存中
  • 主节点在从节点接受数据的期间,将新数据保存到 “ 复制积压缓冲区 ”,当从节点加载 RDB 完毕,再发送过去。(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)
  • 从节点清空数据后加载 RDB 文件,如果 RDB 文件很大,这一步操作仍然耗时,如果此时客户端访问,将导致数据不一致,可以使用配置 slave-server-stale-data 关闭.
  • 从节点成功加载完 RBD 后,如果开启了 AOF,会立刻做 bgrewriteaof(AOF 重写)。
以上加粗的部分是整个全量同步耗时的地方。

注意:
如过 RDB 文件大于 6GB,并且是千兆网卡,Redis 的默认超时机制(60 秒),会导致全量复制失败。可以通过调大 repl-timeout 参数来解决此问题。

Redis 虽然支持无盘复制(不使用磁盘作为中间存储),即直接通过网络发送给从节点,但功能不是很完善,生产环境慎用。

# 四。部分复制

当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将 复制缓冲区 的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。
前提时offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,否则还是全量复制

步骤如下:

img

  • 当从节点出现网络中断,超过了 repl-timeout 时间,主节点就会中断复制连接。
  • 主节点会将请求的数据写入到 “复制积压缓冲区”,默认 1MB。
  • 当从节点恢复,重新连接上主节点,从节点会将 offset 和主节点 id 发送到主节点
  • 主节点校验后,如果偏移量数之后的数据在缓冲区中并且没有超过复制积压缓冲区大小,就发送 continue 响应 —— 表示可以进行部分复制
  • 主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态。

# 五。增量复制

Redis 增量复制是指 Slave 初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

# 六。心跳

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

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

主节点默认每隔 10 秒对从节点发送 ping 命令,可修改配置 repl-ping-slave-period 控制发送频率。

从节点在主线程每隔一秒发送 replconf ack {offset} 命令,给主节点上报自身当前的复制偏移量。

主节点收到 replconf 信息后,判断从节点超时时间,如果超过 repl-timeout 60 秒,则判断节点下线。

img

注意:为了降低主从延迟,一般把 redis 主从节点部署在相同的机房 / 同城机房,避免网络延迟带来的网络分区造成的心跳中断等情况。

# 七。异步复制

主节点不但负责数据读写,还负责把写命令同步给从节点,写命令的发送过程是异步完成,也就是说主节点处理完写命令后立即返回客户端,并不等待从节点复制完成。

异步复制的步骤很简单,如下:

  • 主节点接受处理命令
  • 主节点处理完后返回响应结果
  • 对于修改命令,异步发送给从节点,从节点在主线程中执行复制的命令。

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-macPVU1w-1591783378461)(./1591783131281.png)]

# 原理总结

可以看出,RDB 数据之间的同步非常耗时。所以,Redis 在 2.8 版本退出了类似增量复制的 psync 命令,当 Redis 主从直接发生了网络中断,不会进行全量复制,而是将数据放到缓冲区(默认 1MB)里,在通过主从之间各自维护复制 offset 来判断缓存区的数据是否溢出,如果没有溢出,只需要发送缓冲区数据即可,成本很小,反之,则要进行全量复制,因此,控制缓冲区大小非常的重要。

# Redis 哨兵机制

如果主节点挂了,从机是不能负责写命令的处理,必然不可取,就需要结合 Redis 哨兵。
Redis 主从复制的缺点:没有办法对 master 进行动态选举,需要使用 Sentinel 机制完成动态选举。

# 简介

  • Sentinel (哨兵) 进程是用于监控 Redis 集群中 Master 主服务器工作的状态(主服务器本身知道从服务器信息,相当于哨兵能够监控整个集群的信息)
  • 在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可用( HA )
  • 其已经被集成在 redis2.6+ 的版本中, Redis 的哨兵模式到了 2.8 版本之后就稳定了下来。

# 哨兵进程的作用

  • 监控 (Monitoring): 哨兵 (sentinel) 会不断地检查你的 Master 和 Slave 是否运作正常。
  • 提醒 (Notification): 当被监控的某个 Redis 节点出现问题时,哨兵 (sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移 (Automatic failover):当一个 Master 不能正常工作时,哨兵 (sentinel) 会开始一次自动故障迁移操作

# 故障判定原理分析

  1. 每个 Sentinel (哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器, Slave 从服务器以及其他 Sentinel (哨兵)进程发送一个 PING 命令。
  2. 如果一个实例( instance )距离最后一次有效回复 PING 命令的时间超过 down-aftermilliseconds 选项所指定的值, 则这实例会被 Sentinel (哨兵)进程标记为 主观下线( SDOWN )
  3. 如果一个 Master 主服务器被标记为 主观下线( SDOWN ) ,则正在监视这个 Master 主服务器的所有 Sentinel (哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。
  4. 当有足够数量的 Sentinel (哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了 主观下线状态( SDOWN ) , 则 Master 主服务器会被标记为 客观下线( ODOWN )
  5. 在一般情况下, 每个 Sentinel (哨兵)进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、 Slave 从服务器发送 INFO 命令。
  6. 当 Master 主服务器被 Sentinel (哨兵)进程标记为 客观下线( ODOWN ) 时, Sentinel (哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  7. 若没有足够数量的 Sentinel (哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel (哨兵)进程发送 PING 命令返回有效回复, Master 主服务器的主观下线状态就会被移除。

img

图解:
1. 哨兵 Sentinel A/B/C 通过配置监控主节点 Master
2. 哨兵彼此之间也能互相通信(用来投票等)
3. 哨兵通过主机的主从机制,可同时拥有了从机的信息
4. 客户端正常访问直接访问主节点,感知不到哨兵的存在
5. 如果 master 挂了,如果哨兵 A 先检测到(通过 PING 命令,超过 down-aftermilliseconds 指定的时间值还没有回应则认为失效),则哨兵 A 就会对 master 标记为 SDOWN,主观下线
6. 同时监控该主服务器的所有哨兵会加快频率发送 ping 命令,每个都会确认是否是主观下线状态
7. 如果有足够数量的哨兵进程( 通过配置quorumc参数,默认是1,即一个哨兵就有生杀大权,也不能超过所有哨兵的数量,建议大于一半 )确认了 Master 进入了主观下线状态,就会将 master 标记为 ODOWN:客观下线
8. 如果确认客观下线后,就会进行自动故障迁移

# 自动故障迁移

  • 它会将失效 Master 的其中一个 Slave(随机选) 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;
  • 当客户端试图连接失效的 Master 时,集群也会向客户端返回新 Master 的地址,使得集群可以使用现在的 Master 替换失效 Master 。(需要 Redis Sentinel 客户端,详细了解参考哨兵的客户端连接
  • Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即, Master 主服务器的 redis.conf 配置文件中会多一行 slaveof 的配置, sentinel.conf 的监控目标会随之调换。

# 案例演示

哨兵本身也是一个redis节点 ,也是一个完整的程序,也有自己端口等,只是命令不一样,可以和 Redis 节点放一块,也可以单独放到一个服务器

img

  • 启动命令 redis-sentinel
  • 对应的配置文件叫 sentinel.conf

# 核心配置

sentinel.conf :

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <master ip> <master port> <quorum>
sentinel monitor mymaster 192.168.10.133 6379 1

# 其他配置项说明

sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,
# 这个数字越小,完成failover所需的时间就越长,
# 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给客户端:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

# 启动哨兵服务

通过 redis-sentinel 启动哨兵服务

./redis-sentinel sentinel.conf

# Redis 集群

redis3.0 以后推出的 redis cluster 集群方案,redis cluster 集群保证了高可用、高性能、高可扩展性。

# Redis 的集群策略

  • 推特:twemproxy
  • 豌豆荚:codis
  • 官方:redis cluster

# Redis-cluster 架构图

img

特点:

  • 去中心化 :没有 leader 节点
  • 弊端,节点信息量大:每个节点都拥有全部集群的信息,每个节点都需要配置集群中其他节点的信息(不需要手动配置,自动的)

架构细节:

  • (1) 所有的 redis 节点彼此互联 ( PING-PONG机制 ), 内部使用二进制协议优化传输速度和带宽.
  • (2) 节点的 fail 是通过集群中超过半数的节点检测失效时才生效.
  • (3) 客户端与 redis 节点直连,不需要中间 proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • (4) redis-cluster 把所有的物理节点映射到 [0-16383] slot 上,cluster 负责维护 node<->slot<->value

# 数据分配规则:

crc16 算法 + 哈希槽

img

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点

  • 示例如下:

img

# Redis-cluster 投票:容错

img

(1) 节点失效判断:集群中所有 master 参与投票,如果半数以上 master 节点与其中一个 master 节点通信超过 (cluster-node-timeout) , 认为该 master 节点挂掉.

(2) 集群失效判断:什么时候整个集群不可用 (cluster_state:fail)?

  • 如果集群任意 master 挂掉,且当前 master 没有 slave,则集群进入 fail 状态。也可以理解成集群的 [0-16383] slot 映射不完全时进入 fail 状态。
  • 如果集群超过半数以上 master 挂掉,无论是否有 slave,集群进入 fail 状态。

# 安装 RedisCluster

Redis 集群最少需要三台主服务器,三台从服务器。
端口号分别为:7001~7006

  • 第一步:创建 7001 实例,并编辑 redis.conf 文件,修改 port 为 7001。
    注意:创建实例,即拷贝单机版安装时,生成的 bin 目录,为 7001 目录。

img

  • 第二步:修改 redis.conf 配置文件,打开 Cluster-enable yes

img

  • 第三步:复制 7001,创建 7002~7006 实例,注意端口修改。

  • 第四步:启动所有的实例

    • 可以使用脚本批量启动:
      vim start-cluster.sh
cd 7001
        ./redis-server redis.conf
        cd ..
        cd 7002
        ./redis-server redis.conf
        cd ..
        cd 7003
        ./redis-server redis.conf
        cd ..
        cd 7004
        ./redis-server redis.conf
        cd ..
        cd 7005
        ./redis-server redis.conf
        cd ..
        cd 7006
        ./redis-server redis.conf
        cd .. 
        ```
放到和节点同级目录  
![](https://img-blog.csdnimg.cn/20200610215958443.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTk0NzM3OA==,size_16,color_FFFFFF,t_70)  
 chmod u+x start-cluster.sh (授权可执行)
*   第五步:所有节点启动后创建Redis集群```
    ./redis-cli --cluster create 192.168.10.135:7001 192.168.10.135:7002
    192.168.10.135:7003 192.168.10.135:7004 192.168.10.135:7005
    192.168.10.135:7006 --cluster-replicas 1
    # --cluster-replicas:代表每个主节点有几个从节点
    >>> Creating cluster
    Connecting to node 192.168.10.133:7001: OK
    Connecting to node 192.168.10.133:7002: OK
    Connecting to node 192.168.10.133:7003: OK
    Connecting to node 192.168.10.133:7004: OK
    Connecting to node 192.168.10.133:7005: OK
    Connecting to node 192.168.10.133:7006: OK
    >>> Performing hash slots allocation on 6 nodes...
    Using 3 masters:
    192.168.10.133:7001
    192.168.10.133:7002
    192.168.10.133:7003
    Adding replica 192.168.10.133:7004 to 192.168.10.133:7001
    Adding replica 192.168.10.133:7005 to 192.168.10.133:7002
    Adding replica 192.168.10.133:7006 to 192.168.10.133:7003
    M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.10.133:7001
    slots:0-5460 (5461 slots) master
    M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.10.133:7002
    slots:5461-10922 (5462 slots) master
    M: 93f73d2424a796657948c660928b71edd3db881f 192.168.10.133:7003
    slots:10923-16383 (5461 slots) master
    S: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.10.133:7004
    replicates d8f6a0e3192c905f0aad411946f3ef9305350420
    S: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.10.133:7005
    replicates 7a12bc730ddc939c84a156f276c446c28acf798c
    S: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.10.133:7006
    replicates 93f73d2424a796657948c660928b71edd3db881f
    Can I set the above configuration? (type 'yes' to accept): yes
    >>> Nodes configuration updated
    >>> Assign a different config epoch to each node
    >>> Sending CLUSTER MEET messages to join the cluster
    Waiting for the cluster to join....
    >>> Performing Cluster Check (using node 192.168.10.133:7001)
    M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.10.133:7001
    slots:0-5460 (5461 slots) master
    M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.10.133:7002
    slots:5461-10922 (5462 slots) master
    M: 93f73d2424a796657948c660928b71edd3db881f 192.168.10.133:7003
    slots:10923-16383 (5461 slots) master
    M: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.10.133:7004
    slots: (0 slots) master
    replicates d8f6a0e3192c905f0aad411946f3ef9305350420
    M: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.10.133:7005
    slots: (0 slots) master
    replicates 7a12bc730ddc939c84a156f276c446c28acf798c
    M: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.10.133:7006
    slots: (0 slots) master
    replicates 93f73d2424a796657948c660928b71edd3db881f
    [OK] All nodes agree about slots configuration.
    >>> Check for open slots...
    >>> Check slots coverage...
    [OK] All 16384 slots covered.
    [root@localhost-0723 redis]#
  • 执行完成后,每个节点目录下会有 nodes.conf,记录了所有其他节点的信息,主从节点,槽分配等。

img

img

# 命令客户端连接集群

命令:

./redis-cli –h 127.0.0.1 –p 7001 –c

注意:-c 表示是以 redis 集群方式进行连接

./redis-cli -p 7006 -c
127.0.0.1:7006> set key1 123
-> Redirected to slot [9189] located at 127.0.0.1:7002
OK
127.0.0.1:7002>

# 查看集群的命令

# 查看集群状态

127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:926
cluster_stats_messages_received:926

# 查看集群中的节点:

127.0.0.1:7003> cluster nodes
7a12bc730ddc939c84a156f276c446c28acf798c 127.0.0.1:7002 master - 0 1443601739754 2 connected 5461-10922
93f73d2424a796657948c660928b71edd3db881f 127.0.0.1:7003 myself,master - 0 0 3 connected 10923-16383
d8f6a0e3192c905f0aad411946f3ef9305350420 127.0.0.1:7001 master - 0 1443601741267 1 connected 0-5460
4170a68ba6b7757e914056e2857bb84c5e10950e 127.0.0.1:7006 slave 93f73d2424a796657948c660928b71edd3db881f 0 1443601739250 6 connected
f79802d3da6b58ef6f9f30c903db7b2f79664e61 127.0.0.1:7004 slave d8f6a0e3192c905f0aad411946f3ef9305350420 0 1443601742277 4 connected
0bc78702413eb88eb6d7982833a6e040c6af05be 127.0.0.1:7005 slave 7a12bc730ddc939c84a156f276c446c28acf798c 0 1443601740259 5 connected
127.0.0.1:7003>

# 维护节点

集群创建成功后可以继续向集群中添加节点

# 添加主节点

  • 先创建 7007 节点
  • 添加 7007 结点作为新节点
  • 执行命令:
./redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

img

  • 查看集群结点发现 7007 已添加到集群中

img

# hash 槽重新分配(数据迁移)

添加完主节点需要对主节点进行 hash 槽分配,这样该主节才可以存储数据。

  • 查看集群中槽占用情况
cluster nodes

redis 集群有 16384 个槽,集群中的每个结点分配自已槽,通过查看集群结点可以看到槽占用情况。

img

  • 给刚添加的 7007 结点分配槽

  • 第一步:连接上集群(连接集群中任意一个可用结点都行)

    • reshard:重新分配

./redis-cli --cluster reshard 127.0.0.1:7007

  • 第二步:输入要分配的槽数量

img

输入:3000,表示要给目标节点分配 3000 个槽

  • 第三步:输入接收槽的结点 id

img

输入:15b809eadae88955e36bcdbb8144f61bbbaf38fb
PS:这里准备给 7007 分配槽,通过 cluster nodes 查看 7007 结点 id 为 15b809eadae88955e36bcdbb8144f61bbbaf38fb

  • 第四步:输入源结点 id

img

输入:all

  • 第五步:输入 yes 开始移动槽到目标结点 id

img

输入:yes

# 添加从节点

  • 添加 7008 从结点,将 7008 作为 7007 的从结点

    • 命令:```
      ./redis-cli --cluster add-node 新节点的 ip 和端口 旧节点 ip 和端口 --cluster-slave --cluster-master-id 主节点 id
    • 例如:```
      ./redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7007 --cluster-slave --cluster-master-id d1ba0092526cdfe66878e8879d446acfdcde25d8
    • d1ba0092526cdfe66878e8879d446acfdcde25d8 是 7007 结点的 id,可通过 cluster nodes 查看。

img

    • 注意:如果原来该结点在集群中的配置信息已经生成到 cluster-config-file 指定的配置文件中(如果 cluster-config-file 没有指定则默认为 nodes.conf),这时可能会报错:

      • [ERR] Node XXXXXX is not empty. Either the node already knows other nodes(check with CLUSTER NODES) or contains some key in database 0
      • 解决方法是删除生成的配置文件 nodes.conf,删除后再执行 ./redis-trib.rb add-node 指令
  • 查看集群中的结点,刚添加的 7008 为 7007 的从节点:

img

# 删除结点

  • 命令:
./redis-cli --cluster del-node 127.0.0.1:7008 41592e62b83a8455f07f7797f1d5c071cffedb50
  • 删除已经占有 hash 槽的结点会失败,报错如下:

    • [ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.
    • 需要将该结点占用的 hash 槽分配出去(参考 hash 槽重新分配章节)。

# Jedis 连接集群

需要开启防火墙,或者直接关闭防火墙。

service iptables stop

# 创建 JedisCluster 类连接 redis 集群。

@Test
public void testJedisCluster() throws Exception {
    //创建一连接,JedisCluster对象,在系统中是单例存在
    Set<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("192.168.10.133", 7001));
    nodes.add(new HostAndPort("192.168.10.133", 7002));
    nodes.add(new HostAndPort("192.168.10.133", 7003));
    nodes.add(new HostAndPort("192.168.10.133", 7004));
    nodes.add(new HostAndPort("192.168.10.133", 7005));
    nodes.add(new HostAndPort("192.168.10.133", 7006));
    JedisCluster cluster = new JedisCluster(nodes);
    //执行JedisCluster对象中的方法,方法和redis一一对应。
    cluster.set("cluster-test", "my jedis cluster test");
    String result = cluster.get("cluster-test");
    System.out.println(result);
    //程序结束时需要关闭JedisCluster对象
    cluster.close();
}

# 使用 spring

配置 applicationContext.xml

<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <!-- 最大连接数 -->
    <property name="maxTotal" value="30" />
    <!-- 最大空闲连接数 -->
    <property name="maxIdle" value="10" />
    <!-- 每次释放连接的最大数目 -->
    <property name="numTestsPerEvictionRun" value="1024" />
    <!-- 释放连接的扫描间隔(毫秒) -->
    <property name="timeBetweenEvictionRunsMillis" value="30000" />
    <!-- 连接最小空闲时间 -->
    <property name="minEvictableIdleTimeMillis" value="1800000" />
    <!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
    <property name="softMinEvictableIdleTimeMillis" value="10000" />
    <!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
    <property name="maxWaitMillis" value="1500" />
    <!-- 在获取连接的时候检查有效性, 默认false -->
    <property name="testOnBorrow" value="true" />
    <!-- 在空闲时检查有效性, 默认false -->
    <property name="testWhileIdle" value="true" />
    <!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
    <property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis集群 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
    <constructor-arg index="0">
        <set>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7001"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7002"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7003"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7004"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7005"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7006"></constructor-arg>
            </bean>
        </set>
    </constructor-arg>
    <constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>

测试代码

private ApplicationContext applicationContext;
@Before
public void init() {
    applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
}
// redis集群
@Test
public void testJedisCluster() {
    JedisCluster jedisCluster = (JedisCluster) applicationContext.getBean("jedisCluster");
    jedisCluster.set("name", "zhangsan");
    String value = jedisCluster.get("name");
    System.out.println(value);
}

# 总结

这篇文章我们介绍了 Redis 主从复制的流程和工作原理,以及在复制过程中可能引发的问题。

虽然搭建一个复制集群很简单,但其中涉及到的细节也很多。Redis 在复制过程也可能存在各种问题,我们需要设置合适的配置参数和合理运维 Redis,才能保证 Redis 有稳定可用的副本数据,为我们的高可用提供基础。