DATAREDIS-324 - Add Redis Sentinel support.

We’ve added RedisSentinelConfiguration holding required information for connecting to redis sentinels. This can be used to set up ConnectionFeactory for HA environments.

**Using Jedis**
Providing RedisSentinelConfiguration will force the JedisConnectionFactory to use JedisSentinelPool for managing resources.

**Using Lettuce/JRedis/SRP**
There’s currently no support for sentinel in those clients.

**CI Build**
We’ve added makefile to build and set up redis instances for testing sentinel support on travis-ci. There’s already a section for redis cluster.
The cluster section is for whatever reason currently not working as the cluster nodes won’t start.
This will be fixed when we add redis-cluster support.

_side note:_ there’s an alternative fork of lettuce at mp911de/lettuce that already has sentinel support.
This commit is contained in:
Christoph Strobl
2014-07-15 09:22:04 +02:00
committed by Thomas Darimont
parent c36a58da7f
commit 55ff415a71
13 changed files with 898 additions and 12 deletions

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
/**
* @author Christoph Strobl
* @since 1.4
*/
public interface NamedNode {
String getName();
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @since 1.4
*/
public class RedisNode {
private final String host;
private final int port;
public RedisNode(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() {
return host;
}
public Integer getPort() {
return port;
}
public String asString() {
return host + ":" + port;
}
@Override
public String toString() {
return asString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(host);
result = prime * result + ObjectUtils.nullSafeHashCode(port);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !(obj instanceof RedisNode)) {
return false;
}
RedisNode other = (RedisNode) obj;
if (!ObjectUtils.nullSafeEquals(this.host, other.host)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.port, other.port)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.util.Assert;
/**
* Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} using connecting
* to <a href="http://redis.io/topics/sentinel">Redis Sentinel(s)</a>. Useful when setting up a high availability Redis
* environment.
*
* @author Christoph Strobl
* @since 1.4
*/
public class RedisSentinelConfiguration {
private NamedNode master;
private Set<RedisNode> sentinels;
/**
* Creates new {@link RedisSentinelConfiguration}.
*/
public RedisSentinelConfiguration() {
this.sentinels = new LinkedHashSet<RedisNode>();
}
/**
* Set {@literal Sentinels} to connect to.
*
* @param sentinels must not be {@literal null}.
*/
public void setSentinels(Iterable<RedisNode> sentinels) {
Assert.notNull(sentinels, "Cannot set sentinels to 'null'.");
this.sentinels.clear();
for (RedisNode sentinel : sentinels) {
addSentinel(sentinel);
}
}
/**
* Returns an {@link Collections#unmodifiableSet(Set)} of {@literal Sentinels}.
*
* @return {@link Set} of sentinels. Never {@literal null}.
*/
public Set<RedisNode> getSentinels() {
return Collections.unmodifiableSet(sentinels);
}
/**
* Add sentinel.
*
* @param sentinel must not be {@literal null}.
*/
public void addSentinel(RedisNode sentinel) {
Assert.notNull(sentinel, "Sentinel must not be 'null'.");
this.sentinels.add(sentinel);
}
/**
* Set the master node via its name.
*
* @param name must not be {@literal null}.
*/
public void setMaster(final String name) {
Assert.notNull(name, "Name of sentinel master must not be null.");
setMaster(new NamedNode() {
@Override
public String getName() {
return name;
}
});
}
/**
* Set the master.
*
* @param master must not be {@literal null}.
*/
public void setMaster(NamedNode master) {
Assert.notNull("Sentinel master node must not be 'null'.");
this.master = master;
}
/**
* Get the {@literal Sentinel} master node.
*
* @return
*/
public NamedNode getMaster() {
return master;
}
/**
* @see #setMaster(String)
* @param master
* @return
*/
public RedisSentinelConfiguration master(String master) {
this.setMaster(master);
return this;
}
/**
* @see #setMaster(NamedNode)
* @param master
* @return
*/
public RedisSentinelConfiguration master(NamedNode master) {
this.setMaster(master);
return this;
}
/**
* @see #addSentinel(RedisNode)
* @param sentinel
* @return
*/
public RedisSentinelConfiguration sentinel(RedisNode sentinel) {
this.addSentinel(sentinel);
return this;
}
/**
* @see #sentinel(RedisNode)
* @param host
* @param port
* @return
*/
public RedisSentinelConfiguration sentinel(String host, Integer port) {
return sentinel(new RedisNode(host, port));
}
}

View File

@@ -16,6 +16,11 @@
package org.springframework.data.redis.connection.jedis;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
@@ -26,20 +31,26 @@ import org.springframework.data.redis.PassThroughExceptionTranslationStrategy;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.Protocol;
import redis.clients.util.Pool;
/**
* Connection factory creating <a href="http://github.com/xetorthio/jedis">Jedis</a> based connections.
*
* @author Costin Leau
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory {
@@ -53,10 +64,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
private int timeout = Protocol.DEFAULT_TIMEOUT;
private String password;
private boolean usePool = true;
private JedisPool pool = null;
private Pool<Jedis> pool;
private JedisPoolConfig poolConfig = new JedisPoolConfig();
private int dbIndex = 0;
private boolean convertPipelineAndTxResults = true;
private RedisSentinelConfiguration sentinelConfig;
/**
* Constructs a new <code>JedisConnectionFactory</code> instance with default settings (default connection pooling, no
@@ -80,7 +92,31 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
* @param poolConfig pool configuration
*/
public JedisConnectionFactory(JedisPoolConfig poolConfig) {
this.poolConfig = poolConfig;
this(null, poolConfig);
}
/**
* Constructs a new {@link JedisConnectionFactory} instance using the given {@link JedisPoolConfig} applied to
* {@link JedisSentinelPool}.
*
* @param sentinelConfig
* @since 1.4
*/
public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig) {
this(sentinelConfig, null);
}
/**
* Constructs a new {@link JedisConnectionFactory} instance using the given {@link JedisPoolConfig} applied to
* {@link JedisSentinelPool}.
*
* @param sentinelConfig
* @param poolConfig pool configuration. Defaulted to new instance if {@literal null}.
* @since 1.4
*/
public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) {
this.sentinelConfig = sentinelConfig;
this.poolConfig = poolConfig != null ? poolConfig : new JedisPoolConfig();
}
/**
@@ -114,6 +150,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
return connection;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() {
if (shardInfo == null) {
shardInfo = new JedisShardInfo(hostName, port);
@@ -128,11 +168,46 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
}
if (usePool) {
pool = new JedisPool(poolConfig, shardInfo.getHost(), shardInfo.getPort(), shardInfo.getTimeout(),
shardInfo.getPassword());
this.pool = createPool();
}
}
private Pool<Jedis> createPool() {
if (isRedisSentinelAware()) {
return createRedisSentinelPool(this.sentinelConfig);
}
return createRedisPool();
}
/**
* Creates {@link JedisSentinelPool}.
*
* @param config
* @return
* @since 1.4
*/
protected Pool<Jedis> createRedisSentinelPool(RedisSentinelConfiguration config) {
return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()),
getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(), getShardInfo().getTimeout(), getShardInfo()
.getPassword());
}
/**
* Creates {@link JedisPool}.
*
* @return
* @since 1.4
*/
protected Pool<Jedis> createRedisPool() {
return new JedisPool(getPoolConfig(), getShardInfo().getHost(), getShardInfo().getPort(), getShardInfo()
.getTimeout(), getShardInfo().getPassword());
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() {
if (usePool && pool != null) {
try {
@@ -144,6 +219,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionFactory#getConnection()
*/
public JedisConnection getConnection() {
Jedis jedis = fetchJedisConnector();
JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis,
@@ -152,6 +231,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
return postProcessConnection(connection);
}
/*
* (non-Javadoc)
* @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException)
*/
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return EXCEPTION_TRANSLATION.translate(ex);
}
@@ -321,4 +404,28 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) {
this.convertPipelineAndTxResults = convertPipelineAndTxResults;
}
/**
* @return true when {@link RedisSentinelConfiguration} is present.
* @since 1.4
*/
public boolean isRedisSentinelAware() {
return sentinelConfig != null;
}
private Set<String> convertToJedisSentinelSet(Collection<RedisNode> nodes) {
if (CollectionUtils.isEmpty(nodes)) {
return Collections.emptySet();
}
Set<String> convertedNodes = new LinkedHashSet<String>(nodes.size());
for (RedisNode node : nodes) {
if (node != null) {
convertedNodes.add(node.asString());
}
}
return convertedNodes;
}
}