Redis主从和集群配置
Redis主从配置
一、持久化
因为Redis数据是在内存中的,所以没办法处理数据量非常大的情况。
Redis的高性能是由于其将所有数据都存储在了内存中,为了使Redis在重启之后仍能保证数据不丢失,需要将数据从内存中同步到硬盘中,这一过程就是持久化。Redis支持两种方式的持久化,一种是RDB方式,一种是AOF方式。可以单独使用其中一种或将二者结合使用。
RDB模式
RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘。RDB是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
save 900 1 #900秒内至少1个键被更改则进行快照
save 300 10 #300秒内至少10个键被更改则进行快照
save 60 10000 #60秒内有10000个键被更改则进行快照
dbfilename dump.rdb #指定快照文件名称
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。
这种方式存在的问题:一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。
AOF模式
默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
appendonly yes #默认为no,设置为yes启用aof模式
appendfilename "appendonly.aof" #aof文件
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。
二、主从复制
1. 主从复制原理
持久化保证了即使redis服务重启也会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,如果通过redis的主从复制机制就可以避免这种单点故障
说明:
- 主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。
- 主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。
- 只有一个主redis,可以有多个从redis。主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求,一个redis可以即是主又是从。
2. 主从配置
- 主从模式,可以是树状的,从服务属于多台主服务,且从服务也可以有从服务。
- 主从模式,可实现读写分离;高可用模式下,主服务出现问题,
也可以通过哨兵切换从服务为主服务;- 主从模式,可实现主服务不用数据持久化,从服务进行持久化工作,减轻主服务负担等等。
- 创建多个redis实例,其中一个作为master,其余作为slave。
- master实例不做任何特殊处理。
- 修改slave配置文件即可。
# replicaof <masterip> <masterport>
# 配置你的master
replicaof 192.168.1.14 6379
# masterauth <master-password>
# 如果master启用了密码,需要配置此项
masterauth yourpassword
bind your slave address
port your slave port
以同样的守护线程方式启动三个redis服务,查看进程
ps aux | grep redis
测试在master中set name tom,在slave中keys *,get name发现数据同步了,即主从配置测试成功。
连接客户端以后,通过一下命令查看当前主从中服务中的运行状态:
info replication
三、哨兵模式
Redis的主从复制模式下, 一旦主节点由于故障不能提供服务, 需要人工将从节点晋升为主节点, 同时还要通知应用方更新主节点地址。
Redis从2.8开始正式提供了Redis Sentinel(哨兵) 架构来解决这个问题。
1. 哨兵模式介绍
- Sentinel(哨兵)进程是用于监控Redis集群中Master主服务器工作的状态,在Master主服务器发生故障的时候,可以自动实现Master和Slave服务器的切换,保证系统的高可用。
- 哨兵会不断检查Master和Slave是否正常运作,当被监控的某个Redis节点出现问题以后,哨兵会通过API向管理员或其他应用程序发送通知。当一个Master不能工作正常时,哨兵会开始做一次故障迁移操作。
哨兵是redis集群架构中非常重要的一个组件。主要功能如下:
- 集群监控:负责监控redis master和slave进程是否正常工作
- 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移:如果master节点挂掉了,会自动把一个slave升为master
- 配置中心:如果故障转移发生了,通知client客户端新的master地址
2. 哨兵工作方式
2.1 监控过程
监听服务器
- 每个哨兵以每秒一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个PING命令
出现故障
- 如果一个实例距离最后一次有效回复PING命令时间超过一定时间,则这个实例会被哨兵进程标记为主观下线(SDOWN)。
- 如果有一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有哨兵进程要以每秒一次的频率确定Master主服务器的确进入了主观下线状态。
- 当有足够数量的Sentinel(哨兵)进程在指定时间范围内确认Master主服务器进入了主观下线状态,则主服务器Master会被标记为客观下线(ODOWN)
开始故障转移(需要通过其他哨兵的同意,先去询问是否允许进行故障转移,如果有一半以上的哨兵同意故障转移,才开始进行故障转移)
- 在一般情况下,每个哨兵进程会以10秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
- 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
2.2 SDOWN和ODOWN
- SDOWN:主观宕机
- ODOWN:客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,就是客观宕机
- SDOWN达成的条件:如果一个哨兵ping一个master,超过了sentinel down-after-milliseconds mymaster标记的时间,就认为主观宕机。
- ODOWN达成的条件:如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机
2.3 quorum和majority
- quorum:确认odown的最少的哨兵数量
- majority:授权进行主从切换的最少的哨兵数量(半数的哨兵)
- 每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
- 如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换,但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换
集群配置中哨兵至少有三个节点
3. 配置文件
修改sentinel.conf,原先在解压后的Redis数据库下,拷贝一份即可。
# 设置端口
port 26379
# 是否守护进程启动
daemonize no
# 守护进程运行的时候需要保留pidfile
pidfile /var/run/redis-sentinel.pid
# 日志文件
logfile "/root/log/sentinel.log"
# sentinel monitor 名称 IP 端口 quorum数
sentinel monitor mymaster 192.168.1.14 6379 3
# sentinel down-after-milliseconds 名称 时间长度
# 超过30000ms就会ping不通会认为SDOWN
sentinel down-after-milliseconds mymaster 30000
# 如果master故障转移后,剩下的slave同时有几个挂载到新的master上
sentinel parallel-syncs mymaster 1
# 执行故障转移的超时时长
sentinel failover-timeout mymaster 180000
# master-name password
sentinel auth-pass mymaster yourpass
注意:
- sentinel monitor 要设置到最前
- 最好不要给redis设置密码,故障转移的时候不会自动在配置文件里添加密码,导致故障转移启动原master成为slave不能生效。
- 每次启动关闭后,要重新配置sentinel.conf
启动:
./redis-sentinel sentinel.conf
集群
哨兵模式下只有一个master可以进行写数据,其余进行读取数据,这样如果写数据量大的时候会造成服务器压力过大,可以通过集群连接多个master。
1. 架构细节
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
- 节点的fail是通过集群中超过半数的节点检测失效时才生效.
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可.
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
2. 投票容错
- 投票容错过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉。
- 什么时候整个集群不可用(cluster_state:fail)?
a. 如果集群任意master挂掉,且当前master没有slave,集群进入fail状态。也可以理解成集群的slot映射[0-16383]不完成时进入fail状态。
b. 如果集群超过半数以上master挂掉,无论是否有slave集群进入fail状态。
ps:
1:当集群不可用时,所有对集群的操作做都不可用
2:当有slave时会将挂掉的节点的slave提升为master
3:一个节点存储的数据可以根据自身服务器的内存来配置要放多少个槽
3. 创建集群
- 2018年十月 Redis 发布了稳定版本的 5.0 版本,推出了各种新特性,其中一点是放弃 Ruby的集群方式,改为使用C语言编写的 redis-cli的方式,使集群的构建方式复杂度大大降低。
- 因为redis集群投票机制,所以最低3个节点,不然无法创建,每个节点一个从节点,一共需要6个redis实例。
- 模拟创建6个redis实例(同一机器下),端口为7001~7006
- 在redis.conf配置文件中开启集群模式
cluster-enabled yes
- 启动所有实例
- 使用 redis-cli 创建集群
redis-cli --cluster create 192.168.1.14:7001 192.168.1.14:7002 192.168.1.14:7003 192.168.1.14:7004 192.168.1.14:7005 192.168.1.14:7006 --cluster-replicas 1
cluster-replicas 1:指定从机数量
开启集群前先删除dump.rdb appendonly.aof nodes.conf文件,保证以一个新的集群去开启
5. 测试
以集群方式登录一个客户端
./redis-cli -h 192.168.1.14 -p 7001 -c
# 如果增加一个数据会自动分配到槽对应的节点上
# 不加-c如果key的槽不在当前机器上会报错
执行:
cluster info
#或
cluster nodes
查看当前集群中的状态。
4. 向集群中添加节点
- 挂载节点
./redis-cli --cluster add-node 192.168.1.14:7007 192.168.1.14:7001
./redis-cli --cluster add-node 要添加的master IP:port 集群中的任意节点
- 分配槽
./redis-cli --cluster reshard 192.168.1.14:7001 --cluster-from all --cluster-to ffedbcb8c2ddf81c1eaf0e581ee882cd2f9272c6 --cluster-slots 4000
./redis-cli --cluster reshard 集群中的一个节点 --cluster-from all --cluster-to 集群中的一个节点id --cluster-slots 分配的槽数
- 给新增的节点添加从机
./redis-cli --cluster add-node 192.168.1.14:7008 192.168.1.14:7001 --cluster-slave --cluster-master-id 7f3bcd2668b736b8e0e522caca41a3e2e6c5c03b
./redis-cli --cluster add-node 要加的slave节点 集群中的一个节点 --cluster-slave --cluster-master-id 要挂载到的新加的master节点id
5. 在集群中删除一个节点
- 首先删除master对应的slave
./redis-cli --cluster del-node 192.168.1.14:7008 482ab1b25f422a401b60e0285cd718098ef120be
./redis-cli --cluster del-node 任意节点(连接到集群) 节点id
- 清空master的slot
./redis-cli --cluster reshard 192.168.1.14:7007 --cluster-from 7f3bcd2668b736b8e0e522caca41a3e2e6c5c03b --cluster-to ffedbcb8c2ddf81c1eaf0e581ee882cd2f9272c6 --cluster-slots 1024 --cluster-yes
由于集群中现在有四个节点,而每次reshard只能写一个目的节点,因此以上命令需要执行三次( --cluster-to 对应不同的节点 --cluster-yes:直接迁移)
如果给一个非常大的数作为归还的槽,将认为全部归还。
- 删掉节点
./redis-cli --cluster del-node 192.168.1.14:7007 7f3bcd2668b736b8e0e522caca41a3e2e6c5c03b
java中使用redis
添加jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
测试
public class Test01 {
/*
* 直接连接
*/
// @Test
public void testJedis() {
Jedis jedis = new Jedis("192.168.1.14", 7001);
String result = jedis.get("name");
System.out.println(result);
jedis.close();
}
/*
* 使用连接池
*/
// @Test
public void testJedisPool() {
JedisPool jedisPool = new JedisPool("192.168.1.14", 7003);
Jedis jedis = jedisPool.getResource();
String result = jedis.get("table");
System.out.println(result);
jedis.close();
jedisPool.close();
}
/*
* 连接哨兵集群
*/
// @Test
public void testSentinel() {
Set<String> sentinelSet = new HashSet<>();
sentinelSet.add("192.168.1.14:26379");
sentinelSet.add("192.168.1.14:26380");
sentinelSet.add("192.168.1.14:26381");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinelSet);
Jedis jedis = pool.getResource();
jedis.set("hello", "world");
String result = jedis.get("hello");
System.out.println(result);
jedis.close();
pool.close();
}
/*
* 连接集群
*/
@Test
public void testJedisCluster() {
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.14", 7001));
nodes.add(new HostAndPort("192.168.1.14", 7002));
nodes.add(new HostAndPort("192.168.1.14", 7003));
nodes.add(new HostAndPort("192.168.1.14", 7004));
nodes.add(new HostAndPort("192.168.1.14", 7005));
nodes.add(new HostAndPort("192.168.1.14", 7006));
JedisCluster jedisCluster = new JedisCluster(nodes);
String result = jedisCluster.get("table");
System.out.println(result);
jedisCluster.close();
}
}