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); + } + } +}