Skip to content

Commit 7155628

Browse files
DATAREDIS-481 - Optimize cache locking, update reference documentation, rename tests classes.
1 parent 09422fd commit 7155628

File tree

6 files changed

+152
-42
lines changed

6 files changed

+152
-42
lines changed

Diff for: src/main/asciidoc/reference/redis.adoc

+76-16
Original file line numberDiff line numberDiff line change
@@ -465,30 +465,90 @@ As shown in the example above, the consuming code is decoupled from the actual s
465465
[[redis:support:cache-abstraction]]
466466
=== Support for Spring Cache Abstraction
467467

468+
NOTE: Changed in 2.0
469+
468470
Spring Redis provides an implementation for Spring http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html[cache abstraction] through the `org.springframework.data.redis.cache` package. To use Redis as a backing implementation, simply add `RedisCacheManager` to your configuration:
469471

470-
[source,xml]
472+
[source,java]
473+
----
474+
@Bean
475+
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
476+
return RedisCacheManager.defaultCacheManager(connectionFactory);
477+
}
471478
----
472-
<beans xmlns="http://www.springframework.org/schema/beans"
473-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
474-
xmlns:cache="http://www.springframework.org/schema/cache"
475-
xmlns:c="http://www.springframework.org/schema/c"
476-
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
477-
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
478479

479-
<!-- turn on declarative caching -->
480-
<cache:annotation-driven />
480+
`RedisCacheManager` behavior can be configured via `RedisCacheManagerConfigurator` allowing to set the default `RedisCacheConfiguration`, transaction behaviour and predefined caches.
481481

482-
<!-- declare Redis Cache Manager -->
483-
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager" c:template-ref="redisTemplate"/>
484-
</beans>
482+
[source,java]
483+
----
484+
RedisCacheManager cm = RedisCacheManager.usingRawConnectionFactory(connectionFactory)
485+
.withCacheDefaults(defaultCacheConfig())
486+
.withInitialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
487+
.transactionAware()
488+
.createAndGet();
489+
----
490+
491+
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.
492+
As shown above `RedisCacheManager` allows definition of configurations on a per cache base.
493+
494+
[source,java]
495+
----
496+
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
497+
.entryTtl(Duration.ofSeconds(1))
498+
.disableCachingNullValues();
499+
----
500+
501+
Using default `RedisCacheManager` uses a non locking `RedisCacheWriter` for reading & writing bits.
502+
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.
503+
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.
504+
505+
It is possible to opt in to the locking behavior as follows:
506+
507+
[source,java]
485508
----
509+
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(DefaultRedisCacheWriter.lockingRedisCacheWriter())
510+
.withCacheDefaults(defaultCacheConfig())
511+
...
512+
----
513+
514+
.RedisCacheManager defaults
515+
[width="80%",cols="<1,<2",options="header"]
516+
|====
517+
|Setting
518+
|Value
519+
520+
|Cache Writer
521+
|non locking
486522

487-
NOTE: By default `RedisCacheManager` will lazily initialize `RedisCache` whenever a `Cache` is requested. This can be changed by predefining a `Set` of cache names.
523+
|Cache Configuration
524+
|`RedisCacheConfiguraiton#defaultConfiguration`
488525

489-
NOTE: By default `RedisCacheManager` will not participate in any ongoing transaction. Use `setTransactionAware` to enable transaction support.
526+
|Initial Caches
527+
|none
490528

491-
NOTE: By default `RedisCacheManager` does not prefix keys for cache regions, which can lead to an unexpected growth of a `ZSET` used to maintain known keys. It's highly recommended to enable the usage of prefixes in order to avoid this unexpected growth and potential key clashes using more than one cache region.
529+
|Trasaction Aware
530+
|no
531+
|====
532+
533+
.RedisCacheConfiguration defaults
534+
[width="80%",cols="<1,<2",options="header"]
535+
|====
536+
|Key Expiration
537+
|none
538+
539+
|Cache `null`
540+
|yes
492541

493-
NOTE: By default `RedisCache` will not cache any `null` values as keys without a value get dropped by Redis itself. However you can explicitly enable `null` value caching via `RedisCacheManager` which will store `org.springframework.cache.support.NullValue` as a placeholder.
542+
|Prefix Keys
543+
|yes
544+
545+
|Default Prefix
546+
|the actual cache name
547+
548+
|Key Serializer
549+
|`StringRedisSerializer`
550+
551+
|Value Serializer
552+
|`JdkSerializationRedisSerializer`
553+
|====
494554

Diff for: src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java

+46-13
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@
3030
/**
3131
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
3232
* and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
33-
* {@link RedisConnection}.
33+
* {@link RedisConnection}. <br />
34+
* {@link DefaultRedisCacheWriter} can be used in {@link #lockingRedisCacheWriter(RedisConnectionFactory) locking} or
35+
* {@link #nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking} aims for
36+
* maximum performance it may result in overlapping, non atomic, command execution for operations spanning multiple
37+
* Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap by setting
38+
* an explicit lock key and checking against presence of this key which leads to additional requests and potential
39+
* command wait times.
3440
*
3541
* @author Christoph Strobl
3642
* @since 2.0
3743
*/
38-
class DefaultRedisCacheWriter implements RedisCacheWriter {
44+
public class DefaultRedisCacheWriter implements RedisCacheWriter {
3945

4046
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;"
4147
.getBytes(Charset.forName("UTF-8"));
@@ -64,12 +70,24 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
6470
this.sleepTime = sleepTime;
6571
}
6672

73+
/**
74+
* Create new {@link RedisCacheWriter} without locking behavior.
75+
*
76+
* @param connectionFactory must not be {@literal null}.
77+
* @return new instance of {@link DefaultRedisCacheWriter}.
78+
*/
6779
public static DefaultRedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
6880

6981
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
7082
return new DefaultRedisCacheWriter(connectionFactory);
7183
}
7284

85+
/**
86+
* Create new {@link RedisCacheWriter} with locking behavior.
87+
*
88+
* @param connectionFactory must not be {@literal null}.
89+
* @return new instance of {@link DefaultRedisCacheWriter}.
90+
*/
7391
public static DefaultRedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
7492

7593
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
@@ -115,15 +133,26 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) {
115133

116134
return execute(name, connection -> {
117135

118-
if (connection.setNX(key, value)) {
136+
if (isLockingCacheWriter()) {
137+
doLock(name, connection);
138+
}
119139

120-
if (shouldExpireWithin(ttl)) {
121-
connection.pExpire(key, ttl.toMillis());
140+
try {
141+
if (connection.setNX(key, value)) {
142+
143+
if (shouldExpireWithin(ttl)) {
144+
connection.pExpire(key, ttl.toMillis());
145+
}
146+
return null;
122147
}
123-
return null;
124-
}
125148

126-
return connection.get(key);
149+
return connection.get(key);
150+
} finally {
151+
152+
if (isLockingCacheWriter()) {
153+
doUnlock(name, connection);
154+
}
155+
}
127156
});
128157
}
129158

@@ -184,21 +213,24 @@ public void clean(String name, byte[] pattern) {
184213

185214
execute(name, connection -> {
186215

187-
if (isLockingCacheWriter()) {
188-
doLock(name, connection);
189-
}
216+
boolean wasLocked = false;
190217

191218
try {
192219
if (connection instanceof RedisClusterConnection) {
193220

221+
if (isLockingCacheWriter()) {
222+
doLock(name, connection);
223+
wasLocked = true;
224+
}
225+
194226
byte[][] keys = connection.keys(pattern).stream().toArray(size -> new byte[size][]);
195227
connection.del(keys);
196228
} else {
197229
connection.eval(CLEAN_SCRIPT, ReturnType.INTEGER, 0, pattern);
198230
}
199231
} finally {
200232

201-
if (isLockingCacheWriter()) {
233+
if (wasLocked && isLockingCacheWriter()) {
202234
doUnlock(name, connection);
203235
}
204236
}
@@ -262,11 +294,12 @@ byte[] createCacheLockKey(String name) {
262294
}
263295

264296
/**
265-
* @author Christoph Strobl
266297
* @param <T>
298+
* @author Christoph Strobl
267299
* @since 2.0
268300
*/
269301
interface ConnectionCallback<T> {
302+
270303
T doWithConnection(RedisConnection connection);
271304
}
272305
}

Diff for: src/main/java/org/springframework/data/redis/cache/RedisCache.java

+12-6
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
6363
* @param cacheWriter must not be {@literal null}.
6464
* @param cacheConfig must not be {@literal null}.
6565
*/
66-
public RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
66+
RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
6767

6868
super(cacheConfig.getAllowCacheNullValues());
6969

@@ -227,11 +227,13 @@ protected Object deserializeCacheValue(byte[] value) {
227227
return cacheConfig.getValueSerializationPair().read(ByteBuffer.wrap(value));
228228
}
229229

230-
private byte[] createAndConvertCacheKey(Object key) {
231-
return serializeCacheKey(createCacheKey(key));
232-
}
233-
234-
private String createCacheKey(Object key) {
230+
/**
231+
* Customization hook for creating cache key before it gets serialized.
232+
*
233+
* @param key will never be {@literal null}.
234+
* @return never {@literal null}.
235+
*/
236+
protected String createCacheKey(Object key) {
235237

236238
String convertedKey = conversionService.convert(key, String.class);
237239
if (!cacheConfig.usePrefix()) {
@@ -241,6 +243,10 @@ private String createCacheKey(Object key) {
241243
return prefixCacheKey(convertedKey);
242244
}
243245

246+
private byte[] createAndConvertCacheKey(Object key) {
247+
return serializeCacheKey(createCacheKey(key));
248+
}
249+
244250
private String prefixCacheKey(String key) {
245251
return cacheConfig.getKeyPrefix().orElseGet(() -> name + "::") + key;
246252
}

Diff for: src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ public RedisCacheManagerConfigurator withInitialCacheConfigurations(
294294
*
295295
* @return new instance of {@link RedisCacheManager}.
296296
*/
297-
public RedisCacheManager build() {
297+
public RedisCacheManager createAndGet() {
298298

299299
RedisCacheManager cm = new RedisCacheManager(cacheWriter, defaultCacheConfiguration, intialCaches) {
300300

Diff for: src/test/java/org/springframework/data/redis/cache/NRedisCacheManagerUnitTests.java renamed to src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import org.mockito.junit.MockitoJUnitRunner;
2626
import org.springframework.cache.Cache;
2727
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.data.redis.connection.RedisConnectionFactory;
2830
import org.springframework.test.util.ReflectionTestUtils;
2931

3032
/**
3133
* @author Christoph Strobl
3234
*/
3335
@RunWith(MockitoJUnitRunner.class)
34-
public class NRedisCacheManagerUnitTests {
36+
public class RedisCacheManagerUnitTests {
3537

3638
@Mock RedisCacheWriter cacheWriter;
3739

@@ -40,7 +42,7 @@ public void missingCacheShouldBeCreatedWithDefaultConfiguration() {
4042

4143
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
4244

43-
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter).withCacheDefaults(configuration).build();
45+
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter).withCacheDefaults(configuration).createAndGet();
4446
assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isEqualTo(configuration);
4547
}
4648

@@ -50,7 +52,7 @@ public void predefinedCacheShouldBeCreatedWithSpecificConfig() {
5052
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
5153

5254
RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter)
53-
.withInitialCacheConfigurations(Collections.singletonMap("predefined-cache", configuration)).build();
55+
.withInitialCacheConfigurations(Collections.singletonMap("predefined-cache", configuration)).createAndGet();
5456

5557
assertThat(((RedisCache) cm.getCache("predefined-cache")).getCacheConfiguration()).isEqualTo(configuration);
5658
assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isNotEqualTo(configuration);
@@ -59,11 +61,20 @@ public void predefinedCacheShouldBeCreatedWithSpecificConfig() {
5961
@Test // DATAREDIS-481
6062
public void transactionAwareCacheManagerShouldDecoracteCache() {
6163

62-
Cache cache = RedisCacheManager.usingCacheWriter(cacheWriter).transactionAware().build()
64+
Cache cache = RedisCacheManager.usingCacheWriter(cacheWriter).transactionAware().createAndGet()
6365
.getCache("decoracted-cache");
6466

6567
assertThat(cache).isInstanceOfAny(TransactionAwareCacheDecorator.class);
6668
assertThat(ReflectionTestUtils.getField(cache, "targetCache")).isInstanceOf(RedisCache.class);
6769
}
6870

71+
@Bean
72+
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
73+
return RedisCacheManager.usingRawConnectionFactory(connectionFactory)
74+
.withCacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
75+
.withInitialCacheConfigurations(Collections.singletonMap("predefined", RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
76+
.transactionAware()
77+
.createAndGet();
78+
}
79+
6980
}

Diff for: src/test/java/org/springframework/data/redis/cache/NRedisCacheTests.java renamed to src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* @author Christoph Strobl
5151
*/
5252
@RunWith(Parameterized.class)
53-
public class NRedisCacheTests {
53+
public class RedisCacheTests {
5454

5555
String key = "key-1";
5656
String cacheKey = "cache::" + key;
@@ -66,7 +66,7 @@ public class NRedisCacheTests {
6666
RedisSerializer serializer;
6767
RedisCache cache;
6868

69-
public NRedisCacheTests(RedisConnectionFactory connectionFactory, RedisSerializer serializer) {
69+
public RedisCacheTests(RedisConnectionFactory connectionFactory, RedisSerializer serializer) {
7070

7171
this.connectionFactory = connectionFactory;
7272
this.serializer = serializer;

0 commit comments

Comments
 (0)