基于spring的Redis Sentinel读写分离Slave连接池
0. 背景
Redis除了配置集群实现高可用之外,对于单机版的Redis,可以通过Master-Slave架构,配合使用Sentinel机制实现高可用架构,同时客户端可以实现自动失效转移。
类似于JdbcTemplate,Spring中使用RedisTemplate来操作Redis。SpringBoot中只需引入如下Maven依赖,即可自动配置一个RedisTemplate实例。
1 |
|
RedisTemplate需要一个RedisConnectionFactory来管理Redis连接。可以在项目中定义一个RedisSentinelConfiguration给RedisConnectionFactory,即可生成一个基于Sentinel的连接池,并且实现了自动失效转移:当master失效时,Sentinel自动提升一个slave成为master保证Redis的master连接高可用。
下面是基于Sentinel的RedisConnectionFactory的典型配置
1 |
|
查看org.springframework.data.redis.connection.jedis.JedisConnectionFactory源码发现,当配置了RedisSentinelConfiguration后,RedisConnectionFactory会返回一个JedisSentinelPool连接池。该连接池里面所有的连接都是连接到Master上面的。 同时,在JedisSentinelPool中为每一个Sentinel都配置了+switch-master频道的监听。当监听到+switch-master消息后表示发生了master切换,有新的Master产生,然后会重新初始化到新Master的连接池。
至此,我们知道基于Sentinel可以创建RedisConnectionFactory,并可实现自动失效转移,但RedisConnectionFactory只会创建到Master的连接。一般情况下,如果所有的连接都是连接到Master上面,Slave就完全当成Master的备份了,造成性能浪费。通常,Slave只是单纯的复制Master的数据,为避免数据不一致,不应该往Slave写数据,可以在Redis配置文件中配置slave-read-onlyyes,让Slave拒绝所有的写操作。于是,对于一个基于Sentinel的Master-Slave Redis 服务器来说,可以将Master配置为可读写服务器,将所有Slave配置为只读服务器来实现读写分离,以充分利用服务器资源,并提高整个Redis系统的性能。
1. 提出问题
JedisSentinelPool连接池中的连接都是到Master的连接,那么如何获取到Slave的连接池呢? 分析了spring-boot-starter-data-redis和jedis之后,发现,并没有现成的Slave连接池可以拿来用,于是决定写一个。
2. 分析问题
通过RedisSentinelConfiguration,可以拿到sentinel的IP和端口,就可以连接到sentinel,再调用sentinel slaves mymaster命令,就可以拿到slave的IP和port。
然后就可以创建到slave的连接了
继续查看JedisFactory源码,了解到其实现了PooledObjectFactory接口,该接口来自org.apache.commons.pool2,由此可见,Jedis连接池是借助Apache
commons.pool2来实现的。
由图看到,JedisConnectionFactory创建一个JedisSentinelPool,JedisSentinelPool创建JedisFactory,JedisFactory实现了PooledObjectFactory接口,在MakeObject()方法中产生新的Redis连接。在JedisSentinelPool中定义MasterListener还订阅+switch-master频道,一旦发生Master转移事件,自动作失效转移重新初始化master连接池。
3. 解决问题
模仿JedisConnectionFactory,JedisSentinelPool,和JedisFactory,创建JedisSentinelSlaveConnectionFactory,JedisSentinelSlavePool和JedisSentinelSlaveFactory它们之间的关系,如图UML-2所示。
其中,JedisSentinelSlaveConnectionFactory就是可以传递给RedisTemplate的。JedisSentinelSlaveConnectionFactory继承自JedisConnectionFactory并且覆盖了createRedisSentinelPool方法,在JedisConnectionFactory中,该方法会返回一个JedisSentinelPool,而新的方法会返回JedisSentinelSlavePool。JedisSentinelSlavePool和JedisSentinelPool都是继承自Pool的。 JedisSentinelSlavePool会生成JedisSentinelSlaveFactory,JedisSentinelSlaveFactory实现了PooledObjectFactory接口,在public PooledObjectmakeObject()方法中,通过sentinel连接,
调用sentinel slaves命令,获取所有可用的slave的ip和port,然后随机的创建一个slave连接并返回。
JedisSentinelSlaveConnectionFactory的createRedisSentinelPool方法
1 |
|
- 通过配置RedisSentinelConfiguration传递sentinel配置和master name给JedisSentinelSlaveConnectionFactory,然后sentinel配置和master name会传递到JedisSentinelSlavePool和JedisSentinelSlaveFactory中
- 创建 JedisSentinelSlavePool,在JedisSentinelSlavePool中启动监听,监听”+switch-master”频道,一旦新master产生,即初始化连接池
- 连接池有JedisSentinelSlaveFactory来代理,JedisSentinelSlaveFactory实现了PooledObjectFactory
在makeObject()中首先根据配置的Sentinel Set找到一个可用的sentinel连接,然后执行sentinel slaves master_name获取所有slave列表
随机选择一个slave创建连接。 如果连接不成功则重试,最大重试5次,依然不能成功创建连接则抛出异常。 - 由图uml-2可知,JedisConnectionFactory实现了InitializingBean,Spring会在Bean初始化之后,调用接口方法void afterPropertiesSet() throws Exception;
在这个方法中创建连接池 - JedisConnectionFactory实现了DisposableBean,会在Spring 容器销毁时,调用public void destroy() 方法销毁连接池
4. 实战
4-1. 工程结构
1. pom.xml
1 |
|
2. JedisSentinelSlaveFactory.java
1 |
|
3. JedisSentinelSlavePool.java
1 |
|
4. JedisSentinelSlaveConFactory.java
1 |
|
4-2. 测试
在应用中,只需配置如下的JedisSentinelSlaveConnectionFactory,Spring Boot会自动配置一个
RedisTemplate redisTemplate和StringRedisTemplate stringRedisTemplate;
在代码中使用@Autowired注入即可。
1 |
|
- RedisConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package com.jack.yin.redis.configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelSlaveConnectionFactory;
@Configuration
public class RedisConfiguration {
@Value("${spring.redis.password}")
private String redisPasswd;
@Bean
public RedisConnectionFactory jedisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.0.1", 26479)
.sentinel("192.168.0.2", 26479)
.sentinel("192.168.0.3", 26479);
sentinelConfig.setPassword(RedisPassword.of(redisPasswd));
JedisClientConfiguration clientConfiguration = JedisClientConfiguration.builder()
.clientName("MyRedisClient")
.build();
JedisConnectionFactory jedisConnectionFactory = new JedisSentinelSlaveConnectionFactory(sentinelConfig,clientConfiguration);
return jedisConnectionFactory;
}
} - DemoApplicationTests.java
1 |
|
4. 总结
优点:
连接池中的连接是随机建立的到所有slave的连接
当监测到master失效转移会自动初始化连接池,确保不会连接到master上
新增slave时可以自动被发现
slave下线会被自动侦测到,然后重新初始化连接池,确保不会连接到已经下线的slave
缺点:
reids slave 需要设置slave-read-only yes
slave同步master数据需要时间,在一个短暂时间内master和slave数据会不一致