diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc
index d34879cef..b7827da4f 100644
--- a/src/main/asciidoc/new-features.adoc
+++ b/src/main/asciidoc/new-features.adoc
@@ -11,6 +11,7 @@ New and noteworthy in the latest releases.
* Upgrade to `Lettuce` 5.0.
* Reactive connection support using https://github.com/lettuce-io/lettuce-core[lettuce-io/lettuce-core].
* Introduce Redis feature-specific interfaces for `RedisConnection`.
+* Revised `RedisCache` implementation.
[[new-in-1.8.0]]
diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc
index 84e443a86..4fa246c29 100644
--- a/src/main/asciidoc/reference/redis.adoc
+++ b/src/main/asciidoc/reference/redis.adoc
@@ -473,19 +473,19 @@ Spring Redis provides an implementation for Spring http://docs.spring.io/spring/
----
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
- return RedisCacheManager.defaultCacheManager(connectionFactory);
+ return RedisCacheManager.create(connectionFactory);
}
----
-`RedisCacheManager` behavior can be configured via `RedisCacheManagerConfigurator` allowing to set the default `RedisCacheConfiguration`, transaction behaviour and predefined caches.
+`RedisCacheManager` behavior can be configured via `RedisCacheManagerBuilder` allowing to set the default `RedisCacheConfiguration`, transaction behaviour and predefined caches.
[source,java]
----
-RedisCacheManager cm = RedisCacheManager.usingRawConnectionFactory(connectionFactory)
- .withCacheDefaults(defaultCacheConfig())
- .withInitialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
+RedisCacheManager cm = RedisCacheManager.builder(connectionFactory)
+ .cacheDefaults(defaultCacheConfig())
+ .initialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
.transactionAware()
- .createAndGet();
+ .build();
----
Behavior of `RedisCache` created via `RedisCacheManager` is defined via `RedisCacheConfiguration`. The configuration allows to set key expiration times, prefixes and ``RedisSerializer``s for converting to and from the binary storage format.
@@ -498,16 +498,15 @@ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues();
----
-Using default `RedisCacheManager` uses a non locking `RedisCacheWriter` for reading & writing bits.
-While this ensures a max of performance the lack of entry locking can lead to overlapping, non atomic commands, for `putIfAbsent` and `clean` as those methods combine a series of commands sent to Redis.
-The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key which leads to additional requests and potential command wait times.
+`RedisCacheManager` defaults to a lock-free `RedisCacheWriter` for reading & writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non atomic commands, for `putIfAbsent` and `clean` methods as those require multiple commands sent to Redis.
+The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times.
It is possible to opt in to the locking behavior as follows:
[source,java]
----
-RedisCacheManager cm = RedisCacheManager.usingCacheWriter(DefaultRedisCacheWriter.lockingRedisCacheWriter())
- .withCacheDefaults(defaultCacheConfig())
+RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.lockingRedisCacheWriter())
+ .cacheDefaults(defaultCacheConfig())
...
----
@@ -521,7 +520,7 @@ RedisCacheManager cm = RedisCacheManager.usingCacheWriter(DefaultRedisCacheWrite
|non locking
|Cache Configuration
-|`RedisCacheConfiguraiton#defaultConfiguration`
+|`RedisCacheConfiguration#defaultConfiguration`
|Initial Caches
|none
@@ -550,5 +549,8 @@ RedisCacheManager cm = RedisCacheManager.usingCacheWriter(DefaultRedisCacheWrite
|Value Serializer
|`JdkSerializationRedisSerializer`
+
+|Conversion Service
+|`DefaultFormattingConversionService` with default cache key converters
|====
diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java
index b97b77e85..13867093b 100644
--- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java
+++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java
@@ -15,15 +15,16 @@
*/
package org.springframework.data.redis.cache;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
-import org.springframework.data.redis.connection.RedisClusterConnection;
+import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
-import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.Assert;
@@ -31,20 +32,19 @@ import org.springframework.util.Assert;
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
* and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
* {@link RedisConnection}.
- * {@link DefaultRedisCacheWriter} can be used in {@link #lockingRedisCacheWriter(RedisConnectionFactory) locking} or
- * {@link #nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking} aims for
- * maximum performance it may result in overlapping, non atomic, command execution for operations spanning multiple
- * Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap by setting
- * an explicit lock key and checking against presence of this key which leads to additional requests and potential
- * command wait times.
+ * {@link DefaultRedisCacheWriter} can be used in
+ * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
+ * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
+ * {@literal non-locking} aims for maximum performance it may result in overlapping, non atomic, command execution for
+ * operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
+ * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional
+ * requests and potential command wait times.
*
* @author Christoph Strobl
+ * @author Mark Paluch
* @since 2.0
*/
-public class DefaultRedisCacheWriter implements RedisCacheWriter {
-
- private static final byte[] CLEAN_SCRIPT = "local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;"
- .getBytes(Charset.forName("UTF-8"));
+class DefaultRedisCacheWriter implements RedisCacheWriter {
private final RedisConnectionFactory connectionFactory;
private final Duration sleepTime;
@@ -70,30 +70,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
this.sleepTime = sleepTime;
}
- /**
- * Create new {@link RedisCacheWriter} without locking behavior.
- *
- * @param connectionFactory must not be {@literal null}.
- * @return new instance of {@link DefaultRedisCacheWriter}.
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#put(java.lang.String, byte[], byte[], java.time.Duration)
*/
- public static DefaultRedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
-
- Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
- return new DefaultRedisCacheWriter(connectionFactory);
- }
-
- /**
- * Create new {@link RedisCacheWriter} with locking behavior.
- *
- * @param connectionFactory must not be {@literal null}.
- * @return new instance of {@link DefaultRedisCacheWriter}.
- */
- public static DefaultRedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
-
- Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
- return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50));
- }
-
@Override
public void put(String name, byte[] key, byte[] value, Duration ttl) {
@@ -114,6 +94,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
});
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#get(java.lang.String, byte[])
+ */
@Override
public byte[] get(String name, byte[] key) {
@@ -123,6 +107,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
return execute(name, connection -> connection.get(key));
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#putIfAbsent(java.lang.String, byte[], byte[], java.time.Duration)
+ */
@Override
public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) {
@@ -156,6 +144,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
});
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#remove(java.lang.String, byte[])
+ */
@Override
public void remove(String name, byte[] key) {
@@ -165,46 +157,10 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
execute(name, connection -> connection.del(key));
}
- /**
- * Explicitly set a write lock on a cache.
- *
- * @param name the name of the cache to lock.
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.cache.RedisCacheWriter#clean(java.lang.String, byte[])
*/
- public void lock(String name) {
- execute(name, connection -> doLock(name, connection));
- }
-
- private Boolean doLock(String name, RedisConnection connection) {
- return connection.setNX(createCacheLockKey(name), new byte[] {});
- }
-
- /**
- * Explicitly remove a write lock from a cache.
- *
- * @param name the name of the cache to unlock.
- */
- public void unlock(String name) {
- executeWithoutLockCheck(connection -> doUnlock(name, connection));
- }
-
- private Long doUnlock(String name, RedisConnection connection) {
- return connection.del(createCacheLockKey(name));
- }
-
- /**
- * Check if a cache has set a lock.
- *
- * @param name the name of the cache to check for presence of a lock.
- * @return {@literal true} if lock found.
- */
- public boolean isLoked(String name) {
- return executeWithoutLockCheck(connection -> doCheckLock(name, connection));
- }
-
- private boolean doCheckLock(String name, RedisConnection connection) {
- return connection.exists(createCacheLockKey(name));
- }
-
@Override
public void clean(String name, byte[] pattern) {
@@ -216,17 +172,16 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
boolean wasLocked = false;
try {
- if (connection instanceof RedisClusterConnection) {
- if (isLockingCacheWriter()) {
- doLock(name, connection);
- wasLocked = true;
- }
+ if (isLockingCacheWriter()) {
+ doLock(name, connection);
+ wasLocked = true;
+ }
- byte[][] keys = connection.keys(pattern).stream().toArray(size -> new byte[size][]);
+ byte[][] keys = connection.keys(pattern).toArray(new byte[0][]);
+
+ if (keys.length > 0) {
connection.del(keys);
- } else {
- connection.eval(CLEAN_SCRIPT, ReturnType.INTEGER, 0, pattern);
}
} finally {
@@ -239,31 +194,61 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
});
}
+ /**
+ * Explicitly set a write lock on a cache.
+ *
+ * @param name the name of the cache to lock.
+ */
+ void lock(String name) {
+ execute(name, connection -> doLock(name, connection));
+ }
+
+ /**
+ * Explicitly remove a write lock from a cache.
+ *
+ * @param name the name of the cache to unlock.
+ */
+ void unlock(String name) {
+ executeLockFree(connection -> doUnlock(name, connection));
+ }
+
+ private Boolean doLock(String name, RedisConnection connection) {
+ return connection.setNX(createCacheLockKey(name), new byte[0]);
+ }
+
+ private Long doUnlock(String name, RedisConnection connection) {
+ return connection.del(createCacheLockKey(name));
+ }
+
+ boolean doCheckLock(String name, RedisConnection connection) {
+ return connection.exists(createCacheLockKey(name));
+ }
+
/**
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
*/
- public boolean isLockingCacheWriter() {
+ private boolean isLockingCacheWriter() {
return !sleepTime.isZero() && !sleepTime.isNegative();
}
- T execute(String name, ConnectionCallback callback) {
+ private T execute(String name, Function callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
- return callback.doWithConnection(connection);
+ return callback.apply(connection);
} finally {
connection.close();
}
}
- private T executeWithoutLockCheck(ConnectionCallback callback) {
+ private void executeLockFree(Consumer callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
- return callback.doWithConnection(connection);
+ callback.accept(connection);
} finally {
connection.close();
}
@@ -271,35 +256,30 @@ public class DefaultRedisCacheWriter implements RedisCacheWriter {
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
- if (isLockingCacheWriter()) {
+ if (!isLockingCacheWriter()) {
+ return;
+ }
- long timeout = sleepTime.toMillis();
+ try {
while (doCheckLock(name, connection)) {
- try {
- Thread.sleep(timeout);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
+ Thread.sleep(sleepTime.toMillis());
}
+ } catch (InterruptedException ex) {
+
+ // Re-interrupt current thread, to allow other participants to react.
+ Thread.currentThread().interrupt();
+
+ throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
+ ex);
}
}
- private boolean shouldExpireWithin(Duration ttl) {
+ private static boolean shouldExpireWithin(Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}
- byte[] createCacheLockKey(String name) {
- return (name + "~lock").getBytes(Charset.forName("UTF-8"));
- }
-
- /**
- * @param
- * @author Christoph Strobl
- * @since 2.0
- */
- interface ConnectionCallback {
-
- T doWithConnection(RedisConnection connection);
+ private static byte[] createCacheLockKey(String name) {
+ return (name + "~lock").getBytes(StandardCharsets.UTF_8);
}
}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java
index 112f498e5..ba3183175 100644
--- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java
@@ -15,24 +15,31 @@
*/
package org.springframework.data.redis.cache;
+import java.lang.reflect.Method;
import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.cache.support.NullValue;
import org.springframework.cache.support.SimpleValueWrapper;
-import org.springframework.core.convert.support.ConfigurableConversionService;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
-import org.springframework.format.support.DefaultFormattingConversionService;
+import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
+import org.springframework.util.ReflectionUtils;
/**
* {@link org.springframework.cache.Cache} implementation using for Redis as underlying store.
+ *
+ * Use {@link RedisCacheManager} to create {@link RedisCache} instances.
*
* @author Christoph Strobl
+ * @author Mark Paluch
* @since 2.0
+ * @see RedisCacheConfiguration
+ * @see RedisCacheWriter
*/
public class RedisCache extends AbstractValueAdaptingCache {
@@ -41,7 +48,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
private final String name;
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration cacheConfig;
- private final ConfigurableConversionService conversionService = new DefaultFormattingConversionService();
+ private final ConversionService conversionService;
/**
* Create new {@link RedisCache}.
@@ -50,7 +57,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
* @param cacheWriter must not be {@literal null}.
* @param cacheConfig must not be {@literal null}.
*/
- RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
+ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(cacheConfig.getAllowCacheNullValues());
@@ -61,10 +68,13 @@ public class RedisCache extends AbstractValueAdaptingCache {
this.name = name;
this.cacheWriter = cacheWriter;
this.cacheConfig = cacheConfig;
-
- conversionService.addConverter(String.class, byte[].class, source -> source.getBytes(Charset.forName("UTF-8")));
+ this.conversionService = cacheConfig.getConversionService();
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.support.AbstractValueAdaptingCache#lookup(java.lang.Object)
+ */
@Override
protected Object lookup(Object key) {
@@ -77,17 +87,30 @@ public class RedisCache extends AbstractValueAdaptingCache {
return deserializeCacheValue(value);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#getName()
+ */
@Override
public String getName() {
return name;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#getNativeCache()
+ */
@Override
public RedisCacheWriter getNativeCache() {
return this.cacheWriter;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#get(java.lang.Object, java.util.concurrent.Callable)
+ */
@Override
+ @SuppressWarnings("unchecked")
public synchronized T get(Object key, Callable valueLoader) {
ValueWrapper result = get(key);
@@ -101,6 +124,10 @@ public class RedisCache extends AbstractValueAdaptingCache {
return value;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
+ */
@Override
public void put(Object key, Object value) {
@@ -116,6 +143,10 @@ public class RedisCache extends AbstractValueAdaptingCache {
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
+ */
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
@@ -135,11 +166,19 @@ public class RedisCache extends AbstractValueAdaptingCache {
return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#evict(java.lang.Object)
+ */
@Override
public void evict(Object key) {
cacheWriter.remove(name, createAndConvertCacheKey(key));
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.Cache#clear()
+ */
@Override
public void clear() {
@@ -163,7 +202,6 @@ public class RedisCache extends AbstractValueAdaptingCache {
* @param value can be {@literal null}.
* @return preprocessed value. Can be {@literal null}.
*/
-
protected Object preProcessCacheValue(Object value) {
if (value != null) {
@@ -180,7 +218,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
* @return never {@literal null}.
*/
protected byte[] serializeCacheKey(String cacheKey) {
- return cacheConfig.getKeySerializationPair().write(cacheKey).array();
+ return ByteUtils.getBytes(cacheConfig.getKeySerializationPair().write(cacheKey));
}
/**
@@ -195,7 +233,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
return BINARY_NULL_VALUE;
}
- return cacheConfig.getValueSerializationPair().write(value).array();
+ return ByteUtils.getBytes(cacheConfig.getValueSerializationPair().write(value));
}
/**
@@ -204,7 +242,6 @@ public class RedisCache extends AbstractValueAdaptingCache {
* @param value must not be {@literal null}.
* @return can be {@literal null}.
*/
-
protected Object deserializeCacheValue(byte[] value) {
if (isAllowNullValues() && ObjectUtils.nullSafeEquals(value, BINARY_NULL_VALUE)) {
@@ -222,7 +259,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
*/
protected String createCacheKey(Object key) {
- String convertedKey = conversionService.convert(key, String.class);
+ String convertedKey = convertKey(key);
+
if (!cacheConfig.usePrefix()) {
return convertedKey;
}
@@ -230,6 +268,30 @@ public class RedisCache extends AbstractValueAdaptingCache {
return prefixCacheKey(convertedKey);
}
+ /**
+ * Convert {@code key} to a {@link String} representation used for cache key creation.
+ *
+ * @param key will never be {@literal null}.
+ * @return never {@literal null}.
+ * @throws IllegalStateException if {@code key} cannot be converted to {@link String}.
+ */
+ protected String convertKey(Object key) {
+
+ TypeDescriptor source = TypeDescriptor.forObject(key);
+ if (conversionService.canConvert(source, TypeDescriptor.valueOf(String.class))) {
+ return conversionService.convert(key, String.class);
+ }
+
+ Method toString = ReflectionUtils.findMethod(key.getClass(), "toString");
+
+ if (toString != null && !Object.class.equals(toString.getDeclaringClass())) {
+ return key.toString();
+ }
+
+ throw new IllegalStateException(
+ "Cannot convert " + source + " to String. Register a Converter or override toString().");
+ }
+
private byte[] createAndConvertCacheKey(Object key) {
return serializeCacheKey(createCacheKey(key));
}
@@ -246,5 +308,4 @@ public class RedisCache extends AbstractValueAdaptingCache {
throw new ValueRetrievalException(key, valueLoader, e);
}
}
-
}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java
index 1c71e9bb8..aa347bcee 100644
--- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java
@@ -15,22 +15,28 @@
*/
package org.springframework.data.redis.cache;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import org.springframework.cache.Cache;
+import org.springframework.cache.interceptor.SimpleKey;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
/**
* Immutable {@link RedisCacheConfiguration} helps customizing {@link RedisCache} behaviour such as caching
* {@literal null} values, cache key prefixes and binary serialization.
- * Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from
- * there on.
+ * Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from there
+ * on.
*
* @author Christoph Strobl
+ * @author Mark Paluch
* @since 2.0
*/
public class RedisCacheConfiguration {
@@ -41,17 +47,22 @@ public class RedisCacheConfiguration {
private final boolean usePrefix;
private final SerializationPair keySerializationPair;
- private final SerializationPair> valueSerializationPair;
+ private final SerializationPair