diff --git a/Makefile b/Makefile index 728ba1965..b97c63319 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,8 @@ work/redis-%.conf: echo notify-keyspace-events Ex >> $@ echo pidfile $(shell pwd)/work/redis-$*.pid >> $@ echo logfile $(shell pwd)/work/redis-$*.log >> $@ + echo unixsocket $(shell pwd)/work/redis-$*.sock >> $@ + echo unixsocketperm 755 >> $@ echo save \"\" >> $@ echo slaveof 127.0.0.1 6379 >> $@ @@ -42,6 +44,8 @@ work/redis-6379.conf: echo notify-keyspace-events Ex >> $@ echo pidfile $(shell pwd)/work/redis-6379.pid >> $@ echo logfile $(shell pwd)/work/redis-6379.log >> $@ + echo unixsocket $(shell pwd)/work/redis-6379.sock >> $@ + echo unixsocketperm 755 >> $@ echo save \"\" >> $@ work/redis-%.pid: work/redis-%.conf work/redis/bin/redis-server @@ -122,7 +126,7 @@ cluster-init: cluster-start cluster-meet cluster-slots # Global ######## clean: - rm -rf work/*.conf work/*.log + rm -rf work/*.conf work/*.log dump.rdb clobber: rm -rf work diff --git a/pom.xml b/pom.xml index cb29afff3..e4ecae36e 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ 5.0.1.RELEASE 2.9.0 1.01 + 4.1.15.Final spring.data.redis @@ -85,6 +86,22 @@ true + + io.netty + netty-transport-native-epoll + linux-x86_64 + ${netty} + test + + + + io.netty + netty-transport-native-kqueue + osx-x86_64 + ${netty} + test + + io.projectreactor diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 4df7939e4..2adefbedb 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -3,7 +3,11 @@ New and noteworthy in the latest releases. -[[new-in-2.0.0]] +[[new-in-2.1.0]] +== New in Spring Data Redis 2.1 + +* Unix domain socket connections. + == New in Spring Data Redis 2.0 * Upgrade to Java 8. diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc index cdeeb539e..c8b6fdcff 100644 --- a/src/main/asciidoc/reference/redis.adoc +++ b/src/main/asciidoc/reference/redis.adoc @@ -39,64 +39,78 @@ NOTE: Depending on the underlying configuration, the factory can return a new co The easiest way to work with a `RedisConnectionFactory` is to configure the appropriate connector through the IoC container and inject it into the using class. -IMPORTANT: Unfortunately, currently, not all connectors support all Redis features. When invoking a method on the Connection API that is unsupported by the underlying library, an `UnsupportedOperationException` is thrown. - This situation is likely to be fixed in the future, as the various connectors mature. - -[[redis:connectors:jedis]] -=== Configuring Jedis connector - -http://github.com/xetorthio/jedis[Jedis] is one of the connectors supported by the Spring Data Redis module through the `org.springframework.data.redis.connection.jedis` package. In its simplest form, the Jedis configuration looks as follow: - -[source,xml] ----- - - - - - - - ----- - -For production use however, one might want to tweak the settings such as the host or password: - -[source,xml] ----- - - - - - - ----- +IMPORTANT: Unfortunately, currently, not all connectors support all Redis features. When invoking a method on the Connection API that is unsupported by the underlying library, an `UnsupportedOperationException` is thrown. [[redis:connectors:lettuce]] === Configuring Lettuce connector -https://github.com/mp911de/lettuce[Lettuce] is a http://netty.io/[netty]-based open-source connector supported by Spring Data Redis through the `org.springframework.data.redis.connection.lettuce` package. +https://github.com/lettuce-io/lettuce-core[Lettuce] is a http://netty.io/[netty]-based open-source connector supported by Spring Data Redis through the `org.springframework.data.redis.connection.lettuce` package. -Its configuration is probably easy to guess: - -[source,xml] +[source,java] ---- - - +@Configuration +class AppConfig { - + @Bean + public LettuceConnectionFactory redisConnectionFactory() { - + return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); + } +} ---- There are also a few Lettuce-specific connection parameters that can be tweaked. By default, all `LettuceConnection` s created by the `LettuceConnectionFactory` share the same thread-safe native connection for all non-blocking and non-transactional operations. Set `shareNativeConnection` to false to use a dedicated connection each time. `LettuceConnectionFactory` can also be configured with a `LettucePool` to use for pooling blocking and transactional connections, or all connections if `shareNativeConnection` is set to false. +Lettuce integrates with netty's http://netty.io/wiki/native-transports.html[native transports] allowing to use unix domain sockets to communicate with Redis. Make sure to include the appropriate native transport dependencies that match your runtime environment. + +[source,java] +---- +@Configuration +class AppConfig { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + + return new LettuceConnectionFactory(new RedisSocketConfiguration("/var/run/redis.sock")); + } +} +---- + +NOTE: Netty currently supports epoll (Linux) and kqueue (BSD/macOS) interfaces for OS-native transport. + +[[redis:connectors:jedis]] +=== Configuring Jedis connector + +http://github.com/xetorthio/jedis[Jedis] is a community-driven connector supported by the Spring Data Redis module through the `org.springframework.data.redis.connection.jedis` package. In its simplest form, the Jedis configuration looks as follow: + +[source,java] +---- +@Configuration +class AppConfig { + + @Bean + public JedisConnectionFactory redisConnectionFactory() { + return new JedisConnectionFactory(); + } +} +---- + +For production use however, one might want to tweak the settings such as the host or password: + +[source,java] +---- +@Configuration +class RedisConfiguration { + + @Bean + public JedisConnectionFactory redisConnectionFactory() { + + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("server", 6379); + return new JedisConnectionFactory(config); + } +} +---- + [[redis:sentinel]] == Redis Sentinel Support @@ -105,7 +119,7 @@ For dealing with high available Redis there is support for http://redis.io/topic [source,java] ---- /** - * jedis + * Jedis */ @Bean public RedisConnectionFactory jedisConnectionFactory() { diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java new file mode 100644 index 000000000..919ad4532 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017 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.Assert; + +/** + * Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} by connecting to a + * single node Redis using a local unix domain socket. + * + * @author Mark Paluch + * @since 2.1 + */ +public class RedisSocketConfiguration { + + private static final String DEFAULT_SOCKET = "/tmp/redis.sock"; + + private String socket = DEFAULT_SOCKET; + private int database; + private RedisPassword password = RedisPassword.none(); + + /** + * Create a new default {@link RedisSocketConfiguration}. + */ + public RedisSocketConfiguration() {} + + /** + * Create a new {@link RedisSocketConfiguration} given {@code socket}. + * + * @param socket must not be {@literal null} or empty. + */ + public RedisSocketConfiguration(String socket) { + + Assert.hasText(socket, "Socket path must not be null or empty!"); + + this.socket = socket; + } + + /** + * @return path to the Redis socket. + */ + public String getSocket() { + return socket; + } + + /** + * @param socket path to the Redis socket. + */ + public void setSocket(String socket) { + this.socket = socket; + } + + /** + * @return the db index. + */ + public int getDatabase() { + return database; + } + + /** + * Sets the index of the database used by this connection factory. Default is 0. + * + * @param index database index. + */ + public void setDatabase(int index) { + + Assert.isTrue(index >= 0, () -> String.format("Invalid DB index '%s' (a positive index required)", index)); + + this.database = index; + } + + /** + * @return never {@literal null}. + */ + public RedisPassword getPassword() { + return password; + } + + /** + * @param password must not be {@literal null}. + */ + public void setPassword(RedisPassword password) { + + Assert.notNull(password, "RedisPassword must not be null!"); + + this.password = password; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index 8da79fb26..0d9683662 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -67,6 +67,7 @@ import org.springframework.util.ClassUtils; * {@link LettuceConnectionFactory client configuration}. Lettuce supports the following environmental configurations: * @@ -99,6 +100,7 @@ public class LettuceConnectionFactory private final Object connectionMonitor = new Object(); private boolean convertPipelineAndTxResults = true; private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379); + private @Nullable RedisSocketConfiguration socketConfiguration; private @Nullable RedisSentinelConfiguration sentinelConfiguration; private @Nullable RedisClusterConfiguration clusterConfiguration; private @Nullable ClusterCommandExecutor clusterCommandExecutor; @@ -138,7 +140,17 @@ public class LettuceConnectionFactory } /** - * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration} + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSocketConfiguration}. + * + * @param socketConfiguration must not be {@literal null}. + * @since 2.1 + */ + public LettuceConnectionFactory(RedisSocketConfiguration socketConfiguration) { + this(socketConfiguration, new MutableLettuceClientConfiguration()); + } + + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration}. * * @param sentinelConfiguration must not be {@literal null}. * @since 1.6 @@ -186,6 +198,24 @@ public class LettuceConnectionFactory this.standaloneConfig = standaloneConfig; } + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSocketConfiguration} and + * {@link LettuceClientConfiguration}. + * + * @param socketConfiguration must not be {@literal null}. + * @param clientConfig must not be {@literal null}. + * @since 2.1 + */ + public LettuceConnectionFactory(RedisSocketConfiguration socketConfiguration, + LettuceClientConfiguration clientConfig) { + + this(clientConfig); + + Assert.notNull(socketConfiguration, "RedisSocketConfiguration must not be null!"); + + this.socketConfiguration = socketConfiguration; + } + /** * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration} and * {@link LettuceClientConfiguration}. @@ -588,6 +618,10 @@ public class LettuceConnectionFactory */ public int getDatabase() { + if (isDomainSocketAware()) { + return socketConfiguration.getDatabase(); + } + if (isRedisSentinelAware()) { return sentinelConfiguration.getDatabase(); } @@ -604,6 +638,11 @@ public class LettuceConnectionFactory Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); + if (isDomainSocketAware()) { + socketConfiguration.setDatabase(index); + return; + } + if (isRedisSentinelAware()) { sentinelConfiguration.setDatabase(index); return; @@ -648,6 +687,10 @@ public class LettuceConnectionFactory private RedisPassword getRedisPassword() { + if (isDomainSocketAware()) { + return socketConfiguration.getPassword(); + } + if (isRedisSentinelAware()) { return sentinelConfiguration.getPassword(); } @@ -669,6 +712,11 @@ public class LettuceConnectionFactory @Deprecated public void setPassword(String password) { + if (isDomainSocketAware()) { + socketConfiguration.setPassword(RedisPassword.of(password)); + return; + } + if (isRedisSentinelAware()) { sentinelConfiguration.setPassword(RedisPassword.of(password)); return; @@ -745,6 +793,15 @@ public class LettuceConnectionFactory return standaloneConfig; } + /** + * @return the {@link RedisSocketConfiguration}. + * @since 2.1 + */ + @Nullable + public RedisSocketConfiguration getSocketConfiguration() { + return socketConfiguration; + } + /** * @return the {@link RedisStandaloneConfiguration}, may be {@literal null}. * @since 2.0 @@ -793,6 +850,14 @@ public class LettuceConnectionFactory return sentinelConfiguration != null; } + /** + * @return true when {@link RedisSocketConfiguration} is present. + * @since 2.1 + */ + private boolean isDomainSocketAware() { + return socketConfiguration != null; + } + /** * @return true when {@link RedisClusterConfiguration} is present. * @since 1.7 @@ -833,6 +898,7 @@ public class LettuceConnectionFactory } private LettuceConnectionProvider doConnectionProvider(AbstractRedisClient client, RedisCodec codec) { + if (isClusterAware()) { return new ClusterConnectionProvider((RedisClusterClient) client, codec); } @@ -871,7 +937,8 @@ public class LettuceConnectionFactory return clusterClient; } - RedisURI uri = createRedisURIAndApplySettings(getHostName(), getPort()); + RedisURI uri = isDomainSocketAware() ? createRedisSocketURIAndApplySettings(socketConfiguration.getSocket()) + : createRedisURIAndApplySettings(getHostName(), getPort()); RedisClient redisClient = clientConfiguration.getClientResources() // .map(clientResources -> RedisClient.create(clientResources, uri)) // .orElseGet(() -> RedisClient.create(uri)); @@ -906,6 +973,16 @@ public class LettuceConnectionFactory return builder.build(); } + private RedisURI createRedisSocketURIAndApplySettings(String socketPath) { + + RedisURI.Builder builder = RedisURI.Builder.socket(socketPath); + + getRedisPassword().toOptional().ifPresent(builder::withPassword); + builder.withTimeout(clientConfiguration.getCommandTimeout()); + + return builder.build(); + } + @Override public RedisSentinelConnection getSentinelConnection() { return new LettuceSentinelConnection(connectionProvider); diff --git a/src/test/java/org/springframework/data/redis/SettingsUtils.java b/src/test/java/org/springframework/data/redis/SettingsUtils.java index 688c39aed..1d39a9f5e 100644 --- a/src/test/java/org/springframework/data/redis/SettingsUtils.java +++ b/src/test/java/org/springframework/data/redis/SettingsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2017 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. @@ -17,19 +17,25 @@ package org.springframework.data.redis; import java.util.Properties; +import org.springframework.data.redis.connection.RedisSocketConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; /** + * Utility class exposing connection settings to connect Redis instances during test execution. Settings can be adjusted + * by overriding these in {@literal org/springframework/data/redis/test.properties}. + * * @author Costin Leau * @author Mark Paluch */ public abstract class SettingsUtils { + private final static Properties DEFAULTS = new Properties(); private static final Properties SETTINGS; static { DEFAULTS.put("host", "127.0.0.1"); DEFAULTS.put("port", "6379"); + DEFAULTS.put("socket", "work/redis-6379.sock"); SETTINGS = new Properties(DEFAULTS); @@ -40,15 +46,44 @@ public abstract class SettingsUtils { } } + private SettingsUtils() {} + + /** + * @return the Redis hostname. + */ public static String getHost() { return SETTINGS.getProperty("host"); } + /** + * @return the Redis port. + */ public static int getPort() { return Integer.valueOf(SETTINGS.getProperty("port")); } + /** + * @return path to the unix domain socket. + */ + public static String getSocket() { + return SETTINGS.getProperty("socket"); + } + + /** + * Construct a new {@link RedisStandaloneConfiguration} initialized with test endpoint settings. + * + * @return a new {@link RedisStandaloneConfiguration} initialized with test endpoint settings. + */ public static RedisStandaloneConfiguration standaloneConfiguration() { return new RedisStandaloneConfiguration(getHost(), getPort()); } + + /** + * Construct a new {@link RedisSocketConfiguration} initialized with test endpoint settings. + * + * @return a new {@link RedisSocketConfiguration} initialized with test endpoint settings. + */ + public static RedisSocketConfiguration socketConfiguration() { + return new RedisSocketConfiguration(getSocket()); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java index d0fe1b916..1a28c7fdb 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java @@ -19,11 +19,15 @@ import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; +import io.lettuce.core.EpollProvider; +import io.lettuce.core.KqueueProvider; import io.lettuce.core.RedisException; import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.reactive.BaseRedisReactiveCommands; +import java.io.File; import java.time.Duration; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -362,6 +366,25 @@ public class LettuceConnectionFactoryTests { factory.destroy(); } + @Test // DATAREDIS-687 + public void connectsThroughRedisSocket() { + + assumeTrue(EpollProvider.isAvailable() || KqueueProvider.isAvailable()); + assumeTrue(new File(SettingsUtils.getSocket()).exists()); + + LettuceClientConfiguration configuration = LettuceTestClientConfiguration.create(); + + LettuceConnectionFactory factory = new LettuceConnectionFactory(SettingsUtils.socketConfiguration(), configuration); + factory.setShareNativeConnection(false); + factory.afterPropertiesSet(); + + RedisConnection connection = factory.getConnection(); + assertThat(connection.ping(), is(equalTo("PONG"))); + + connection.close(); + factory.destroy(); + } + @Test // DATAREDIS-576 public void connectionAppliesClientName() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java index 66f4bb171..042a9b22f 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java @@ -43,6 +43,7 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSocketConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.test.util.ReflectionTestUtils; @@ -285,6 +286,21 @@ public class LettuceConnectionFactoryUnitTests { } } + @Test // DATAREDIS-682 + public void socketShouldBeSetOnStandaloneClient() { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisSocketConfiguration()); + connectionFactory.afterPropertiesSet(); + ConnectionFactoryTracker.add(connectionFactory); + + AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client"); + assertThat(client, instanceOf(RedisClient.class)); + + RedisURI redisUri = (RedisURI) getField(client, "redisURI"); + + assertThat(redisUri.getSocket(), is("/tmp/redis.sock")); + } + @Test // DATAREDIS-574 public void shouldReadStandalonePassword() { @@ -337,6 +353,20 @@ public class LettuceConnectionFactoryUnitTests { assertThat(envConfig.getPassword(), is(equalTo(RedisPassword.of("bar")))); } + @Test // DATAREDIS-682 + public void shouldWriteSocketPassword() { + + RedisSocketConfiguration envConfig = new RedisSocketConfiguration(); + envConfig.setPassword(RedisPassword.of("foo")); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.defaultConfiguration()); + connectionFactory.setPassword("bar"); + + assertThat(connectionFactory.getPassword(), is(equalTo("bar"))); + assertThat(envConfig.getPassword(), is(equalTo(RedisPassword.of("bar")))); + } + @Test // DATAREDIS-574 public void shouldReadClusterPassword() { @@ -415,6 +445,20 @@ public class LettuceConnectionFactoryUnitTests { assertThat(envConfig.getDatabase(), is(3)); } + @Test // DATAREDIS-682 + public void shouldWriteSocketDatabaseIndex() { + + RedisSocketConfiguration envConfig = new RedisSocketConfiguration(); + envConfig.setDatabase(2); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(envConfig, + LettuceClientConfiguration.defaultConfiguration()); + connectionFactory.setDatabase(3); + + assertThat(connectionFactory.getDatabase(), is(3)); + assertThat(envConfig.getDatabase(), is(3)); + } + @Test // DATAREDIS-574 public void shouldApplyClientConfiguration() { @@ -456,6 +500,19 @@ public class LettuceConnectionFactoryUnitTests { assertThat(connectionFactory.getClusterConfiguration(), is(nullValue())); } + @Test // DATAREDIS-682 + public void shouldReturnSocketConfiguration() { + + RedisSocketConfiguration configuration = new RedisSocketConfiguration("/var/redis/socket"); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, + LettuceClientConfiguration.defaultConfiguration()); + + assertThat(connectionFactory.getStandaloneConfiguration(), is(notNullValue())); + assertThat(connectionFactory.getSocketConfiguration(), is(configuration)); + assertThat(connectionFactory.getSentinelConfiguration(), is(nullValue())); + assertThat(connectionFactory.getClusterConfiguration(), is(nullValue())); + } + @Test // DATAREDIS-574 public void shouldReturnSentinelConfiguration() { diff --git a/src/test/resources/org/springframework/data/redis/test.properties b/src/test/resources/org/springframework/data/redis/test.properties index 40b884246..6d3f61a5f 100644 --- a/src/test/resources/org/springframework/data/redis/test.properties +++ b/src/test/resources/org/springframework/data/redis/test.properties @@ -1,3 +1,4 @@ # redis connection properties host=127.0.0.1 -port=6379 \ No newline at end of file +port=6379 +socket=work/redis-6379.sock