Refactor code and cleanup compiler warnings in Redis caching infrastructure.

* Apply Java 17 syntax try-with-resources in DefaultRedisCacheWriter execute methods.
* Organize source code
* Edit Javadoc.

Closes #2733
This commit is contained in:
John Blum
2023-08-10 17:31:20 -07:00
parent df4be6f8b5
commit 747a7d03ee
4 changed files with 112 additions and 81 deletions

View File

@@ -28,18 +28,15 @@ import org.springframework.data.redis.core.ScanOptions;
import org.springframework.util.Assert;
/**
* A collection of predefined {@link BatchStrategy} implementations using {@code KEYS} or {@code SCAN} command.
* Collection of predefined {@link BatchStrategy} implementations using the Redis {@code KEYS} or {@code SCAN} command.
*
* @author Mark Paluch
* @author Christoph Strobl
* @author John Blum
* @since 2.6
*/
public abstract class BatchStrategies {
private BatchStrategies() {
// can't touch this - oh-oh oh oh oh-oh-oh
}
/**
* A {@link BatchStrategy} using a single {@code KEYS} and {@code DEL} command to remove all matching keys.
* {@code KEYS} scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time
@@ -68,6 +65,10 @@ public abstract class BatchStrategies {
return new Scan(batchSize);
}
private BatchStrategies() {
// can't touch this - oh-oh oh oh oh-oh-oh
}
/**
* {@link BatchStrategy} using {@code KEYS}.
*/
@@ -108,9 +109,11 @@ public abstract class BatchStrategies {
long count = 0;
PartitionIterator<byte[]> partitions = new PartitionIterator<>(cursor, batchSize);
while (partitions.hasNext()) {
List<byte[]> keys = partitions.next();
count += keys.size();
if (keys.size() > 0) {
@@ -141,7 +144,7 @@ public abstract class BatchStrategies {
@Override
public boolean hasNext() {
return iterator.hasNext();
return this.iterator.hasNext();
}
@Override
@@ -151,9 +154,10 @@ public abstract class BatchStrategies {
throw new NoSuchElementException();
}
List<T> list = new ArrayList<>(size);
while (list.size() < size && iterator.hasNext()) {
list.add(iterator.next());
List<T> list = new ArrayList<>(this.size);
while (list.size() < this.size && this.iterator.hasNext()) {
list.add(this.iterator.next());
}
return list;

View File

@@ -102,27 +102,6 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
this.batchStrategy = batchStrategy;
}
@Override
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
execute(name, connection -> {
if (shouldExpireWithin(ttl)) {
connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
} else {
connection.set(key, value);
}
return "OK";
});
statistics.incPuts(name);
}
@Override
public byte[] get(String name, byte[] key) {
return get(name, key, null);
@@ -135,8 +114,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
Assert.notNull(key, "Key must not be null");
byte[] result = shouldExpireWithin(ttl)
? execute(name, connection -> connection.getEx(key, Expiration.from(ttl)))
: execute(name, connection -> connection.get(key));
? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl)))
: execute(name, connection -> connection.stringCommands().get(key));
statistics.incGets(name);
@@ -149,6 +128,28 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
return result;
}
@Override
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
execute(name, connection -> {
if (shouldExpireWithin(ttl)) {
connection.stringCommands()
.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
} else {
connection.stringCommands().set(key, value);
}
return "OK";
});
statistics.incPuts(name);
}
@Override
public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
@@ -167,9 +168,9 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
boolean put;
if (shouldExpireWithin(ttl)) {
put = connection.set(key, value, Expiration.from(ttl), SetOption.ifAbsent());
put = isTrue(connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent()));
} else {
put = connection.setNX(key, value);
put = isTrue(connection.stringCommands().setNX(key, value));
}
if (put) {
@@ -177,7 +178,7 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
return null;
}
return connection.get(key);
return connection.stringCommands().get(key);
} finally {
if (isLockingCacheWriter()) {
@@ -193,7 +194,7 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
Assert.notNull(name, "Name must not be null");
Assert.notNull(key, "Key must not be null");
execute(name, connection -> connection.del(key));
execute(name, connection -> connection.keyCommands().del(key));
statistics.incDeletes(name);
}
@@ -257,6 +258,16 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
execute(name, connection -> doLock(name, name, null, connection));
}
@Nullable
private Boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue,
RedisConnection connection) {
Expiration expiration = Expiration.from(this.lockTtl.getTimeToLive(contextualKey, contextualValue));
return connection.stringCommands()
.set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT);
}
/**
* Explicitly remove a write lock from a cache.
*
@@ -266,20 +277,13 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
executeLockFree(connection -> doUnlock(name, connection));
}
private Boolean doLock(String name, Object contextualKey, Object contextualValue, RedisConnection connection) {
Expiration expiration = lockTtl == null ? Expiration.persistent()
: Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue));
return connection.set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT);
}
@Nullable
private Long doUnlock(String name, RedisConnection connection) {
return connection.del(createCacheLockKey(name));
return connection.keyCommands().del(createCacheLockKey(name));
}
boolean doCheckLock(String name, RedisConnection connection) {
return connection.exists(createCacheLockKey(name));
return isTrue(connection.keyCommands().exists(createCacheLockKey(name)));
}
/**
@@ -291,24 +295,16 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
private <T> T execute(String name, Function<RedisConnection, T> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
try (RedisConnection connection = connectionFactory.getConnection()) {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
return callback.apply(connection);
} finally {
connection.close();
}
}
private void executeLockFree(Consumer<RedisConnection> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
try (RedisConnection connection = connectionFactory.getConnection()) {
callback.accept(connection);
} finally {
connection.close();
}
}
@@ -337,11 +333,15 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
}
}
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}
private static byte[] createCacheLockKey(String name) {
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
}
private boolean isTrue(@Nullable Boolean value) {
return Boolean.TRUE.equals(value);
}
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}
}

View File

@@ -24,8 +24,10 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.StringJoiner;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
@@ -37,6 +39,7 @@ import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.data.redis.util.RedisAssertions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -69,7 +72,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
private final String name;
/**
* Create a new {@link RedisCache} with the given {@link String name}.
* Create a new {@link RedisCache} with the given {@link String name} and {@link RedisCacheConfiguration},
* using the {@link RedisCacheWriter} to execute Redis commands supporting the cache operations.
*
* @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
@@ -81,11 +85,11 @@ public class RedisCache extends AbstractValueAdaptingCache {
*/
protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfiguration) {
super(cacheConfiguration.getAllowCacheNullValues());
super(RedisAssertions.requireNonNull(cacheConfiguration, "CacheConfiguration must not be null")
.getAllowCacheNullValues());
Assert.notNull(name, "Name must not be null");
Assert.notNull(cacheWriter, "CacheWriter must not be null");
Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null");
this.name = name;
this.cacheWriter = cacheWriter;
@@ -116,7 +120,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
* 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.
* accessing entries in the cache.
* @see RedisCacheConfiguration#getConversionService()
* @see #getCacheConfiguration()
*/
@@ -163,7 +167,6 @@ public class RedisCache extends AbstractValueAdaptingCache {
try {
ValueWrapper result = get(key);
return result != null ? (T) result.get() : loadCacheValue(key, valueLoader);
} finally {
lock.unlock();
@@ -285,10 +288,19 @@ public class RedisCache extends AbstractValueAdaptingCache {
*/
@Nullable
protected Object preProcessCacheValue(@Nullable Object value) {
return value != null ? value : isAllowNullValues() ? NullValue.INSTANCE : null;
}
@Override
public CompletableFuture<?> retrieve(Object key) {
return super.retrieve(key);
}
@Override
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
return super.retrieve(key, valueLoader);
}
/**
* Serialize the given {@link String cache key}.
*
@@ -321,7 +333,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
*
* @param value array of bytes to deserialize; must not be {@literal null}.
* @return an {@link Object} deserialized from the array of bytes using the configured value
* {@link RedisSerializationContext.SerializationPair}; can be {@literal null}.
* {@link RedisSerializationContext.SerializationPair}; can be {@literal null}.
* @see RedisCacheConfiguration#getValueSerializationPair()
*/
@Nullable
@@ -382,9 +394,8 @@ public class RedisCache extends AbstractValueAdaptingCache {
return key.toString();
}
String message = String.format(
"Cannot convert cache key %s to String; Please register a suitable Converter"
+ " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'",
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());
throw new IllegalStateException(message);

View File

@@ -32,12 +32,13 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link CacheManager} backed by a {@link RedisCache}.
* {@link CacheManager} implementation for Redis backed by {@link RedisCache}.
* <p>
* This {@link CacheManager} creates {@link Cache caches} by default upon first write. Empty {@link Cache caches}
* This {@link CacheManager} creates {@link Cache caches} on first write, by default. Empty {@link Cache caches}
* are not visible in Redis due to how Redis represents empty data structures.
* <p>
* {@link Cache Caches} requiring a different {@link RedisCacheConfiguration} than the default cache configuration
* {@link Cache Caches} requiring a different {@link RedisCacheConfiguration cache configuration}
* than the {@link RedisCacheConfiguration#defaultCacheConfig() default cache configuration}
* can be specified via {@link RedisCacheManagerBuilder#withInitialCacheConfigurations(Map)} or individually
* using {@link RedisCacheManagerBuilder#withCacheConfiguration(String, RedisCacheConfiguration)}.
*
@@ -45,7 +46,10 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @author Yanming Zhou
* @author John Blum
* @see org.springframework.cache.Cache
* @see org.springframework.cache.CacheManager
* @see org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager
* @see org.springframework.data.redis.connection.RedisConnectionFactory
* @see org.springframework.data.redis.cache.RedisCacheConfiguration
* @see org.springframework.data.redis.cache.RedisCacheWriter
* @since 2.0
@@ -100,7 +104,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Factory method used to construct a new {@link RedisCacheManager} initialized with
* the given {@link RedisConnectionFactory} and using the defaults for caching.
* the given {@link RedisConnectionFactory} and using {@link RedisCacheConfiguration#defaultCacheConfig() defaults}
* for caching.
*
* <dl>
* <dt>locking</dt>
@@ -127,7 +132,7 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory),
return new RedisCacheManager(org.springframework.data.redis.cache.RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory),
RedisCacheConfiguration.defaultCacheConfig());
}
@@ -359,6 +364,17 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
return Collections.unmodifiableMap(this.initialCacheConfiguration);
}
/**
* Returns a reference to the configured {@link RedisCacheWriter} used to perform {@link RedisCache} operations,
* such as reading from and writing to the cache.
*
* @return a reference to the configured {@link RedisCacheWriter}.
* @see org.springframework.data.redis.cache.RedisCacheWriter
*/
protected RedisCacheWriter getCacheWriter() {
return this.cacheWriter;
}
@Override
protected RedisCache getMissingCache(String name) {
return isAllowRuntimeCacheCreation() ? createRedisCache(name, getDefaultCacheConfiguration()) : null;
@@ -373,7 +389,7 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
* @return a new {@link RedisCache} instance; never {@literal null}.
*/
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfiguration) {
return new RedisCache(name, cacheWriter, resolveCacheConfiguration(cacheConfiguration));
return new RedisCache(name, getCacheWriter(), resolveCacheConfiguration(cacheConfiguration));
}
@Override
@@ -630,19 +646,19 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
Assert.state(cacheWriter != null, "CacheWriter must not be null;"
+ " You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'");
RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(statisticsCollector)
? cacheWriter.withStatisticsCollector(statisticsCollector)
: cacheWriter;
RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(this.statisticsCollector)
? this.cacheWriter.withStatisticsCollector(this.statisticsCollector)
: this.cacheWriter;
RedisCacheManager cacheManager = newRedisCacheManager(resolvedCacheWriter);
cacheManager.setTransactionAware(enableTransactions);
cacheManager.setTransactionAware(this.enableTransactions);
return cacheManager;
}
private RedisCacheManager newRedisCacheManager(RedisCacheWriter cacheWriter) {
return new RedisCacheManager(cacheWriter, cacheDefaults(), allowRuntimeCacheCreation, initialCaches);
return new RedisCacheManager(cacheWriter, cacheDefaults(), this.allowRuntimeCacheCreation, this.initialCaches);
}
}
}