我们都知道单机版的redis,无法保证CAP。所以我们搭建redis集群,实现高可用。
图来自网络.png看本篇文章之前,最好看如下几篇文章。
1.【Linux学习】 Redis常用的一些指令
2.分布式架构之旅】Redis入门
3.Java开发技术大杂烩(一)之Redis、Jmeter、MySQL的那些事
4.Java开发技术大杂烩(二)(redis、mysql、http、shiro、threadlocal)
Redis最好部署在独立系统上,避免其他系统干扰和增加排错的成本。这里我就用我的一台服务器通过端口的方式划分redis,模拟不同的系统中的redis。ps 111.230.11.184代表我服务器的地址。
master: 111.230.11.184 6379
slave01:111.230.11.184 6380
slave02:111.230.11.184 6381
sentinel01:111.230.11.184 26380
sentinel02:111.230.11.184 26381
Redis主从搭建
在slave01和slave02上进行配置,指定master的地址。
slaveof 127.0.0.1 6379
masterauth "password" #如果master设置了密码,那么指定master密码
使用redis-cli -p 6379 -a password
连接redis的客户端,输入info replication
命令查看当前redis处于集群中的角色。
查看master的状态
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=111.230.11.184,port=6380,state=online,offset=1170324,lag=1
slave1:ip=111.230.11.184,port=6381,state=online,offset=1170324,lag=1
master_replid:22ae18ccd087e06840f3c03c5dd9ee7390bb4add
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1170468
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1123449
repl_backlog_histlen:47020
查看slave01的状态
# Replication
role:slave
master_host:111.230.11.184
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:1190694
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:22ae18ccd087e06840f3c03c5dd9ee7390bb4add
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1190694
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1123449
repl_backlog_histlen:67246
查看slave02的状态
# Replication
role:slave
master_host:111.230.11.184
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:1213670
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:22ae18ccd087e06840f3c03c5dd9ee7390bb4add
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1213670
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1127402
repl_backlog_histlen:86269
我们在master中,进行设值操作。
127.0.0.1:6379> set good-coder 'cmazxiaoma'
OK
127.0.0.1:6379>
接着在slave01和slave02,进行取值操作。能取到值,证明搭建的主从集群成功了。
127.0.0.1:6380> get good-coder
"cmazxiaoma"
127.0.0.1:6380>
127.0.0.1:6381> get good-coder
"cmazxiaoma"
Redis主从复制可分为增量同步和全量同步。
增量同步:
master每执行一个写的命令就会向slave发送相同的命令,slave接收到写命令后就会去执行。这样master和slave就处于一致性了。
全量同步:
1.master和slave建立连接,slave向master发送sync命令。
2.master收到sync命令后,执行bgsave命令,开启后台进程对数据库数据进行快照生成rdb文件,同时使用缓冲区记录此后执行的所有写命令。
3.master执行bgsave命令后,向所有slave发送快照文件,并且在发送期继续记录写命令。
4.slave收到快照文件后,丢弃所有的旧数据,加载快照文件构建最新的数据。
5.master的快照文件发送完毕后,将缓冲区的写命令依次向slave发送。
6.slave加载快照文件完毕之后,开始接收master发送的写命令并执行写命令。
然而这样有一种缺点,在主从复制过程中断线后,又要重新来过。这样很操蛋,快照生成rdb文件这个过程比较耗时且占用cpu、磁盘,内存,发送rdb文件也是比较占用服务器带宽的。
所以在redis2.8版本后做了优化:使用PSYNC命令代替SYNC命令,PSYNC命令支持完整重同步和部分重同步。完整重同步没什么好讲的,和上面的差不多。
部分重同步由3个部分组成:
1.replication offset:master和slave的复制偏移量。
2.replication log:master的复制积压缓冲区,是一个固定长度的先进先出队列,默认是1MB。如果你的master平均1s产生1mb的数据,而slave断线之后平均需要10s才能重连,那么master的复制积压缓冲区就需要20mb了(2乘以second乘以write_size_per_second)。
3.run id:服务器运行的id。
当master执行写命令的时候,复制积压缓冲区就会保存最近的写命令。每一条命令都对应着一个复制偏移量。当进行主从复制的时候,master就会将最新的复制偏移量同步给slave。如果复制中断后再进行复制,slave可以通过runid找到当时复制的master,再根据上一次复制偏移量找到中断过程中未执行到的命令,再进行复制操作。
Redis主从架构缺点很明显,master挂了之后,之后的请求无法进行写操作。后面的哨兵模式就解决了这种痛点。
master具备写功能,slave具备读功能。大多数请求都是读多写少,redis主从架构把请求进行削峰,把大部分的压力都落到slave,大大减少了对master的压力。除此以外,还可以关闭master的持久化功能,让其slave去做。
值得注意的是slave01的配置信息是连接master,slave02的配置信息是连接slave01,以此类推。
Redis哨兵模式搭建
所谓的哨兵,就是监听slave和master的一举一动。当master挂了之后,就会推举slave当新的master。当旧master恢复之后,也只能当新master的slave。
哨兵的主要功能有以下几点:
1.默认情况下,哨兵会以每秒一次的频率对master和slave发送ping命令(基于ICMP协议),判断目标是否可达。当单个sentinel对redis服务器做出了下线的判断,被称之为主观下线(SDOWN)。当多个sentinel都对服务器做出了下线的判断,被称之为客观下线(ODOWN)。一个sentinel可以通过 is-master-down-by-addr
命令向其他sentinel询问指定的redis服务器是否已下线。当一个sentinel发送了ping后,在master-down-after-milliseconds
时间之内还没有收到答复的话,就可以被认定为SDOWN。
2.当master和slave发生故障时,sentinel可以通过notification-script
和reconfig-script
来通知系统管理员和做一些其他的骚操作。比如通过notification-script
发送报警邮件。当sentinel做故障转移的时候,master会发生变化,此时会执行到reconfig-script
通知相关程序master已经发生变化。sentinel在执行这个脚本的时候,会传递6个参数master-name
,role
,state
,from-ip
,from-port
,to-ip
,to-port
,我们利用这个特性,可以做漂移VIP。
3.当master发生故障时,哨兵可以开启自动故障转移。在所有的slave中选举出一个slave,将其转换成master。让其他slave重新配置使用新的master。将旧的master设置成新master的slave。当旧的master重新上线时,它会成为新master的slave。
配置sentinel01和sentienl02:
daemonize yes
protected-mode no
port 26380
sentinel monitor mymaster 111.230.11.184 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 15000
sentinel auth-pass mymaster password
sentinel parallel-syncs mymaster 1
启动哨兵时,你会发现sentinel.conf会多了以下配置
image.png
具体的配置说明:
-
sentinel monitor mymaster 111.230.11.184 6379 2
:sentinel去监视一个名为mymaster的主redis实例。而将这个主实例判断为失效至少需要2个 sentinel进程的同意,只要同意sentinel的数量不达标,自动failover就不会执行。 -
sentinel down-after-milliseconds mymaster 5000
:指定了sentinel认为redis实例已经失效所需的毫秒数。当实例超过该时间没有返回ping,或者直接返回错误,那么sentinel将这个实例标记为主观下线。只有一个 sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线,这时自动故障迁移才会执行。 -
sentinel parallel-syncs mymaster 1
:指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长。 -
sentinel failover-timeout mymaster 15000
:如果在该时间(ms)内未能完成failover操作,则认为该failover失败。
我们可以查看sentinel日志,观察sentinel 切换master以及询问其他哨兵是否认为某个主节点已经主观下线和开始故障转移时,当前哨兵向其他哨兵进行拉票选举leader的过程。
image.png image.png
客观下线的过程是这样的(参考Redis主从哨兵):
-
当sentinel监视的某个服务主观下线后,sentinel会询问其它监视该服务的sentinel,看它们是否也认为该服务主观下线,接收到足够数量(这个值可以配置)的sentinel判断为主观下线,既任务该服务客观下线,并对其做故障转移操作。
-
sentinel通过发送 SENTINEL is-master-down-by-addr ip port current_epoch runid,(ip:主观下线的服务id,port:主观下线的服务端口,current_epoch:sentinel的纪元,runid:*表示检测服务下线状态,如果是sentinel 运行id,表示用来选举领头sentinel)来询问其它sentinel是否同意服务下线。
-
一个sentinel接收另一个sentinel发来的is-master-down-by-addr后,提取参数,根据ip和端口,检测该服务时候在该sentinel主观下线,并且回复is-master-down-by-addr,回复包含三个参数:down_state(1表示已下线,0表示未下线),leader_runid(领头sentinal id),leader_epoch(领头sentinel纪元)。
-
sentinel接收到回复后,根据配置设置的下线最小数量,达到这个值,既认为该服务客观下线。
SpringBoot集合Redis哨兵模式
1.配置applicaiton-dev.properties
spring.redis.database=0
spring.redis.host=111.230.11.184
spring.redis.password=xiaoma
spring.redis.port=6379
#reids超时连接时间
spring.redis.timeout=100000
#连接池最大连接数
spring.redis.pool.max-active=10000
#连接池最大空闲数
spring.redis.pool.max-idle=1000
#连接池最大等待时间
spring.redis.pool.max-wait=10000
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=111.230.11.184:26380,111.230.11.184:26381
spring.redis.sentinel.timeout=100000
spring.redis.sentinel.password=password
2.配置RedisConfig
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.pool.max-active}")
private int poolMaxActive;
@Value("${spring.redis.pool.max-idle}")
private int poolMaxIdle;
@Value("${spring.redis.pool.max-wait}")
private int poolMaxWait;
@Value("${spring.redis.sentinel.master}")
private String sentinelMaster;
@Value("${spring.redis.sentinel.nodes}")
private String sentinelNodes;
@Value("${spring.redis.sentinel.timeout}")
private int sentinelTimeOut;
@Value("${spring.redis.sentinel.password}")
private String sentinelPassword;
}
3.配置RedisPool
@Component
public class RedisPoolFactory {
@Autowired
private RedisConfig redisConfig;
/**
* 单机redis配置
* @return
*/
@Bean
public JedisPool jedisPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxActive());
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait());
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getTimeout(), redisConfig.getPassword(), 0);
return jp;
}
@Bean
public JedisSentinelPool jedisSentinelPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxActive());
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
String[] sentinelArray = redisConfig.getSentinelNodes().split(",");
Set<String> sentinels = new HashSet<>();
CollectionUtils.addAll(sentinels, sentinelArray);
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(
redisConfig.getSentinelMaster(),
sentinels,
poolConfig,
redisConfig.getSentinelTimeOut(),
redisConfig.getSentinelPassword()
);
return jedisSentinelPool;
}
/**
* redis主从集群+哨兵模式配置
* @return
*/
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
String[] sentinelArray = redisConfig.getSentinelNodes().split(",");
for (String sentinel : sentinelArray) {
String[] hostAndPort = sentinel.split(":");
String host = hostAndPort[0].trim();
int port = Integer.parseInt(hostAndPort[1].trim());
redisSentinelConfiguration.addSentinel(new RedisNode(host, port));
}
redisSentinelConfiguration.setMaster(redisConfig.getSentinelMaster());
return redisSentinelConfiguration;
}
}
4.编写测试用例
public class RedisTest extends InitSpringTest {
@Autowired
private JedisSentinelPool jedisSentinelPool;
@Autowired
private JedisPool jedisPool;
@Test
public void testJedisSentinelPool() {
HostAndPort hostAndPort = jedisSentinelPool.getCurrentHostMaster();
System.out.println("=====>hostAndPort=" + hostAndPort);
Jedis jedis = null;
try {
jedis = jedisSentinelPool.getResource();
jedis.del("name");
jedis.set("name", "SUCCESS");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Test
public void testJedisPool() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del("name");
jedis.set("name", "jedisSentinel");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
5.查看log.发现运行成功。
2018-11-01 13:37:35.187 INFO [main] [redis.clients.jedis.JedisSentinelPool 136] :Trying to find master from available Sentinels...
2018-11-01 13:37:35.204 INFO [main] [redis.clients.jedis.JedisSentinelPool 185] :Redis master running at 111.230.11.184:6379, starting Sentinel listeners...
2018-11-01 13:37:35.206 INFO [main] [redis.clients.jedis.JedisSentinelPool 127] :Created JedisPool to master at 111.230.11.184:6379
2018-11-01 13:37:39.275 INFO [main] [org.springframework.boot.StartupInfoLogger 57] :Started RedisTest in 352.397 seconds (JVM running for 404.139)
=====>hostAndPort=111.230.11.184:6379
6.在master和slave01和slave02查看key为name的信息。master支持读写,slave01和slave02只支持读,不支持写。
image.png
尾言
这篇文章拖了好久才写完,是时候该治治我拖延症了,orz。
网友评论