diff --git a/README.md b/README.md
index 875379cd..fd34ddcb 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,11 @@ We have separate folders for the samples of individual modules:
* `starbucks` - A sample REST web-service built with Spring Data REST and MongoDB.
* `multi-store` - A sample REST web-service based on both Spring Data JPA and Spring Data MongoDB.
+## Spring Data Redis
+
+* `example` - Example for basic Sprign Data Redis setup.
+* `cluster-sentinel` - Example for Redis cluster and Sentinel support.
+
## Miscellaneous
* `multi-store` - Example project to use both Spring Data MongoDB and Spring Data JPA in one project.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 2f341b5a..db38bc55 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,6 +20,7 @@
jpa
mongodb
rest
+ redis
diff --git a/redis/cluster-sentinel/README.md b/redis/cluster-sentinel/README.md
new file mode 100644
index 00000000..fa60d06a
--- /dev/null
+++ b/redis/cluster-sentinel/README.md
@@ -0,0 +1,28 @@
+# Spring Data Redis - Sentinel and Cluster Examples
+
+This project contains samples of Sentinel and CLuster specific features of Spring Data Redis.
+
+## Support for Sentinel
+
+```java
+@Configuration
+public class RedisSentinelApplicationConfig {
+
+ static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration().master("mymaster") //
+ .sentinel("localhost", 26379) //
+ .sentinel("localhost", 26380) //
+ .sentinel("localhost", 26381);
+
+ @Bean
+ public RedisConnectionFactory connectionFactory() {
+ return new JedisConnectionFactory(sentinelConfig());
+ }
+
+ @Bean
+ public RedisSentinelConfiguration sentinelConfig() {
+ return SENTINEL_CONFIG;
+ }
+
+}
+```
+
diff --git a/redis/cluster-sentinel/pom.xml b/redis/cluster-sentinel/pom.xml
new file mode 100644
index 00000000..83ceecbd
--- /dev/null
+++ b/redis/cluster-sentinel/pom.xml
@@ -0,0 +1,29 @@
+
+ 4.0.0
+
+ spring-data-redis-cluster-sentinel-example
+ Spring Data Redis - Cluster/Sentinel Example
+
+
+ org.springframework.data.examples
+ spring-data-redis-examples
+ 1.0.0.BUILD-SNAPSHOT
+ ../pom.xml
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-redis
+
+
+ ${project.groupId}
+ spring-data-redis-example-utils
+ ${project.version}
+ test
+
+
+
+
\ No newline at end of file
diff --git a/redis/cluster-sentinel/src/test/java/example/springdata/redis/sentinel/RedisSentinelApplicationConfig.java b/redis/cluster-sentinel/src/test/java/example/springdata/redis/sentinel/RedisSentinelApplicationConfig.java
new file mode 100644
index 00000000..79c476dc
--- /dev/null
+++ b/redis/cluster-sentinel/src/test/java/example/springdata/redis/sentinel/RedisSentinelApplicationConfig.java
@@ -0,0 +1,105 @@
+/*
+ * 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 example.springdata.redis.sentinel;
+
+import javax.annotation.PreDestroy;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+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.RedisSentinelConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.util.StopWatch;
+
+/**
+ * @author Christoph Strobl
+ */
+@Configuration
+public class RedisSentinelApplicationConfig {
+
+ static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration().master("mymaster") //
+ .sentinel("localhost", 26379) //
+ .sentinel("localhost", 26380) //
+ .sentinel("localhost", 26381);
+
+ @Autowired RedisConnectionFactory factory;
+
+ public static void main(String[] args) throws Exception {
+
+ ApplicationContext context = SpringApplication.run(RedisSentinelApplicationConfig.class, args);
+
+ RedisConnectionFactory factory = context.getBean(RedisConnectionFactory.class);
+
+ StringRedisTemplate template = new StringRedisTemplate();
+ template.setConnectionFactory(factory);
+ template.afterPropertiesSet();
+
+ template.opsForValue().set("loop-forever", "0");
+
+ StopWatch stopWatch = new StopWatch();
+
+ while (true) {
+
+ try {
+ String value = "IT:= " + template.opsForValue().increment("loop-forever", 1);
+ printBackFromErrorStateInfoIfStopWatchIsRunning(stopWatch);
+ System.out.println(value);
+ } catch (RuntimeException e) {
+ System.err.println(e.getCause().getMessage());
+ startStopWatchIfNotRunning(stopWatch);
+ }
+
+ Thread.sleep(1000);
+ }
+ }
+
+ @Bean
+ public RedisConnectionFactory connectionFactory() {
+ return new JedisConnectionFactory(sentinelConfig());
+ }
+
+ @Bean
+ public RedisSentinelConfiguration sentinelConfig() {
+ return SENTINEL_CONFIG;
+ }
+
+ /**
+ * Clear database before shut down.
+ */
+ @PreDestroy
+ public void flushTestDb() {
+ factory.getConnection().flushDb();
+ }
+
+ private static void startStopWatchIfNotRunning(StopWatch stopWatch) {
+
+ if (!stopWatch.isRunning()) {
+ stopWatch.start();
+ }
+ }
+
+ private static void printBackFromErrorStateInfoIfStopWatchIsRunning(StopWatch stopWatch) {
+
+ if (stopWatch.isRunning()) {
+ stopWatch.stop();
+ System.err.println("INFO: Recovered after: " + stopWatch.getLastTaskInfo().getTimeSeconds());
+ }
+ }
+}
diff --git a/redis/cluster-sentinel/src/test/resources/logback.xml b/redis/cluster-sentinel/src/test/resources/logback.xml
new file mode 100644
index 00000000..06e36318
--- /dev/null
+++ b/redis/cluster-sentinel/src/test/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/redis/example/README.md b/redis/example/README.md
new file mode 100644
index 00000000..f77b1f5a
--- /dev/null
+++ b/redis/example/README.md
@@ -0,0 +1,4 @@
+# Spring Data Redis - Examples
+
+This project contains samples of specific features of Spring Data Redis.
+
diff --git a/redis/example/pom.xml b/redis/example/pom.xml
new file mode 100644
index 00000000..a0bff9c9
--- /dev/null
+++ b/redis/example/pom.xml
@@ -0,0 +1,31 @@
+
+ 4.0.0
+
+ spring-data-redis-example
+ Spring Data Redis - Example
+
+
+ org.springframework.data.examples
+ spring-data-redis-examples
+ 1.0.0.BUILD-SNAPSHOT
+ ../pom.xml
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-redis
+
+
+
+ ${project.groupId}
+ spring-data-redis-example-utils
+ ${project.version}
+ test
+
+
+
+
+
\ No newline at end of file
diff --git a/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java b/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java
new file mode 100644
index 00000000..406fea7c
--- /dev/null
+++ b/redis/example/src/test/java/example/springdata/redis/RedisTestConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * 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 example.springdata.redis;
+
+import javax.annotation.PreDestroy;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+
+/**
+ * @author Christoph Strobl
+ */
+@Configuration
+@EnableAutoConfiguration
+public class RedisTestConfiguration {
+
+ @Autowired RedisConnectionFactory factory;
+
+ /**
+ * Clear database before shut down.
+ */
+ @PreDestroy
+ public void flushTestDb() {
+ factory.getConnection().flushDb();
+ }
+}
diff --git a/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java b/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java
new file mode 100644
index 00000000..93284e45
--- /dev/null
+++ b/redis/example/src/test/java/example/springdata/redis/commands/KeyOperationsTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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 example.springdata.redis.commands;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import example.springdata.redis.RedisTestConfiguration;
+import example.springdata.redis.test.util.RequiresRedisServer;
+
+/**
+ * Show usage of operations on redis keys using low level API provided by {@link RedisConnection}.
+ *
+ * @author Christoph Strobl
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = { RedisTestConfiguration.class })
+public class KeyOperationsTests {
+
+ // we only want to run this tests when redis is up an running
+ public static @ClassRule RequiresRedisServer requiresServer = RequiresRedisServer.onLocalhost();
+
+ private static final String PREFIX = KeyOperationsTests.class.getSimpleName();
+ private static final String KEY_PATTERN = PREFIX + "*";
+
+ @Autowired RedisConnectionFactory connectionFactory;
+
+ private RedisConnection connection;
+ private RedisSerializer serializer = new StringRedisSerializer();
+
+ @Before
+ public void setUp() {
+ this.connection = connectionFactory.getConnection();
+ }
+
+ /**
+ * Uses {@code KEYS} command for loading all matching keys.
+ * Note that {@code KEYS} is a blocking command that potentially might affect other operations execution time.
+ * All keys will be loaded within one single operation.
+ */
+ @Test
+ public void iterateOverKeysMatchingPrefixUsingKeysCommand() {
+
+ generateRandomKeys(1000);
+
+ Set keys = this.connection.keys(serializer.serialize(KEY_PATTERN));
+ printKeys(keys.iterator());
+ }
+
+ /**
+ * Uses {@code SCAN} command for loading all matching keys.
+ * {@code SCAN} uses a cursor on server side returning only a subset of the available data with the possibility to
+ * ripple load further elements using the cursors position.
+ * All keys will be loaded using multiple operations.
+ */
+ @Test
+ public void iterateOverKeysMatchingPrefixUsingScanCommand() {
+
+ generateRandomKeys(1000);
+
+ Cursor cursor = this.connection.scan(ScanOptions.scanOptions().match(KEY_PATTERN).build());
+ printKeys(cursor);
+ }
+
+ private void printKeys(Iterator keys) {
+
+ int i = 0;
+ while (keys.hasNext()) {
+ System.out.println(new String(keys.next()));
+ i++;
+ }
+ System.out.println(String.format("Total No. found: %s", i));
+ }
+
+ private void generateRandomKeys(int nrKeys) {
+
+ for (int i = 0; i < nrKeys; i++) {
+ this.connection.set((PREFIX + "-" + i).getBytes(), UUID.randomUUID().toString().getBytes());
+ }
+ }
+}
diff --git a/redis/example/src/test/resources/logback.xml b/redis/example/src/test/resources/logback.xml
new file mode 100644
index 00000000..61c86dd9
--- /dev/null
+++ b/redis/example/src/test/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/redis/pom.xml b/redis/pom.xml
new file mode 100644
index 00000000..01bdbfa7
--- /dev/null
+++ b/redis/pom.xml
@@ -0,0 +1,38 @@
+
+ 4.0.0
+
+ spring-data-redis-examples
+ pom
+
+
+ org.springframework.data.examples
+ spring-data-examples
+ 1.0.0.BUILD-SNAPSHOT
+
+
+ Spring Data Redis - Examples
+ Sample projects for Spring Data Redis
+
+
+ util
+ cluster-sentinel
+ example
+
+
+
+
+
+ joda-time
+ joda-time
+
+
+
+ redis.clients
+ jedis
+ 2.5.2
+
+
+
+
+
\ No newline at end of file
diff --git a/redis/util/pom.xml b/redis/util/pom.xml
new file mode 100644
index 00000000..bb99a097
--- /dev/null
+++ b/redis/util/pom.xml
@@ -0,0 +1,26 @@
+
+ 4.0.0
+
+ spring-data-redis-example-utils
+ Spring Data Redis - Example utilities
+
+
+ org.springframework.data.examples
+ spring-data-redis-examples
+ 1.0.0.BUILD-SNAPSHOT
+ ../pom.xml
+
+
+
+
+ junit
+ junit
+
+
+ org.springframework.boot
+ spring-boot-starter-redis
+
+
+
+
\ No newline at end of file
diff --git a/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisSentinel.java b/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisSentinel.java
new file mode 100644
index 00000000..83631663
--- /dev/null
+++ b/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisSentinel.java
@@ -0,0 +1,183 @@
+/*
+ * 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 example.springdata.redis.test.util;
+
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.springframework.data.redis.connection.RedisNode;
+import org.springframework.data.redis.connection.RedisSentinelConfiguration;
+
+import redis.clients.jedis.Jedis;
+
+/**
+ * @author Christoph Strobl
+ */
+public class RequiresRedisSentinel implements TestRule {
+
+ public enum SentinelsAvailable {
+ ALL_ACTIVE, ONE_ACTIVE, NONE_ACTIVE
+ }
+
+ private static final RedisSentinelConfiguration DEFAULT_SENTINEL_CONFIG = new RedisSentinelConfiguration()
+ .master("mymaster").sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380).sentinel("127.0.0.1", 26381);
+
+ private RedisSentinelConfiguration sentinelConfig;
+ private SentinelsAvailable requiredSentinels;
+
+ protected RequiresRedisSentinel(RedisSentinelConfiguration config) {
+ this.sentinelConfig = config;
+ }
+
+ /**
+ * Create new {@link RedisSentinelRule} for given {@link RedisSentinelConfiguration}.
+ *
+ * @param config
+ * @return
+ */
+ public static RequiresRedisSentinel forConfig(RedisSentinelConfiguration config) {
+ return new RequiresRedisSentinel(config != null ? config : DEFAULT_SENTINEL_CONFIG);
+ }
+
+ /**
+ * Create new {@link RedisSentinelRule} using default configuration.
+ *
+ * @return
+ */
+ public static RequiresRedisSentinel withDefaultConfig() {
+ return new RequiresRedisSentinel(DEFAULT_SENTINEL_CONFIG);
+ }
+
+ public RequiresRedisSentinel sentinelsDisabled() {
+
+ this.requiredSentinels = SentinelsAvailable.NONE_ACTIVE;
+ return this;
+ }
+
+ /**
+ * Verifies all {@literal Sentinel} nodes are available.
+ *
+ * @return
+ */
+ public RequiresRedisSentinel allActive() {
+
+ this.requiredSentinels = SentinelsAvailable.ALL_ACTIVE;
+ return this;
+ }
+
+ /**
+ * Verifies at least one {@literal Sentinel} node is available.
+ *
+ * @return
+ */
+ public RequiresRedisSentinel oneActive() {
+
+ this.requiredSentinels = SentinelsAvailable.ONE_ACTIVE;
+ return this;
+ }
+
+ /**
+ * Will only check {@link RedisSentinelConfiguration} configuration in case {@link RequiresRedisSentinel} is detected
+ * on test method.
+ *
+ * @return
+ */
+ public RequiresRedisSentinel dynamicModeSelection() {
+ this.requiredSentinels = null;
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.rules.TestRule#apply(org.junit.runners.model.Statement, org.junit.runner.Description)
+ */
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+
+ if (description.isTest()) {
+ if (RequiresRedisSentinel.this.requiredSentinels != null) {
+ verify(RequiresRedisSentinel.this.requiredSentinels);
+ }
+
+ } else {
+ verify(RequiresRedisSentinel.this.requiredSentinels);
+ }
+
+ base.evaluate();
+ }
+ };
+ }
+
+ private void verify(SentinelsAvailable verificationMode) {
+
+ int failed = 0;
+ for (RedisNode node : sentinelConfig.getSentinels()) {
+ if (!isAvailable(node)) {
+ failed++;
+ }
+ }
+
+ if (failed > 0) {
+ if (SentinelsAvailable.ALL_ACTIVE.equals(verificationMode)) {
+ throw new AssumptionViolatedException(String.format(
+ "Expected all Redis Sentinels to respone but %s of %s did not responde", failed, sentinelConfig
+ .getSentinels().size()));
+ }
+
+ if (SentinelsAvailable.ONE_ACTIVE.equals(verificationMode) && sentinelConfig.getSentinels().size() - 1 < failed) {
+ throw new AssumptionViolatedException(
+ "Expected at least one sentinel to respond but it seems all are offline - Game Over!");
+ }
+ }
+
+ if (SentinelsAvailable.NONE_ACTIVE.equals(verificationMode) && failed != sentinelConfig.getSentinels().size()) {
+ throw new AssumptionViolatedException(String.format(
+ "Expected to have no sentinels online but found that %s are still alive.", (sentinelConfig.getSentinels()
+ .size() - failed)));
+ }
+ }
+
+ private boolean isAvailable(RedisNode node) {
+
+ Jedis jedis = null;
+
+ try {
+ jedis = new Jedis(node.getHost(), node.getPort());
+ jedis.connect();
+ jedis.ping();
+ } catch (Exception e) {
+ return false;
+ } finally {
+
+ if (jedis != null) {
+ try {
+ jedis.disconnect();
+ jedis.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisServer.java b/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisServer.java
new file mode 100644
index 00000000..c0472323
--- /dev/null
+++ b/redis/util/src/main/java/example/springdata/redis/test/util/RequiresRedisServer.java
@@ -0,0 +1,62 @@
+/*
+ * 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 example.springdata.redis.test.util;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.rules.ExternalResource;
+
+/**
+ * Implementation of junit rule {@link ExternalResource} to verify Redis (or at least something on the defined host and
+ * port) is up and running.
+ *
+ * @author Christoph Strobl
+ */
+public class RequiresRedisServer extends ExternalResource {
+
+ private int timeout = 30;
+
+ private final String host;
+ private final int port;
+
+ private RequiresRedisServer(String host, int port) {
+
+ this.host = host;
+ this.port = port;
+ }
+
+ public static RequiresRedisServer onLocalhost() {
+ return new RequiresRedisServer("localhost", 6379);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.junit.rules.ExternalResource#before()
+ */
+ @Override
+ protected void before() throws Throwable {
+
+ try (Socket socket = new Socket()) {
+ socket.setTcpNoDelay(true);
+ socket.setSoLinger(true, 0);
+ socket.connect(new InetSocketAddress(host, port), timeout);
+ } catch (Exception e) {
+ throw new AssumptionViolatedException(String.format("Seems as redis is not running at %s:%s.", host, port), e);
+ }
+ }
+}