原
擴展Redis的Jedis客戶端,哨兵模式讀請求走Slave集羣
2018年12月06日 14:26:45 溫故而知新666 閱讀數 897
版權聲明:本文爲博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處連接和本聲明。
Redis哨兵模式,由Sentinel節點和Redis節點組成,哨兵節點負責監控Redis的健康情況,負責協調Redis主從複製的關係。html
本文不詳細討論Redis哨兵模式,關於哨兵的詳細介紹能夠參考(http://www.javashuo.com/article/p-hcihbkbc-mh.html)java
在使用哨兵模式之後,客戶端不能直接鏈接到Redis集羣,而是鏈接到哨兵集羣,經過哨兵節點獲取Redis主節點(Master)的信息,再進行鏈接,下面給出一小段代碼。redis
-
JedisPoolConfig jedisPoolConfig =
new JedisPoolConfig();
-
jedisPoolConfig.setMaxTotal(
10);
-
jedisPoolConfig.setMaxIdle(
5);
-
jedisPoolConfig.setMinIdle(
5);
-
-
Set<String> sentinels = new HashSet<>(Arrays.asList(
-
-
-
-
-
GenericObjectPoolConfig poolConfig =
new GenericObjectPoolConfig();
-
poolConfig.setMaxTotal(
10);
-
poolConfig.setMaxIdle(
5);
-
poolConfig.setMinIdle(
5);
-
JedisSentinelPool pool =
new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig);
能夠看到,客戶端只配置了哨兵集羣的IP地址,經過哨兵獲取redis主節點信息,再與其進行鏈接,下面給出關鍵代碼的源碼分析,下面代碼片斷講述瞭如何獲取主節點信息。數據庫
-
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
-
-
HostAndPort master =
null;
-
-
boolean sentinelAvailable = false;
-
-
log.info(
"Trying to find master from available Sentinels...");
-
-
-
-
-
for (String sentinel : sentinels) {
-
-
-
final HostAndPort hap = HostAndPort.parseString(sentinel);
-
-
log.info(
"Connecting to Sentinel " + hap);
-
-
-
-
-
jedis =
new Jedis(hap.getHost(), hap.getPort());
-
-
-
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
-
-
-
sentinelAvailable =
true;
-
-
-
if (masterAddr == null || masterAddr.size() != 2) {
-
log.warn(
"Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
-
-
-
-
-
-
master = toHostAndPort(masterAddr);
-
log.info(
"Found Redis master at " + master);
-
-
}
catch (JedisException e) {
-
-
-
-
log.warn(
"Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
throw new JedisException("Can connect to sentinel, but " + masterName
-
+
" seems to be not monitored...");
-
-
-
throw new JedisConnectionException("All sentinels down, cannot determine where is "
-
+ masterName +
" master is running...");
-
-
-
-
log.info(
"Redis master running at " + master + ", starting Sentinel listeners...");
-
-
-
-
for (String sentinel : sentinels) {
-
final HostAndPort hap = HostAndPort.parseString(sentinel);
-
JedisSentinelSlavePool.MasterListener masterListener =
new JedisSentinelSlavePool.MasterListener(masterName, hap.getHost(), hap.getPort());
-
-
masterListener.setDaemon(
true);
-
masterListeners.add(masterListener);
-
-
-
-
-
-
下面的代碼判斷,分析了客戶端如何感知到redis主從節點關係發生變化,原理是經過訂閱哨兵的頻道獲取的,當又新的主節點出現,則清空原有鏈接池,根據新的主節點從新建立鏈接對象。apache
-
-
-
-
-
-
-
-
-
j =
new Jedis(host, port);
-
-
-
-
-
-
-
-
-
j.subscribe(
new JedisPubSub() {
-
-
public void onMessage(String channel, String message) {
-
log.info(
"Sentinel " + host + ":" + port + " published: " + message + ".");
-
-
String[] switchMasterMsg = message.split(
" ");
-
-
if (switchMasterMsg.length > 3) {
-
-
if (masterName.equals(switchMasterMsg[0])) {
-
-
-
-
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[
3], switchMasterMsg[4])));
-
-
log.info(
"Ignoring message on +switch-master for master name "
-
+ switchMasterMsg[
0] + ", our master name is " + masterName);
-
-
-
-
log.warn(
"Invalid message received on Sentinel " + host + ":" + port
-
+
" on channel +switch-master: " + message);
-
-
-
-
-
}
catch (JedisConnectionException e) {
-
-
-
log.info(
"Lost connection to Sentinel at " + host + ":" + port
-
+
". Sleeping 5000ms and retrying.", e);
-
-
-
Thread.sleep(subscribeRetryWaitTimeMillis);
-
}
catch (InterruptedException e1) {
-
log.info(
"Sleep interrupted: ", e1);
-
-
-
log.info(
"Unsubscribing from Sentinel at " + host + ":" + port);
-
-
-
-
-
-
由此咱們知道,Redis的客戶端,在哨兵模式下的實現,讀寫都是走Master,那麼缺點是顯而易見的,那就是若干個Slave徹底變成了熱備,沒有系統分擔壓力,接下來咱們擴展它,讓它支持能夠在Slave節點讀取數據,這樣咱們的程序,在寫入數據時走Master,在讀取數據時走Slave,大大提升了系統的性能。緩存
第一步,咱們重寫這個類 JedisSentinelSlavePool extends Pool<Jedis>負載均衡
全部代碼都拷貝JedisSentinelPool,只修改了下面代碼,建立了JedisSlaveFactory,傳入了哨兵集羣信息,和哨兵的名字。dom
-
private void initPool(HostAndPort master) {
-
if (!master.equals(currentHostMaster)) {
-
currentHostMaster = master;
-
-
factory =
new JedisSlaveFactory(sentinels, masterName, connectionTimeout,
-
soTimeout, password, database, clientName,
false, null, null, null);
-
initPool(poolConfig, factory);
-
-
-
-
-
log.info(
"Created JedisPool to master at " + master);
-
-
第二步,建立JedisSlaveFactory。ide
makeObject這個方法,是Redis鏈接池獲取底層鏈接的地方,我麼只須要在這裏,建立一個鏈接到Slave節點的對象便可,源碼分析
思路就是經過哨兵集羣,獲取到可用的slave節點信息,而後隨機選取一個建立對象,達到負載均衡的效果。
-
package com.framework.core.redis;
-
-
import lombok.extern.slf4j.Slf4j;
-
import org.apache.commons.pool2.PooledObject;
-
import org.apache.commons.pool2.PooledObjectFactory;
-
import org.apache.commons.pool2.impl.DefaultPooledObject;
-
import redis.clients.jedis.BinaryJedis;
-
import redis.clients.jedis.HostAndPort;
-
import redis.clients.jedis.Jedis;
-
import redis.clients.jedis.exceptions.JedisConnectionException;
-
import redis.clients.jedis.exceptions.JedisException;
-
-
import javax.net.ssl.HostnameVerifier;
-
import javax.net.ssl.SSLParameters;
-
import javax.net.ssl.SSLSocketFactory;
-
-
-
-
public class JedisSlaveFactory implements PooledObjectFactory<Jedis> {
-
-
private final Set<String> sentinels;
-
private final String masterName;
-
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 JedisSlaveFactory(final Set<String> sentinels, final String masterName, 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) {
-
this.sentinels = sentinels;
-
this.masterName = masterName;
-
this.connectionTimeout = connectionTimeout;
-
this.soTimeout = soTimeout;
-
this.password = password;
-
this.database = database;
-
this.clientName = clientName;
-
-
this.sslSocketFactory = sslSocketFactory;
-
this.sslParameters = sslParameters;
-
this.hostnameVerifier = hostnameVerifier;
-
this.random = new Random();
-
-
-
-
public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
-
final BinaryJedis jedis = pooledJedis.getObject();
-
if (jedis.getDB() != database) {
-
-
-
-
-
-
-
-
-
public void destroyObject(PooledObject<Jedis> pooledJedis){
-
log.debug(
"destroyObject =" + pooledJedis.getObject());
-
final BinaryJedis jedis = pooledJedis.getObject();
-
if (jedis.isConnected()) {
-
-
-
-
-
-
-
-
-
-
-
-
-
public PooledObject<Jedis> makeObject() {
-
List<HostAndPort> slaves =
this.getAlivedSlaves();
-
-
-
int index = slaves.size() == 1 ? 0 : random.nextInt(slaves.size());
-
final HostAndPort hostAndPort = slaves.get(index);
-
-
log.debug(
"Create jedis instance from slaves=[" + slaves + "] , choose=[" + hostAndPort + "]");
-
-
-
final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
-
soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
-
-
-
-
-
if (null != this.password) {
-
jedis.auth(
this.password);
-
-
-
-
-
if (clientName != null) {
-
jedis.clientSetname(clientName);
-
-
}
catch (JedisException je) {
-
-
-
-
-
return new DefaultPooledObject<Jedis>(jedis);
-
-
-
-
-
-
-
private List<HostAndPort> getAlivedSlaves() {
-
log.debug(
"Get alived salves start...");
-
-
List<HostAndPort> alivedSalaves =
new ArrayList<>();
-
boolean sentinelAvailable = false;
-
-
-
-
for (String sentinel : sentinels) {
-
final HostAndPort hap = HostAndPort.parseString(sentinel);
-
-
log.debug(
"Connecting to Sentinel " + hap);
-
-
-
-
jedis =
new Jedis(hap.getHost(), hap.getPort());
-
-
List<Map<String, String>> slavesInfo = jedis.sentinelSlaves(masterName);
-
-
-
sentinelAvailable =
true;
-
-
-
if (slavesInfo == null || slavesInfo.size() == 0) {
-
log.warn(
"Cannot get slavesInfo, master name: " + masterName + ". Sentinel: " + hap
-
-
-
-
-
-
for (Map<String, String> slave : slavesInfo) {
-
if(slave.get("flags").equals("slave")) {
-
String host = slave.get(
"ip");
-
int port = Integer.valueOf(slave.get("port"));
-
HostAndPort hostAndPort =
new HostAndPort(host, port);
-
-
log.info(
"Found alived redis slave:[" + hostAndPort + "]");
-
-
alivedSalaves.add(hostAndPort);
-
-
-
-
log.debug(
"Get alived salves end...");
-
-
}
catch (JedisException e) {
-
-
log.warn(
"Cannot get slavesInfo from sentinel running @ " + hap + ". Reason: " + e
-
-
-
-
-
-
-
-
-
-
if (alivedSalaves.isEmpty()) {
-
-
throw new JedisException("Can connect to sentinel, but " + masterName
-
+
" cannot find any redis slave");
-
-
throw new JedisConnectionException("All sentinels down");
-
-
-
-
-
-
-
-
public void passivateObject(PooledObject<Jedis> pooledJedis) {
-
-
-
-
-
-
-
-
-
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
-
final BinaryJedis jedis = pooledJedis.getObject();
-
-
-
boolean result = jedis.isConnected()
-
&& jedis.ping().equals(
"PONG")
-
&& jedis.info(
"Replication").contains("role:slave");
-
-
log.debug(
"ValidateObject Jedis=["+jedis+"] host=[ " + jedis.getClient().getHost() +
-
"] port=[" + jedis.getClient().getPort() +"] return=[" + result + "]");
-
-
}
catch (final Exception e) {
-
log.warn(
"ValidateObject error jedis client cannot use", e);
-
-
-
-
-
使用的時候跟原來同樣,建立slave鏈接池就能夠了。
-
JedisPoolConfig jedisPoolConfig =
new JedisPoolConfig();
-
-
jedisPoolConfig.setMaxTotal(
100);
-
-
jedisPoolConfig.setMaxIdle(
1);
-
-
jedisPoolConfig.setMinIdle(
1);
-
-
jedisPoolConfig.setMaxWaitMillis(
3000);
-
-
jedisPoolConfig.setTestWhileIdle(
true);
-
-
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(
30000);
-
-
-
-
-
jedisPoolConfig.setMinEvictableIdleTimeMillis(
60000);
-
-
jedisPoolConfig.setNumTestsPerEvictionRun(
10);
-
-
-
jedisPoolConfig.setTestOnBorrow(
false);
-
-
jedisPoolConfig.setTestOnReturn(
false);
-
-
Set<String> sentinels = new HashSet<>(Arrays.asList(
-
-
-
-
-
-
JedisSentinelSlavePool pool =
new JedisSentinelSlavePool("mymaster", sentinels, jedisPoolConfig);
與Spring集成,分別建立不一樣的對象便可,在程序中查詢接口能夠先走slave進行查詢,查詢不到在查詢master, master也沒有則寫入緩存,返回數據,下載在查詢slave就同步過去啦,這樣一來redis的性能會大幅度的提高。
-
-
@Bean(name =
"redisTemplateMaster")
-
public RedisTemplate<Object, Object> redisTemplateMaster() {
-
RedisTemplate<Object, Object>
template = new RedisTemplate<>();
-
template.setConnectionFactory(redisMasterConnectionFactory());
-
template.setKeySerializer(new StringRedisSerializer());
-
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-
-
-
-
@Bean(name =
"redisTemplateSlave")
-
public RedisTemplate<Object, Object> redisTemplateSlave() {
-
RedisTemplate<Object, Object>
template = new RedisTemplate<>();
-
template.setConnectionFactory(redisSlaveConnectionFactory());
-
template.setKeySerializer(new StringRedisSerializer());
-
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-
-