Synchronize RedisCache.get(…) with ValueLoader only if value is absent.

RedisCache.get(…) now optimistically fetches the cache value before entering cache-wide synchronization. The previous version synchronized all calls to `get(key, valueLoader)`.

Closes #2079
Original pull request: #2082.
This commit is contained in:
Piotr Mionskowski
2021-06-10 23:04:16 +02:00
committed by Mark Paluch
parent 59562e3125
commit 5e1c2be082
2 changed files with 57 additions and 8 deletions

View File

@@ -44,6 +44,7 @@ import org.springframework.util.ReflectionUtils;
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Piotr Mionskowski
* @see RedisCacheConfiguration
* @see RedisCacheWriter
* @since 2.0
@@ -118,7 +119,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
*/
@Override
@SuppressWarnings("unchecked")
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
public <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
@@ -126,6 +127,17 @@ public class RedisCache extends AbstractValueAdaptingCache {
return (T) result.get();
}
return getSynchronized(key, valueLoader);
}
@SuppressWarnings("unchecked")
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
if (result != null) {
return (T) result.get();
}
T value = valueFromLoader(key, valueLoader);
put(key, value);
return value;

View File

@@ -27,7 +27,12 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
@@ -377,15 +382,47 @@ public class RedisCacheTests {
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> cache.put(key, sample));
}
void doWithConnection(Consumer<RedisConnection> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
callback.accept(connection);
} finally {
connection.close();
}
@ParameterizedRedisTest // GH-2079
void multipleThreadsLoadValueOnce() {
int threadCount = 5;
ConcurrentMap<Integer, Integer> valuesByThreadId = new ConcurrentHashMap<>(threadCount);
CountDownLatch waiter = new CountDownLatch(threadCount);
AtomicInteger threadIds = new AtomicInteger(0);
AtomicInteger currentValueForKey = new AtomicInteger(0);
Stream.generate(threadIds::getAndIncrement)
.limit(threadCount)
.parallel()
.forEach((threadId) -> {
waiter.countDown();
try {
waiter.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer valueForThread = cache.get("key", currentValueForKey::incrementAndGet);
valuesByThreadId.put(threadId, valueForThread);
});
valuesByThreadId.forEach((thread, valueForThread) -> {
assertThat(valueForThread).isEqualTo(currentValueForKey.get());
});
}
void doWithConnection(Consumer<RedisConnection> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
callback.accept(connection);
} finally {
connection.close();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor