阅读更多
更好阅读体验,请移步: http://www.jack-yin.com/coding/spring-boot/2683.html
0. 背景
Reids除了配置集群实现高可用之外,对于单机版的Redis,可以通过Master-Slave架构,配合使用Sentinel机制实现高可用架构,
同时客户端可以实现自动失效转移。
类似于JdbcTemplate,Spring中使用RedisTemplate来操作Redis。Spring Boot中只需引入如下Maven依赖,即可自动配置
一个RedisTemplate实例。
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0
RedisTemplate需要一个RedisConnectionFactory来管理Redis连接。 可以在项目中定义一个RedisSentinelConfiguration给
RedisConnectionFactory,即可生成一个基于Sentinel的连接池,并且实现了自动失效转移:当master失效时,Sentinel自动提升一个slave
成为master保证Redis的master连接高可用。
下面是基于Sentinel的RedisConnectionFactory的典型配置
@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));
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
System.out.println(jedisConnectionFactory.getClientConfiguration().getClientName());
return jedisConnectionFactory;
}
查看 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-only yes,让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来实现的。
[-----------------UML-1---------------------------]
由图看到,JedisConnectionFactory创建一个JedisSentinelPool,JedisSentinelPool创建JedisFactory,JedisFactory实现了PooledObjectFactory接口
,在MakeObject()方法中产生新的Redis连接。 在JedisSentinelPool中定义MasterListener还订阅+switch-master频道,一旦发生Master转移事件,自动作失效转移
重新初始化master连接池。
3. 解决问题
模仿JedisConnectionFactory,JedisSentinelPool,和JedisFactory, 创建JedisSentinelSlaveConnectionFactory,JedisSentinelSlavePool和JedisSentinelSlaveFactory
它们之间的关系,如图UML-2所示。
[-----------------UML-2---------------------------]
其中,JedisSentinelSlaveConnectionFactory就是可以传递给RedisTemplate的。JedisSentinelSlaveConnectionFactory继承自JedisConnectionFactory
并且覆盖了createRedisSentinelPool方法,在JedisConnectionFactory中,该方法会返回一个JedisSentinelPool,而新的方法会返回JedisSentinelSlavePool。
JedisSentinelSlavePool和JedisSentinelPool都是继承自Pool的。 JedisSentinelSlavePool会生成JedisSentinelSlaveFactory,
JedisSentinelSlaveFactory实现了PooledObjectFactory接口,在public PooledObject makeObject()方法中,通过sentinel连接,
调用sentinel slaves命令,获取所有可用的slave的ip和port,然后随机的创建一个slave连接并返回。
JedisSentinelSlaveConnectionFactory的createRedisSentinelPool方法
@Override
protected Pool createRedisSentinelPool(RedisSentinelConfiguration config){
GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig();
return new JedisSentinelSlavePool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()),
poolConfig, getConnectTimeout(), getReadTimeout(), getPassword(), getDatabase(), getClientName());
}
1) 通过配置RedisSentinelConfiguration传递sentinel配置和master name给JedisSentinelSlaveConnectionFactory,然后sentinel配置和master name
会传递到JedisSentinelSlavePool和JedisSentinelSlaveFactory中
2)创建 JedisSentinelSlavePool,在JedisSentinelSlavePool中启动监听,监听"+switch-master"频道,一旦新master产生,即初始化连接池
3) 连接池有JedisSentinelSlaveFactory来代理,JedisSentinelSlaveFactory实现了PooledObjectFactory
在makeObject()中首先根据配置的Sentinel Set找到一个可用的sentinel连接,然后执行sentinel slaves master_name获取所有slave列表
随机选择一个slave创建连接。 如果连接不成功则重试,最大重试5次,依然不能成功创建连接则抛出异常。
4) 由图uml-2可知,JedisConnectionFactory实现了InitializingBean,Spring会在Bean初始化之后,调用接口方法void afterPropertiesSet() throws Exception;
在这个方法中创建连接池
5) JedisConnectionFactory实现了DisposableBean,会在Spring 容器销毁时,调用public void destroy() 方法销毁连接池
6)
4 实战
4.1 redis-sentinel-slave-connection-factory 工程结构
1) pom文件
---------------------------pom.xml-------------------------------------------------
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.jack.yin
redis-sentinel-slave-connection-factory
1.0-SNAPSHOT
spring-boot-starter-redis-readonly-connection-factory
http://www.example.com
UTF-8
1.8
1.8
org.springframework.boot
spring-boot-starter-data-redis
2.0.1.RELEASE
redis.clients
jedis
2.9.0
junit
junit
4.12
test
maven-clean-plugin
3.0.0
maven-resources-plugin
3.0.2
maven-compiler-plugin
3.7.0
maven-surefire-plugin
2.20.1
maven-jar-plugin
3.0.2
maven-install-plugin
2.5.2
maven-deploy-plugin
2.8.2
2) JedisSentinelSlaveFactory.java
----------------------JedisSentinelSlaveFactory.java----------------------
package redis.clients.jedis;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import redis.clients.jedis.exceptions.InvalidURIException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.util.JedisURIHelper;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocketFactory;
import java.net.URI;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public class JedisSentinelSlaveFactory implements PooledObjectFactory {
private final String masterName;
private final int retryTimeWhenRetrieveSlave = 5;
private final AtomicReference hostAndPortOfASentinel = new AtomicReference();
private final int connectionTimeout;
private final int soTimeout;
private final String password;
private final int database;
private final String clientName;
private final boolean ssl;
private final SSLSocketFactory sslSocketFactory;
private SSLParameters sslParameters;
private HostnameVerifier hostnameVerifier;
public JedisSentinelSlaveFactory(final String host, final int port, final int connectionTimeout,
final int soTimeout, final String password, final int database, final String clientName,
final boolean ssl, final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters,
final HostnameVerifier hostnameVerifier,String masterName) {
this.hostAndPortOfASentinel.set(new HostAndPort(host, port));
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = password;
this.database = database;
this.clientName = clientName;
this.ssl = ssl;
this.sslSocketFactory = sslSocketFactory;
this.sslParameters = sslParameters;
this.hostnameVerifier = hostnameVerifier;
this.masterName = masterName;
}
public JedisSentinelSlaveFactory(final URI uri, final int connectionTimeout, final int soTimeout,
final String clientName, final boolean ssl, final SSLSocketFactory sslSocketFactory,
final SSLParameters sslParameters, final HostnameVerifier hostnameVerifier,String masterName) {
if (!JedisURIHelper.isValid(uri)) {
throw new InvalidURIException(String.format(
"Cannot open Redis connection due invalid URI. %s", uri.toString()));
}
this.hostAndPortOfASentinel.set(new HostAndPort(uri.getHost(), uri.getPort()));
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = JedisURIHelper.getPassword(uri);
this.database = JedisURIHelper.getDBIndex(uri);
this.clientName = clientName;
this.ssl = ssl;
this.sslSocketFactory = sslSocketFactory;
this.sslParameters = sslParameters;
this.hostnameVerifier = hostnameVerifier;
this.masterName = masterName;
}
public void setHostAndPortOfASentinel(final HostAndPort hostAndPortOfASentinel) {
this.hostAndPortOfASentinel.set(hostAndPortOfASentinel);
}
@Override
public void activateObject(PooledObject pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.getDB() != database) {
jedis.select(database);
}
}
@Override
public void destroyObject(PooledObject pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.isConnected()) {
try {
try {
jedis.quit();
} catch (Exception e) {
}
jedis.disconnect();
} catch (Exception e) {
}
}
}
@Override
public PooledObject makeObject() throws Exception {
final Jedis jedisSentinel = getASentinel();
List