Skip to content

Commit 5e1c2be

Browse files
miensolmp911de
authored andcommitted
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.
1 parent 59562e3 commit 5e1c2be

File tree

2 files changed

+57
-8
lines changed

2 files changed

+57
-8
lines changed

src/main/java/org/springframework/data/redis/cache/RedisCache.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
*
4545
* @author Christoph Strobl
4646
* @author Mark Paluch
47+
* @author Piotr Mionskowski
4748
* @see RedisCacheConfiguration
4849
* @see RedisCacheWriter
4950
* @since 2.0
@@ -118,14 +119,25 @@ public RedisCacheWriter getNativeCache() {
118119
*/
119120
@Override
120121
@SuppressWarnings("unchecked")
121-
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
122+
public <T> T get(Object key, Callable<T> valueLoader) {
122123

123124
ValueWrapper result = get(key);
124125

125126
if (result != null) {
126127
return (T) result.get();
127128
}
128129

130+
return getSynchronized(key, valueLoader);
131+
}
132+
133+
@SuppressWarnings("unchecked")
134+
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
135+
ValueWrapper result = get(key);
136+
137+
if (result != null) {
138+
return (T) result.get();
139+
}
140+
129141
T value = valueFromLoader(key, valueLoader);
130142
put(key, value);
131143
return value;

src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727
import java.util.Collection;
2828
import java.util.Collections;
2929
import java.util.Date;
30+
import java.util.concurrent.ConcurrentHashMap;
31+
import java.util.concurrent.ConcurrentMap;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.atomic.AtomicInteger;
3034
import java.util.function.Consumer;
35+
import java.util.stream.Stream;
3136

3237
import org.junit.jupiter.api.BeforeEach;
3338

@@ -377,15 +382,47 @@ void cacheShouldFailOnNonConvertibleCacheKey() {
377382
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> cache.put(key, sample));
378383
}
379384

380-
void doWithConnection(Consumer<RedisConnection> callback) {
381-
RedisConnection connection = connectionFactory.getConnection();
382-
try {
383-
callback.accept(connection);
384-
} finally {
385-
connection.close();
386-
}
385+
@ParameterizedRedisTest // GH-2079
386+
void multipleThreadsLoadValueOnce() {
387+
388+
int threadCount = 5;
389+
390+
ConcurrentMap<Integer, Integer> valuesByThreadId = new ConcurrentHashMap<>(threadCount);
391+
392+
CountDownLatch waiter = new CountDownLatch(threadCount);
393+
394+
AtomicInteger threadIds = new AtomicInteger(0);
395+
396+
AtomicInteger currentValueForKey = new AtomicInteger(0);
397+
398+
Stream.generate(threadIds::getAndIncrement)
399+
.limit(threadCount)
400+
.parallel()
401+
.forEach((threadId) -> {
402+
waiter.countDown();
403+
try {
404+
waiter.await();
405+
} catch (InterruptedException e) {
406+
e.printStackTrace();
407+
}
408+
Integer valueForThread = cache.get("key", currentValueForKey::incrementAndGet);
409+
valuesByThreadId.put(threadId, valueForThread);
410+
});
411+
412+
valuesByThreadId.forEach((thread, valueForThread) -> {
413+
assertThat(valueForThread).isEqualTo(currentValueForKey.get());
414+
});
387415
}
388416

417+
void doWithConnection(Consumer<RedisConnection> callback) {
418+
RedisConnection connection = connectionFactory.getConnection();
419+
try {
420+
callback.accept(connection);
421+
} finally {
422+
connection.close();
423+
}
424+
}
425+
389426
@Data
390427
@NoArgsConstructor
391428
@AllArgsConstructor

0 commit comments

Comments
 (0)