Review and polish for confiuring a cache lock TTL.
Closes #2300 Pull request: #2597.
This commit is contained in:
@@ -31,12 +31,13 @@ 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}. <br />
|
||||
* and {@literal cluster} environments, and uses a given {@link RedisConnectionFactory} to obtain the actual
|
||||
* {@link RedisConnection}.
|
||||
* <p>
|
||||
* {@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
|
||||
* {@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.
|
||||
@@ -44,16 +45,20 @@ import org.springframework.util.Assert;
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author André Prata
|
||||
* @author John Blum
|
||||
* @since 2.0
|
||||
*/
|
||||
class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
private final BatchStrategy batchStrategy;
|
||||
|
||||
private final CacheStatisticsCollector statistics;
|
||||
|
||||
private final Duration sleepTime;
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
private final TtlFunction lockTtl;
|
||||
private final CacheStatisticsCollector statistics;
|
||||
private final BatchStrategy batchStrategy;
|
||||
|
||||
/**
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
@@ -166,8 +171,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
}
|
||||
|
||||
return connection.get(key);
|
||||
} finally {
|
||||
|
||||
} finally {
|
||||
if (isLockingCacheWriter()) {
|
||||
doUnlock(name, connection);
|
||||
}
|
||||
@@ -196,21 +201,21 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
boolean wasLocked = false;
|
||||
|
||||
try {
|
||||
|
||||
if (isLockingCacheWriter()) {
|
||||
doLock(name, name, pattern, connection);
|
||||
wasLocked = true;
|
||||
}
|
||||
|
||||
long deleteCount = batchStrategy.cleanCache(connection, name, pattern);
|
||||
|
||||
while (deleteCount > Integer.MAX_VALUE) {
|
||||
statistics.incDeletesBy(name, Integer.MAX_VALUE);
|
||||
deleteCount -= Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
statistics.incDeletesBy(name, (int) deleteCount);
|
||||
|
||||
} finally {
|
||||
|
||||
if (wasLocked && isLockingCacheWriter()) {
|
||||
doUnlock(name, connection);
|
||||
}
|
||||
@@ -280,8 +285,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
private <T> T execute(String name, Function<RedisConnection, T> callback) {
|
||||
|
||||
RedisConnection connection = connectionFactory.getConnection();
|
||||
try {
|
||||
|
||||
try {
|
||||
checkAndPotentiallyWaitUntilUnlocked(name, connection);
|
||||
return callback.apply(connection);
|
||||
} finally {
|
||||
@@ -307,18 +312,19 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
|
||||
}
|
||||
|
||||
long lockWaitTimeNs = System.nanoTime();
|
||||
try {
|
||||
|
||||
try {
|
||||
while (doCheckLock(name, connection)) {
|
||||
Thread.sleep(sleepTime.toMillis());
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
} catch (InterruptedException cause) {
|
||||
|
||||
// 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);
|
||||
String message = String.format("Interrupted while waiting to unlock cache %s", name);
|
||||
|
||||
throw new PessimisticLockingFailureException(message, cause);
|
||||
} finally {
|
||||
statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
|
||||
}
|
||||
|
||||
@@ -21,12 +21,16 @@ import org.springframework.data.redis.cache.RedisCacheWriter.TtlFunction;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Singleton implementation of {@link TtlFunction}.
|
||||
* {@link TtlFunction} implementation returning the given, predetermined {@link Duration} used for per cache entry
|
||||
* {@literal time-to-live (TTL) expiration}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author John Blum
|
||||
* @see java.time.Duration
|
||||
* @see org.springframework.data.redis.cache.RedisCacheWriter.TtlFunction
|
||||
* @since 3.2
|
||||
*/
|
||||
public record SingletonTtlFunction(Duration duration) implements TtlFunction {
|
||||
public record FixedDurationTtlFunction(Duration duration) implements TtlFunction {
|
||||
|
||||
@Override
|
||||
public Duration getTimeToLive(Object key, @Nullable Object value) {
|
||||
@@ -67,10 +67,10 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
* Create a new {@link RedisCache}.
|
||||
*
|
||||
* @param name {@link String name} for this {@link Cache}; must not be {@literal null}.
|
||||
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate
|
||||
* Redis commands; must not be {@literal null}.
|
||||
* @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache on creation; must not
|
||||
* be {@literal null}.
|
||||
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing
|
||||
* the necessary Redis commands; must not be {@literal null}.
|
||||
* @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache} on creation;
|
||||
* must not be {@literal null}.
|
||||
* @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration}
|
||||
* are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}.
|
||||
*/
|
||||
@@ -88,18 +88,33 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link RedisCacheConfiguration} used.
|
||||
* Get the {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization.
|
||||
*
|
||||
* @return immutable {@link RedisCacheConfiguration}. Never {@literal null}.
|
||||
* @return an immutable {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization;
|
||||
* never {@literal null}.
|
||||
*/
|
||||
public RedisCacheConfiguration getCacheConfiguration() {
|
||||
return this.cacheConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured {@link RedisCacheWriter} used to modify Redis for cache operations.
|
||||
*
|
||||
* @return the configured {@link RedisCacheWriter} used to modify Redis for cache operations.
|
||||
*/
|
||||
protected RedisCacheWriter getCacheWriter() {
|
||||
return this.cacheWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String}
|
||||
* when accessing entries in the cache.
|
||||
*
|
||||
* @return the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String}
|
||||
* when accessing entries in the cache.
|
||||
* @see RedisCacheConfiguration#getConversionService()
|
||||
* @see #getCacheConfiguration()
|
||||
*/
|
||||
protected ConversionService getConversionService() {
|
||||
return getCacheConfiguration().getConversionService();
|
||||
}
|
||||
@@ -111,12 +126,13 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
|
||||
@Override
|
||||
public RedisCacheWriter getNativeCache() {
|
||||
return this.cacheWriter;
|
||||
return getCacheWriter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link CacheStatistics} snapshot for this cache instance. Statistics are accumulated per cache instance
|
||||
* and not from the backing Redis data store.
|
||||
* Return the {@link CacheStatistics} snapshot for this cache instance.
|
||||
* <p>
|
||||
* Statistics are accumulated per cache instance and not from the backing Redis data store.
|
||||
*
|
||||
* @return statistics object for this {@link RedisCache}.
|
||||
* @since 2.4
|
||||
@@ -134,8 +150,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
return result != null ? (T) result.get() : getSynchronized(key, valueLoader);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
|
||||
|
||||
ValueWrapper result = get(key);
|
||||
@@ -143,6 +159,16 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
return result != null ? (T) result.get() : loadCacheValue(key, valueLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts}
|
||||
* the {@link Object loaded value} in the cache.
|
||||
*
|
||||
* @param <T> {@link Class type} of the loaded {@link Object cache value}.
|
||||
* @param key {@link Object key} mapped to the loaded {@link Object cache value}.
|
||||
* @param valueLoader {@link Callable} object used to load the {@link Object value}
|
||||
* for the given {@link Object key}.
|
||||
* @return the loaded {@link Object value}.
|
||||
*/
|
||||
protected <T> T loadCacheValue(Object key, Callable<T> valueLoader) {
|
||||
|
||||
T value;
|
||||
@@ -172,7 +198,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
|
||||
Object cacheValue = preProcessCacheValue(value);
|
||||
|
||||
if (!isAllowNullValues() && cacheValue == null) {
|
||||
if (nullCacheValueIsNotAllowed(cacheValue)) {
|
||||
|
||||
String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null"
|
||||
+ " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'"
|
||||
@@ -182,7 +208,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
}
|
||||
|
||||
getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
|
||||
getCacheConfiguration().getTtl());
|
||||
getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,12 +216,12 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
|
||||
Object cacheValue = preProcessCacheValue(value);
|
||||
|
||||
if (!isAllowNullValues() && cacheValue == null) {
|
||||
if (nullCacheValueIsNotAllowed(cacheValue)) {
|
||||
return get(key);
|
||||
}
|
||||
|
||||
byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key),
|
||||
serializeCacheValue(cacheValue), getCacheConfiguration().getTtl());
|
||||
serializeCacheValue(cacheValue), getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
|
||||
|
||||
return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null;
|
||||
}
|
||||
@@ -206,7 +232,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear keys that match the provided {@link String keyPattern}.
|
||||
* Clear keys that match the given {@link String keyPattern}.
|
||||
* <p>
|
||||
* Useful when cache keys are formatted in a style where Redis patterns can be used for matching these.
|
||||
*
|
||||
@@ -303,7 +329,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@code key} to a {@link String} representation used for cache key creation.
|
||||
* Convert {@code key} to a {@link String} used in cache key creation.
|
||||
*
|
||||
* @param key will never be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
@@ -338,10 +364,9 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
String message = String.format(
|
||||
"Cannot convert cache key %s to String; Please register a suitable Converter"
|
||||
String message = String.format("Cannot convert cache key %s to String; Please register a suitable Converter"
|
||||
+ " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'",
|
||||
source, key.getClass().getName());
|
||||
source, key.getClass().getName());
|
||||
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
@@ -403,4 +428,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
|
||||
// allow contextual cache names by computing the key prefix on every call.
|
||||
return getCacheConfiguration().getKeyPrefixFor(getName()) + key;
|
||||
}
|
||||
|
||||
private boolean nullCacheValueIsNotAllowed(@Nullable Object cacheValue) {
|
||||
return cacheValue == null && !isAllowNullValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Immutable {@link RedisCacheConfiguration} used to customize {@link RedisCache} behaviour, such as caching
|
||||
* Immutable {@link RedisCacheConfiguration} used to customize {@link RedisCache} behavior, such as caching
|
||||
* {@literal null} values, computing cache key prefixes and handling binary serialization.
|
||||
* <p>
|
||||
* Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour
|
||||
* from that point on.
|
||||
* Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behavior
|
||||
* using the builder methods, such as {@link #entryTtl(Duration)}, {@link #serializeKeysWith(SerializationPair)}
|
||||
* and {@link #serializeValuesWith(SerializationPair)}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
@@ -107,10 +108,11 @@ public class RedisCacheConfiguration {
|
||||
|
||||
registerDefaultConverters(conversionService);
|
||||
|
||||
return new RedisCacheConfiguration(Duration.ZERO, DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX,
|
||||
return new RedisCacheConfiguration(TtlFunction.persistent(), DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX,
|
||||
CacheKeyPrefix.simple(),
|
||||
SerializationPair.fromSerializer(RedisSerializer.string()),
|
||||
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
|
||||
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)),
|
||||
conversionService);
|
||||
}
|
||||
|
||||
private final boolean cacheNullValues;
|
||||
@@ -120,25 +122,17 @@ public class RedisCacheConfiguration {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private final TtlFunction ttl;
|
||||
|
||||
private final SerializationPair<String> keySerializationPair;
|
||||
private final SerializationPair<Object> valueSerializationPair;
|
||||
|
||||
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
|
||||
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
|
||||
ConversionService conversionService) {
|
||||
|
||||
this(TtlFunction.just(ttl), cacheNullValues, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair,
|
||||
conversionService);
|
||||
}
|
||||
private final TtlFunction ttlFunction;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private RedisCacheConfiguration(TtlFunction ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
|
||||
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
|
||||
ConversionService conversionService) {
|
||||
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean usePrefix,
|
||||
CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
|
||||
SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
|
||||
|
||||
this.ttl = ttl;
|
||||
this.ttlFunction = ttlFunction;
|
||||
this.cacheNullValues = cacheNullValues;
|
||||
this.usePrefix = usePrefix;
|
||||
this.keyPrefix = keyPrefix;
|
||||
@@ -172,10 +166,10 @@ public class RedisCacheConfiguration {
|
||||
*/
|
||||
public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix) {
|
||||
|
||||
Assert.notNull(cacheKeyPrefix, "Function for computing prefix must not be null");
|
||||
Assert.notNull(cacheKeyPrefix, "Function used to compute prefix must not be null");
|
||||
|
||||
return new RedisCacheConfiguration(ttl, cacheNullValues, DEFAULT_USE_PREFIX, cacheKeyPrefix,
|
||||
keySerializationPair, valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DEFAULT_USE_PREFIX,
|
||||
cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(), getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,8 +181,8 @@ public class RedisCacheConfiguration {
|
||||
* @return new {@link RedisCacheConfiguration}.
|
||||
*/
|
||||
public RedisCacheConfiguration disableCachingNullValues() {
|
||||
return new RedisCacheConfiguration(ttl, DO_NOT_CACHE_NULL_VALUES, usePrefix, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, usePrefix(), getKeyPrefix(),
|
||||
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,9 +193,8 @@ public class RedisCacheConfiguration {
|
||||
* @return new {@link RedisCacheConfiguration}.
|
||||
*/
|
||||
public RedisCacheConfiguration disableKeyPrefix() {
|
||||
|
||||
return new RedisCacheConfiguration(ttl, cacheNullValues, DO_NOT_USE_PREFIX, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DO_NOT_USE_PREFIX,
|
||||
getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,8 +222,8 @@ public class RedisCacheConfiguration {
|
||||
|
||||
Assert.notNull(ttlFunction, "TtlFunction must not be null");
|
||||
|
||||
return new RedisCacheConfiguration(ttlFunction, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
|
||||
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,8 +236,8 @@ public class RedisCacheConfiguration {
|
||||
|
||||
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null");
|
||||
|
||||
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
|
||||
keySerializationPair, getValueSerializationPair(), getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,8 +250,8 @@ public class RedisCacheConfiguration {
|
||||
|
||||
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null");
|
||||
|
||||
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
|
||||
getKeySerializationPair(), valueSerializationPair, getConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,15 +264,15 @@ public class RedisCacheConfiguration {
|
||||
|
||||
Assert.notNull(conversionService, "ConversionService must not be null");
|
||||
|
||||
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
|
||||
valueSerializationPair, conversionService);
|
||||
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
|
||||
getKeySerializationPair(), getValueSerializationPair(), conversionService);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if caching {@literal null} is allowed.
|
||||
*/
|
||||
public boolean getAllowCacheNullValues() {
|
||||
return cacheNullValues;
|
||||
return this.cacheNullValues;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,14 +280,23 @@ public class RedisCacheConfiguration {
|
||||
* the default which resolves to {@link Cache#getName()}.
|
||||
*/
|
||||
public boolean usePrefix() {
|
||||
return usePrefix;
|
||||
return this.usePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The {@link ConversionService} used for cache key to {@link String} conversion. Never {@literal null}.
|
||||
*/
|
||||
public ConversionService getConversionService() {
|
||||
return conversionService;
|
||||
return this.conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured {@link CacheKeyPrefix}.
|
||||
*
|
||||
* @return the configured {@link CacheKeyPrefix}.
|
||||
*/
|
||||
public CacheKeyPrefix getKeyPrefix() {
|
||||
return this.keyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,32 +309,30 @@ public class RedisCacheConfiguration {
|
||||
|
||||
Assert.notNull(cacheName, "Cache name must not be null");
|
||||
|
||||
return keyPrefix.compute(cacheName);
|
||||
return this.keyPrefix.compute(cacheName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public SerializationPair<String> getKeySerializationPair() {
|
||||
return keySerializationPair;
|
||||
return this.keySerializationPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public SerializationPair<Object> getValueSerializationPair() {
|
||||
return valueSerializationPair;
|
||||
return this.valueSerializationPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The expiration time (ttl) for cache entries. Never {@literal null}.
|
||||
* Gets the {@link TtlFunction} used to compute a cache key {@literal time-to-live (TTL) expiration}.
|
||||
*
|
||||
* @return the {@link TtlFunction} used to compute expiration time (TTL) for cache entries; never {@literal null}.
|
||||
*/
|
||||
public Duration getTtl() {
|
||||
return getTtlFunction().getTimeToLive(null, null);
|
||||
}
|
||||
|
||||
public TtlFunction getTtlFunction() {
|
||||
return this.ttl;
|
||||
return this.ttlFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,16 +22,18 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RedisCacheWriter} provides low level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for
|
||||
* caching. <br />
|
||||
* The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for writing / reading
|
||||
* binary data to / from Redis. The implementation honors potential cache lock flags that might be set.
|
||||
* {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for
|
||||
* caching.
|
||||
* <p>
|
||||
* The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for reading/writing
|
||||
* binary data from/to Redis. The implementation honors potential cache lock flags that might be set.
|
||||
* <p>
|
||||
* The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} to tune performance
|
||||
* behavior.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author John Blum
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface RedisCacheWriter extends CacheStatisticsProvider {
|
||||
@@ -178,6 +180,8 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
|
||||
@FunctionalInterface
|
||||
interface TtlFunction {
|
||||
|
||||
Duration NO_EXPIRATION = Duration.ZERO;
|
||||
|
||||
/**
|
||||
* Creates a singleton {@link TtlFunction} using the given {@link Duration}.
|
||||
*
|
||||
@@ -189,7 +193,7 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
|
||||
|
||||
Assert.notNull(duration, "TTL Duration must not be null");
|
||||
|
||||
return new SingletonTtlFunction(duration);
|
||||
return new FixedDurationTtlFunction(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,19 +202,23 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
|
||||
* @return a {@link TtlFunction} to create persistent entires that do not expire.
|
||||
*/
|
||||
static TtlFunction persistent() {
|
||||
return just(Duration.ZERO);
|
||||
return just(NO_EXPIRATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a {@link Duration time to live duration} using the cache {@code key} and {@code value}. The time to live
|
||||
* is computed on each write operation. Redis uses milliseconds granularity for timeouts. Any more granular values
|
||||
* (e.g. micros or nanos) are not considered and are truncated due to rounding. Returning {@link Duration#ZERO} (or
|
||||
* a value less than {@code Duration.ofMillis(1)}) results in a persistent value that does not expire.
|
||||
* Compute a {@link Duration time-to-live (TTL)} using the cache {@code key} and {@code value}.
|
||||
* <p>
|
||||
* The {@link Duration time-to-live (TTL)} is computed on each write operation. Redis uses millisecond
|
||||
* granularity for timeouts. Any more granular values (e.g. micros or nanos) are not considered
|
||||
* and will be truncated due to rounding. Returning {@link Duration#ZERO}, or a value less than
|
||||
* {@code Duration.ofMillis(1)}, results in a persistent value that does not expire.
|
||||
*
|
||||
* @param key the cache key.
|
||||
* @param value the cache value. Can be {@code null} if the cache supports {@code null} value caching.
|
||||
* @return the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry does not expire).
|
||||
* @return the computed {@link Duration time-to-live (TTL)}. Can be {@link Duration#ZERO} for persistent values
|
||||
* (i.e. cache entry does not expire).
|
||||
*/
|
||||
Duration getTimeToLive(Object key, @Nullable Object value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,117 +20,98 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Expiration holds a value with its associated {@link TimeUnit}.
|
||||
* {@link Expiration} holds a {@link Long numeric value} with an associated {@link TimeUnit}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author John Blum
|
||||
* @see java.time.Duration
|
||||
* @see java.util.concurrent.TimeUnit
|
||||
* @since 1.7
|
||||
*/
|
||||
public class Expiration {
|
||||
|
||||
private long expirationTime;
|
||||
private TimeUnit timeUnit;
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration}.
|
||||
* Creates a new {@link Expiration} in {@link TimeUnit#MILLISECONDS}.
|
||||
*
|
||||
* @param expirationTime can be {@literal null}. Defaulted to {@link TimeUnit#SECONDS}
|
||||
* @param timeUnit
|
||||
*/
|
||||
protected Expiration(long expirationTime, @Nullable TimeUnit timeUnit) {
|
||||
|
||||
this.expirationTime = expirationTime;
|
||||
this.timeUnit = timeUnit != null ? timeUnit : TimeUnit.SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration time converted into {@link TimeUnit#MILLISECONDS}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long getExpirationTimeInMilliseconds() {
|
||||
return getConverted(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration time converted into {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long getExpirationTimeInSeconds() {
|
||||
return getConverted(TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration time.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long getExpirationTime() {
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time unit for the expiration time.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public TimeUnit getTimeUnit() {
|
||||
return this.timeUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration time converted into the desired {@code targetTimeUnit}.
|
||||
*
|
||||
* @param targetTimeUnit must not {@literal null}.
|
||||
* @return
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public long getConverted(TimeUnit targetTimeUnit) {
|
||||
|
||||
Assert.notNull(targetTimeUnit, "TargetTimeUnit must not be null");
|
||||
return targetTimeUnit.convert(expirationTime, timeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration} with {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @param expirationTime
|
||||
* @return
|
||||
*/
|
||||
public static Expiration seconds(long expirationTime) {
|
||||
return new Expiration(expirationTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration} with {@link TimeUnit#MILLISECONDS}.
|
||||
*
|
||||
* @param expirationTime
|
||||
* @return
|
||||
* @param expirationTime {@link Long length of time} for expiration.
|
||||
* @return a new {@link Expiration} measured in {@link TimeUnit#MILLISECONDS}.
|
||||
*/
|
||||
public static Expiration milliseconds(long expirationTime) {
|
||||
return new Expiration(expirationTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration} with the given {@literal unix timestamp} and {@link TimeUnit}.
|
||||
* Creates a new {@link Expiration} in {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @param unixTimestamp the unix timestamp at which the key will expire.
|
||||
* @param timeUnit must not be {@literal null}.
|
||||
* @return new instance of {@link Expiration}.
|
||||
* @param expirationTime {@link Long length of time} for expiration.
|
||||
* @return a new {@link Expiration} measured in {@link TimeUnit#SECONDS}.
|
||||
*/
|
||||
public static Expiration seconds(long expirationTime) {
|
||||
return new Expiration(expirationTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Expiration} with the given {@literal unix timestamp} and {@link TimeUnit}.
|
||||
*
|
||||
* @param unixTimestamp {@link Long unix timestamp} at which the key will expire.
|
||||
* @param timeUnit {@link TimeUnit} used to measure the expiration period; must not be {@literal null}.
|
||||
* @return a new {@link Expiration} with the given {@literal unix timestamp} and {@link TimeUnit}.
|
||||
*/
|
||||
public static Expiration unixTimestamp(long unixTimestamp, TimeUnit timeUnit) {
|
||||
return new ExpireAt(unixTimestamp, timeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an {@link Expiration} that indicates to keep the existing one. Eg. when sending a {@code SET} command.
|
||||
* Creates new {@link Expiration} with the provided {@link TimeUnit}. Greater units than {@link TimeUnit#SECONDS} are
|
||||
* converted to {@link TimeUnit#SECONDS}. Units smaller than {@link TimeUnit#MILLISECONDS} are converted to
|
||||
* {@link TimeUnit#MILLISECONDS} and can lose precision since {@link TimeUnit#MILLISECONDS} is the smallest
|
||||
* granularity supported by Redis.
|
||||
*
|
||||
* @param expirationTime {@link Long length of time} for the {@link Expiration}.
|
||||
* @param timeUnit {@link TimeUnit} used to measure the {@link Long expiration time}; can be {@literal null}.
|
||||
* Defaulted to {@link TimeUnit#SECONDS}
|
||||
* @return a new {@link Expiration} configured with the given {@link Long length of time} in {@link TimeUnit}.
|
||||
*/
|
||||
public static Expiration from(long expirationTime, @Nullable TimeUnit timeUnit) {
|
||||
|
||||
if (TimeUnit.NANOSECONDS.equals(timeUnit)
|
||||
|| TimeUnit.MICROSECONDS.equals(timeUnit)
|
||||
|| TimeUnit.MILLISECONDS.equals(timeUnit)) {
|
||||
|
||||
return new Expiration(timeUnit.toMillis(expirationTime), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
return timeUnit != null ? new Expiration(timeUnit.toSeconds(expirationTime), TimeUnit.SECONDS)
|
||||
: new Expiration(expirationTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Expiration} with the given, required {@link Duration}.
|
||||
* <p>
|
||||
* <strong>NOTE: </strong>Please follow the documentation of the individual commands to see if {@link #keepTtl()} is
|
||||
* applicable.
|
||||
* Durations with at least {@literal seconds} resolution uses {@link TimeUnit#SECONDS}. {@link Duration Durations}
|
||||
* in {@literal milliseconds} use {@link TimeUnit#MILLISECONDS}.
|
||||
*
|
||||
* @param duration must not be {@literal null}.
|
||||
* @return a new {@link Expiration} from the given {@link Duration}.
|
||||
* @since 2.0
|
||||
*/
|
||||
public static Expiration from(Duration duration) {
|
||||
|
||||
Assert.notNull(duration, "Duration must not be null");
|
||||
|
||||
return duration.isZero() ? Expiration.persistent()
|
||||
: duration.toMillis() % 1000 == 0 ? new Expiration(duration.getSeconds(), TimeUnit.SECONDS)
|
||||
: new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an {@link Expiration} that indicates to keep the existing one, e.g. when sending a {@code SET} command.
|
||||
* <p>
|
||||
* <strong>NOTE: </strong>Please follow the documentation for the individual commands to see
|
||||
* if keeping the existing TTL is applicable.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
* @since 2.4
|
||||
@@ -140,68 +121,86 @@ public class Expiration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration} with the provided {@link TimeUnit}. Greater units than {@link TimeUnit#SECONDS} are
|
||||
* converted to {@link TimeUnit#SECONDS}. Units smaller than {@link TimeUnit#MILLISECONDS} are converted to
|
||||
* {@link TimeUnit#MILLISECONDS} and can lose precision since {@link TimeUnit#MILLISECONDS} is the smallest
|
||||
* granularity supported by Redis.
|
||||
* Creates a new persistent, non-expiring {@link Expiration}.
|
||||
*
|
||||
* @param expirationTime
|
||||
* @param timeUnit can be {@literal null}. Defaulted to {@link TimeUnit#SECONDS}
|
||||
* @return
|
||||
*/
|
||||
public static Expiration from(long expirationTime, @Nullable TimeUnit timeUnit) {
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(timeUnit, TimeUnit.MICROSECONDS)
|
||||
|| ObjectUtils.nullSafeEquals(timeUnit, TimeUnit.NANOSECONDS)
|
||||
|| ObjectUtils.nullSafeEquals(timeUnit, TimeUnit.MILLISECONDS)) {
|
||||
return new Expiration(timeUnit.toMillis(expirationTime), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if (timeUnit != null) {
|
||||
return new Expiration(timeUnit.toSeconds(expirationTime), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return new Expiration(expirationTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration} with the provided {@link java.time.Duration}. Durations with at least
|
||||
* {@link TimeUnit#SECONDS} resolution use seconds, durations using milliseconds use {@link TimeUnit#MILLISECONDS}
|
||||
* resolution.
|
||||
*
|
||||
* @param duration must not be {@literal null}.
|
||||
* @return
|
||||
* @since 2.0
|
||||
*/
|
||||
public static Expiration from(Duration duration) {
|
||||
|
||||
Assert.notNull(duration, "Duration must not be null");
|
||||
|
||||
if (duration.isZero()) {
|
||||
return Expiration.persistent();
|
||||
}
|
||||
|
||||
if (duration.toMillis() % 1000 == 0) {
|
||||
return new Expiration(duration.getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new persistent {@link Expiration}.
|
||||
*
|
||||
* @return
|
||||
* @return a new persistent, non-expiring {@link Expiration}.
|
||||
*/
|
||||
public static Expiration persistent() {
|
||||
return new Expiration(-1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private final long expirationTime;
|
||||
|
||||
private final TimeUnit timeUnit;
|
||||
|
||||
/**
|
||||
* Creates new {@link Expiration}.
|
||||
*
|
||||
* @param expirationTime {@link Long length of time} for expiration. Defaulted to {@link TimeUnit#SECONDS}.
|
||||
* @param timeUnit {@link TimeUnit} used to measure {@link Long expirationTime}.
|
||||
*/
|
||||
protected Expiration(long expirationTime, @Nullable TimeUnit timeUnit) {
|
||||
|
||||
this.expirationTime = expirationTime;
|
||||
this.timeUnit = timeUnit != null ? timeUnit : TimeUnit.SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Long length of time} for this {@link Expiration}.
|
||||
*
|
||||
* @return the {@link Long length of time} for this {@link Expiration}.
|
||||
*/
|
||||
public long getExpirationTime() {
|
||||
return this.expirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Long expiration time} converted into {@link TimeUnit#MILLISECONDS}.
|
||||
*
|
||||
* @return the expiration time converted into {@link TimeUnit#MILLISECONDS}.
|
||||
*/
|
||||
public long getExpirationTimeInMilliseconds() {
|
||||
return getConverted(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Long expiration time} converted into {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @return the {@link Long expiration time} converted into {@link TimeUnit#SECONDS}.
|
||||
*/
|
||||
public long getExpirationTimeInSeconds() {
|
||||
return getConverted(TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link TimeUnit} for the {@link #getExpirationTime() expiration time}.
|
||||
*
|
||||
* @return the configured {@link TimeUnit} for the {@link #getExpirationTime() expiration time}.
|
||||
*/
|
||||
public TimeUnit getTimeUnit() {
|
||||
return this.timeUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link #getExpirationTime() expiration time} into the given, desired {@link TimeUnit}.
|
||||
*
|
||||
* @param targetTimeUnit {@link TimeUnit} used to convert the {@link #getExpirationTime()} expiration time};
|
||||
* must not be {@literal null}.
|
||||
* @return the {@link #getExpirationTime() expiration time} converted into the given, desired {@link TimeUnit}.
|
||||
* @throws IllegalArgumentException if the given {@link TimeUnit} is {@literal null}.
|
||||
*/
|
||||
public long getConverted(TimeUnit targetTimeUnit) {
|
||||
|
||||
Assert.notNull(targetTimeUnit, "TimeUnit must not be null");
|
||||
|
||||
return targetTimeUnit.convert(getExpirationTime(), getTimeUnit());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if {@link Expiration} is set to persistent.
|
||||
*/
|
||||
public boolean isPersistent() {
|
||||
return expirationTime == -1;
|
||||
return getExpirationTime() == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user