一、Redis的复制
Redis的复制是Redis主从结构、哨兵集群的重要实现依据,Redis在复制中数据的流向只能是由主节点 --> 从节点。
二、Redis复制的拓扑结构
1、一主一从
结构:只有一个主节点和一个从节点,从节点同步主节点的数据,做主节点的数据备份。
场景:读写分离,降低单个节点的压力。
故障转移,如果主节点宕机,从节点升级为主节点。
扩展:可以只开启从节点的数据持久化,提升主节点的性能。但是一旦主节点重启,主节点数据清空,且主节点不会向从节点拿数据,就会导致从节点也跟着同步主节点的数据,最终导致数据丢失。
2、一主多从
结构:只有一台主节点,有多台从节点,所有从节点都从主节点同步数据。
场景:在读数据很多的场景下,为了减缓单台Redis的读压力,可以添加多台Redis从节点。
缺点:由于主节点每写入一次数据,就要向从节点推送一次数据,如果从节点过多,势必会对主节点造成影响,提高主节点压力。
3、树状主从
结构:只有一台主节点,有多台从节点,从节点不是全部都从主节点复制数据,而是只有一部分从节点从主节点复制数据,剩下从节点从其他从节点复制数据。
场景:解决掉了多台从节点都从主节点复制数据的性能问题。
缺点:结构相对复杂,难以维护。
4、主从结构下的持久化
不管使用哪种主从结构,都建议全部开启持久化,减少数据的丢失量。
三、Redis主从结构的配置
命令 | 作用 |
---|---|
任何一台Redis启动时,不加干预,都默认是一台主节点。 | |
info Replication | 查询Redis服务的信息 可以看到role是master,表示当前节点是主节点。 |
flushdb | 清空当前Redis节点的数据 |
在配置文件中加入如下配置 # slaveof <masterip> <masterport> | 以这个配置文件启动Redis,当前Redis节点会变成从节点,并且以127.0.0.1:6380的Redis节点为主节点。 这时可以看到节点信息是slave。 |
Redis客户端输入: slaveof 127.0.0.1 6380 | 如果Redis节点已经启动了,也可以在客户端输入以下命令实现从节点的转换。 命令表示先将当前节点变成从节点,再链接6380主节点。 |
Redis客户端输入: salveof no one | 先断开原有的主从关系,再把当前从节点变成主节点。 |
6380节点收到了 6381和6382两个从节点的请求。 | |
从节点默认只有读权限,没有写权限。 | |
config get slave-read-only | 查询当前节点是否只有读权限,yes代表只能读不能写。 |
config set slave-read-only no | 关闭从节点的只读限制,让从节点也能写入数据。 不过不建议开启从节点的写权限,容易造成主从数据不一致。 |
配置文件配置 masterauth 可以设置连接主节点的密码 | 从库连接主库需要校验密码 |
掌握这些命令就可以愉快地搭建主从结构啦!下面我们来看看主从复制是如何实现数据同步的呢?
四、Redis主从复制原理
① slaveof ip port 命令只是让从节点记录主节点的信息。
② 第一次主从之间同步数据是需要同步主节点已存在的全量数据,比较费时间。
③ 2.8版本之后采用 psync 的方式同步数据,全量同步 + 部分同步,提高同步效率。
④ 从库的过期策略:不会进行定期扫描,key在主库过期后再同步给从库,从库做del操作。
1、psync命令
刚建立主从关系时,如果主节点的数据量太大,使用全量复制会很耗费时间,故加入了psync命令。psync 命令在Redis 2.8 版本以后加入,运行过程分 “全量同步” 和 “部分同步”。
psync 命令的执行需要以下支持:
① 主从节点各自复制偏移量。
② 主节点复制积压缓冲区。
③ 主节点运行id。
2、全量复制
一般用于初次复制场景(第一次建立slave后全量复制),主节点给从节点发送一个RDB数据包。
3、全量复制有哪些问题?
主节点数据量太大,打包RDB文件慢,网络发送RDB文件时间长,从节点解析RDB文件慢。
=====================================================================
解决办法:
配置文件调整 repl-timeout 60 参数,同步RDB时间达到60s使同步作废。
如果想提高全量复制成功率,可以适当增加过期时间,通过计算主节点数据量和带宽 可以得到一个大概的传输时间:数据量 ➗ 带宽 ✖ 2。
如果主节点的写入流量太大,很可能将【客户端缓冲区】冲垮。
=====================================================================
解决办法:
配置文件调整 client-output-buffer-limit slave 256mb 64mb 60 参数
slave:表示主从复制。
256mb:表示如果客户端缓冲区的总数据量达到256mb,停止第7步传输。
64mb 60:表示如果在60s内的数据量达到64mb,停止第7步传输。
既然全量复制不能完全满足初始数据的同步,那我们该怎么办呢?
4、部分复制
部分复制是应对全量复制过程中出现网络中断时,从节点再次连接主节点时,主节点补发缺少的数据。
5、心跳
主从节点之间有长连接心跳,主节点默认每10s向从节点发送ping命令,来检查从节点是否存活。
repl-ping-slave-period 控制发送频率。
从节点每1s向主节点发送一次心跳 ping ,通知主节点自己存活。
6、总结
无论是全量复制还是部分复制,底层都是异步执行的,实时数据进入主节点后,都要暂存到客户端缓冲区或复制积压缓冲区,再启动其他任务发送这些缓冲区的数据,不会阻塞主线程。
所以主从复制中数据同步具有一定的延迟,部分复制在正常情况下延迟在1s内,具体看网络问题。
但是在这样的主从集群下,一旦主节点宕机,整套集群就会失去写入数据的能力,不能达到高可用。
五、Redis哨兵 + 主从
1、原理
当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用。
说人话就是主节点挂掉时哨兵会选一个从节点出来当新的主节点,即使出现数据丢失,一般也只会丢失1s的数据。
2、Redis哨兵的配置
一般配置3台哨兵节点,防止哨兵掉线,而且配置3台哨兵也是为了更快地选出领导者哨兵。
配置 | 作用 |
---|---|
sentinel monitor mymaster 127.0.0.1 6379 2 | 配置当前哨兵监控的主节点信息,包括ip和端口。 最后这个 2 要注意,后面要考。表示该节点有2票就能被哨兵投票出局。 |
windows版本启动: redis-server.exe redis.windows.conf --sentinel | 以哨兵身份启动一个哨兵节点,本质上启动的还是redis,但是职责在配置文件中变了 |
linux版本启动: sh redis-centinel.sh redis.conf | |
sentinel down-after-milliseconds mymaster 3000 | 主观下线时间,配置节点超过3s未响应哨兵的ping命令,就认为这台redis节点主观下线了 |
这样就配置好并且启动了redis哨兵啦!
3、为什么需要启动多个Redis哨兵?
Redis加入哨兵机制是为了处理单点故障问题,避免单台Redis节点故障影响整个集群。Redis哨兵本质上也是一个Redis节点,如果只有一台Redis哨兵,那么这台哨兵自己就可能出现单点故障,还是没达到高可用。
4、启动多台Redis哨兵会影响性能吗?
肯定是会的,但是Redis哨兵的职责只是监控其他Redis节点,不负责数据吞吐,并不会产生特别大的性能影响,可以容忍这样的性能消耗。
5、Redis哨兵的三个定时任务:
第一个:info
机制:每隔10s,哨兵给集群中每个节点发送info指令,可以拿到当前集群中的拓扑结构,同步到主从之间的信息。
第二个:publish/subscribe(发布/订阅)
机制:每隔2s,所有的哨兵都向主节点发送一次发布订阅。
功能:① 通过master节点,哨兵之间就可以感知到其他哨兵的存在。
② 哨兵之间可以交换主节点的信息,为将来出现master的下线或主节点的选举提供数据支撑。
第三个:ping
机制:每隔1s,哨兵节点都向所有节点发送一个ping命令。
功能:检查每个节点是否故障,如果某个节点过了很长时间未响应,那就认为这个节点故障了。
6、Redis哨兵判断主从节点下线
主观下线:
哨兵ping某个数据节点,超过配置文件中 down-after-milliseconds 的时长未响应,那么哨兵就认为这个数据节点下线了,这时还不能确认数据节点是不是真的下线了,因为有可能是这个哨兵下线了。这时不会做故障转移,而且去继续确认。
客观下线:
哨兵在发现有节点主观下线后,会去询问其他哨兵节点,检测目标节点是否下线,最后哨兵之间投票确认,如果达到配置文件中【sentinel monitor mymaster 127.0.0.1 6379 2(前面提到要考)】配置的 2 个哨兵认为下线了,那么就废调这个节点,执行故障转移。
7、领导者哨兵选举
如果master节点客观下线了,要做的第一件事不是选举新的master节点,而是确定出让哪个哨兵节点来做这件事。这个过程叫领导者哨兵选举。
比如哨兵1 和 哨兵2 同时发现master节点下线了,哨兵1 向 哨兵2 和哨兵3 进行询问,哨兵2 向哨兵1 和哨兵3 进行询问,哨兵2 最先获得2票,那么就选哨兵2 做领导者,让它来选出新的master节点。
哨兵领导者选举涉及一些深层次的算法,raft算法,我还不会,就先略过。
8、哨兵故障转移流程
9、在Java程序中连接哨兵+主从
在客户端程序中,需要知道最新的主节点和从节点的关系。如果出现故障转移,那么Java客户端就需要及时发现新的主节点。以下是在Java程序中配置哨兵的代码示例。
只要配置好 JedisSentinelPool 以后,再使用 jedisSentinelPool.getResource() 方法拿到Jedis实例,使用起来就和Jedis一模一样了。
但是这里的从节点只是做数据备份,还不能做读写分离。因为 JedisSentinelPool 连接池只能连接到master节点。
先看配置文件:
# jedis Sentinel Pool
# host:port
redis.sentinels=127.0.0.1:16380,127.0.0.1:16381,127.0.0.1:16382
redis.sentinel.master=mymaster
# unit:Millis
redis.sentinel.timeout=5000
# unit:s
redis.sentinel.maxIdle=10
redis.sentinel.maxWaitMillis=2000
redis.sentinel.blockWhenExhausted=true
redis.sentinel.jmxEnabled=true
再看配置类:
package sentinel.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* redis哨兵集群的连接配置
*
* @Date: 2024/07/26/8:01
*/
@Configuration
@PropertySource("classpath:application.properties")
public class RedisSentinelConfig {
@Value("${redis.sentinels}")
private String hosts;
@Value("${redis.sentinel.master}")
private String master;
@Value("${redis.sentinel.timeout}")
private int timeout;
@Value("${redis.sentinel.maxIdle}")
private int maxIdle;
@Value("${redis.sentinel.maxWaitMillis}")
private int maxWaitMillis;
@Value("${redis.sentinel.blockWhenExhausted}")
private Boolean blockWhenExhausted;
@Value("${redis.sentinel.jmxEnabled}")
private Boolean jmxEnabled;
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
// 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
// 是否启用pool的jmx管理功能, 默认true
jedisPoolConfig.setJmxEnabled(jmxEnabled);
jedisPoolConfig.setTestOnBorrow(true);
jedisPoolConfig.setTestOnReturn(true);
return jedisPoolConfig;
}
@Bean
public JedisSentinelPool jedisSentinelPoolFactory() {
if (StrUtil.isBlank(hosts)) {
throw new RuntimeException("请配置redis哨兵集群的节点");
}
List<String> hostList = Arrays.asList(hosts.split(","));
if (CollectionUtil.isEmpty(hostList)) {
throw new RuntimeException("hostList is empty");
}
Set<String> hostSet = new HashSet<>(hostList);
return new JedisSentinelPool(master, hostSet, jedisPoolConfig(), timeout);
}
}
10、哨兵+主从如何实现读写分离?
我没有找到现成的哨兵+主从实现读写分离插件,不过理论上可以使用Java代码实现读写分离。只要拿到哨兵节点后就可以读取到主从之间的拓扑结构,拿到从节点的host+port就可以对从节点进行读操作了。