Redis主从复制、哨兵机制、Redis集群
生产上永远不可能用单机,所有的节点,包括应用节点,中间件节点,数据节点等都要保证高可用,最基本的要求。
主从必然存在延迟问题
,Redis按照分布式集群来讲,就不是强一致性的CAP结构,只保证了高可用AP,没有保证强一致性CP,所以必然存在延迟。
CAP:
一致性(Consistency)
可用性(Availability)
分区容忍性(Partition tolerance)如果业务需要主从机制必须强一致,那就要考虑适不适合选择Redis。
如果单机情况下,机器重启,内存数据丢失,如何保证数据的高可用呢?持久化方案
如果单机,这台机器的硬件坏了,例如硬盘坏了,或者访问压力太大,服务器崩溃了,如何保证数据的高可用呢?主从复制
Redis的主从机制:主负责读写,从一般只读不能写(客户端)。
和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。 如下图:
主从复制特点:
主Redis配置
从Redis配置
# slaveof
# 表示当前【从服务器】对应的【主服务器】的IP是192.168.10.135,端口是6379。
slaveof 192.168.10.135 6379 (5.0之前的配置)
replicaof 192.168.19.135 6379 (5.0之后的配置)
复制整体步骤如下:
全量同步
)增量同步
)。上面说的整体复制过程,其中有一个步骤是“同步数据集”,这个就是现在讲的‘数据间的同步’。
sync
和 psync
,前者是 redis 2.8 之前的同步命令,后者是 redis 2.8 为了优化 sync 新设计的命令。我们会重点关注 2.8 的 psync 命令。
主从节点各自复制偏移量
主节点复制积压缓冲区
主节点运行 ID
用于部分复制
和复制命令丢失的数据补救
。通过 info replication 可以看到相关信息。命令格式为 psync{runId}{offset}
runId:从节点所复制主节点的运行 id
offset:当前从节点已复制的数据偏移量
首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;
如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步
。流程说明:从节点发送 psync 命令给主节点,runId 就是目标主节点的 ID,如果没有默认为 -1,offset 是从节点保存的复制偏移量,如果是第一次复制则为 -1.
主节点会根据 runid 和 offset 决定返回结果:
全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的的阶段。触发全量复制的命令是 sync 和 psync。之前说过,这两个命令的分水岭版本是 2.8,redis 2.8 之前使用 sync 只能执行全量同步,2.8 之后同时支持全量同步和部分同步
。
介绍一下上图步骤:
复制积压缓冲区
”,当从节点加载 RDB 完毕,再发送过去。(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)以上加粗的部分是整个全量同步耗时的地方。
注意:
如过 RDB 文件大于 6GB,并且是千兆网卡,Redis 的默认超时机制(60 秒),会导致全量复制失败。可以通过调大 repl-timeout 参数来解决此问题。
Redis 虽然支持无盘复制(不使用磁盘作为中间存储),即直接通过网络发送给从节点,但功能不是很完善,生产环境慎用。
当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将复制缓冲区
的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。
前提时offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,否则还是全量复制
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
主从节点在建立复制后,他们之间维护着长连接并彼此发送心跳命令。
心跳的关键机制如下:
主从都有心跳检测机制,各自模拟成对方的客户端进行通信,通过 client list 命令查看复制相关客户端信息,主节点的连接状态为 flags = M,从节点的连接状态是 flags = S。
主节点默认每隔 10 秒对从节点发送 ping 命令,可修改配置 repl-ping-slave-period 控制发送频率。
从节点在主线程每隔一秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
主节点收到 replconf 信息后,判断从节点超时时间,如果超过 repl-timeout 60 秒,则判断节点下线。
注意:为了降低主从延迟,一般把 redis 主从节点部署在相同的机房/同城机房,避免网络延迟带来的网络分区造成的心跳中断等情况。
主节点不但负责数据读写,还负责把写命令同步给从节点,写命令的发送过程是异步完成,也就是说主节点处理完写命令后立即返回客户端,并不等待从节点复制完成。
异步复制的步骤很简单,如下:
可以看出,RDB 数据之间的同步非常耗时。所以,Redis 在 2.8 版本退出了类似增量复制的 psync 命令,当 Redis 主从直接发生了网络中断,不会进行全量复制,而是将数据放到缓冲区(默认 1MB)里,在通过主从之间各自维护复制 offset 来判断缓存区的数据是否溢出,如果没有溢出,只需要发送缓冲区数据即可,成本很小,反之,则要进行全量复制,因此,控制缓冲区大小非常的重要。
如果主节点挂了,从机是不能负责写命令的处理,必然不可取,就需要结合Redis哨兵。
Redis 主从复制的缺点:没有办法对 master 进行动态选举,需要使用 Sentinel 机制完成动态选举。
down-aftermilliseconds
选项所指定的值, 则这实例会被 Sentinel (哨兵)进程标记为主观下线( SDOWN )
。主观下线( SDOWN )
,则正在监视这个 Master 主服务器的所有 Sentinel (哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。主观下线状态( SDOWN )
, 则 Master 主服务器会被标记为客观下线( ODOWN )
。客观下线( ODOWN )
时, Sentinel (哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10秒一次改为每秒一次。通过配置quorumc参数,默认是1,即一个哨兵就有生杀大权,也不能超过所有哨兵的数量,建议大于一半
)确认了Master进入了主观下线状态,就会将master标记为ODOWN:客观下线哨兵本身也是一个redis节点
,也是一个完整的程序,也有自己端口等,只是命令不一样,可以和Redis节点放一块,也可以单独放到一个服务器
sentinel.conf :
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor
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
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,
# 这个数字越小,完成failover所需的时间就越长,
# 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs
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
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给客户端:
#
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
通过 redis-sentinel 启动哨兵服务
./redis-sentinel sentinel.conf
redis3.0以后推出的redis cluster 集群方案,redis cluster集群保证了高可用、高性能、高可扩展性。
去中心化
:没有leader节点架构细节:
PING-PONG机制
),内部使用二进制协议优化传输速度和带宽.Redis 集群中内置了 16384个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
(1)节点失效判断:集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout)
,认为该master节点挂掉.
(2)集群失效判断:什么时候整个集群不可用(cluster_state:fail)?
Redis集群最少需要三台主服务器,三台从服务器。
端口号分别为:7001~7006
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 ..
放到和节点同级目录./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]#
命令:
./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>
集群创建成功后可以继续向集群中添加节点
./redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001
添加完主节点需要对主节点进行hash槽分配,这样该主节才可以存储数据。
cluster nodes
redis集群有16384个槽,集群中的每个结点分配自已槽,通过查看集群结点可以看到槽占用情况。 ./redis-cli --cluster reshard 127.0.0.1:7007
PS:这里准备给7007分配槽,通过cluster nodes查看7007结点id为15b809eadae88955e36bcdbb8144f61bbbaf38fb
./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
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes(check with CLUSTER NODES) or contains some key in database 0
./redis-trib.rb add-node
指令./redis-cli --cluster del-node 127.0.0.1:7008 41592e62b83a8455f07f7797f1d5c071cffedb50
[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.
需要开启防火墙,或者直接关闭防火墙。
service iptables stop
@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();
}
配置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" />
<property name="maxWaitMillis" value="1500" />
<property name="testOnBorrow" value="true" />
<property name="testWhileIdle" value="true" />
<property name="blockWhenExhausted" value="false" />
bean>
<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);
}