DATAREDIS-481 - Revise RedisCache.

RedisCache now operates directly with RedisConnectionFactory allowing each Cache to use its own SerializationPair for cache key and value conversion.

A RedisCacheManager with default settings is created with RedisCacheManager.create(connectionFactory). Use RedisCacheManager.builder(…) to obtain a builder to customize cache settings.

RedisCacheManager cm = RedisCacheManager.builder(connectionFactory)
  .cacheDefaults(defaultCacheConfig())
  .initialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
  .transactionAware()
  .build();

The behavior of a single RedisCache is defined via its RedisCacheConfiguration. Starting from defaultCacheConfig() the configuration can be altered as needed.

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
  .entryTtl(Duration.ofSeconds(1))
  .disableCachingNullValues();

RedisCacheManager defaults to a lock-free RedisCacheWriter for reading & writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non atomic commands, for putIfAbsent and clean methods as those require multiple commands sent to Redis. 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.

Original pull request: #252.
This commit is contained in:
Christoph Strobl
2017-05-30 10:59:32 +02:00
committed by Mark Paluch
parent eb3ad1e351
commit a9c2b1cdda
22 changed files with 1901 additions and 2570 deletions

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2011-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Default implementation for {@link RedisCachePrefix} which uses the given cache name and a delimiter for creating the
* prefix.
*
* @author Costin Leau
*/
public class DefaultRedisCachePrefix implements RedisCachePrefix {
private final RedisSerializer serializer = new StringRedisSerializer();
private final String delimiter;
public DefaultRedisCachePrefix() {
this(":");
}
public DefaultRedisCachePrefix(String delimiter) {
this.delimiter = delimiter;
}
public byte[] prefix(String cacheName) {
return serializer.serialize((delimiter != null ? cacheName.concat(delimiter) : cacheName.concat(":")));
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.types.Expiration;
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 />
* {@link DefaultRedisCacheWriter} can be used in {@link #lockingRedisCacheWriter(RedisConnectionFactory) locking} or
* {@link #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.
*
* @author Christoph Strobl
* @since 2.0
*/
public class DefaultRedisCacheWriter implements RedisCacheWriter {
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;"
.getBytes(Charset.forName("UTF-8"));
private final RedisConnectionFactory connectionFactory;
private final Duration sleepTime;
/**
* @param connectionFactory must not be {@literal null}.
*/
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory) {
this(connectionFactory, Duration.ZERO);
}
/**
* @param connectionFactory must not be {@literal null}.
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
* to disable locking.
*/
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
Assert.notNull(sleepTime, "SleepTime must not be null!");
this.connectionFactory = connectionFactory;
this.sleepTime = sleepTime;
}
/**
* Create new {@link RedisCacheWriter} without locking behavior.
*
* @param connectionFactory must not be {@literal null}.
* @return new instance of {@link DefaultRedisCacheWriter}.
*/
public static DefaultRedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return new DefaultRedisCacheWriter(connectionFactory);
}
/**
* Create new {@link RedisCacheWriter} with locking behavior.
*
* @param connectionFactory must not be {@literal null}.
* @return new instance of {@link DefaultRedisCacheWriter}.
*/
public static DefaultRedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50));
}
@Override
public void put(String name, byte[] key, byte[] value, 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!");
Assert.notNull(ttl, "Ttl 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";
});
}
@Override
public byte[] get(String name, byte[] key) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
return execute(name, connection -> connection.get(key));
}
@Override
public byte[] putIfAbsent(String name, byte[] key, byte[] value, 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!");
Assert.notNull(ttl, "Ttl must not be null!");
return execute(name, connection -> {
if (isLockingCacheWriter()) {
doLock(name, connection);
}
try {
if (connection.setNX(key, value)) {
if (shouldExpireWithin(ttl)) {
connection.pExpire(key, ttl.toMillis());
}
return null;
}
return connection.get(key);
} finally {
if (isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
});
}
@Override
public void remove(String name, byte[] key) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
execute(name, connection -> connection.del(key));
}
/**
* Explicitly set a write lock on a cache.
*
* @param name the name of the cache to lock.
*/
public void lock(String name) {
execute(name, connection -> doLock(name, connection));
}
private Boolean doLock(String name, RedisConnection connection) {
return connection.setNX(createCacheLockKey(name), new byte[] {});
}
/**
* Explicitly remove a write lock from a cache.
*
* @param name the name of the cache to unlock.
*/
public void unlock(String name) {
executeWithoutLockCheck(connection -> doUnlock(name, connection));
}
private Long doUnlock(String name, RedisConnection connection) {
return connection.del(createCacheLockKey(name));
}
/**
* Check if a cache has set a lock.
*
* @param name the name of the cache to check for presence of a lock.
* @return {@literal true} if lock found.
*/
public boolean isLoked(String name) {
return executeWithoutLockCheck(connection -> doCheckLock(name, connection));
}
private boolean doCheckLock(String name, RedisConnection connection) {
return connection.exists(createCacheLockKey(name));
}
@Override
public void clean(String name, byte[] pattern) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(pattern, "Pattern must not be null!");
execute(name, connection -> {
boolean wasLocked = false;
try {
if (connection instanceof RedisClusterConnection) {
if (isLockingCacheWriter()) {
doLock(name, connection);
wasLocked = true;
}
byte[][] keys = connection.keys(pattern).stream().toArray(size -> new byte[size][]);
connection.del(keys);
} else {
connection.eval(CLEAN_SCRIPT, ReturnType.INTEGER, 0, pattern);
}
} finally {
if (wasLocked && isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
return "OK";
});
}
/**
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
*/
public boolean isLockingCacheWriter() {
return !sleepTime.isZero() && !sleepTime.isNegative();
}
<T> T execute(String name, ConnectionCallback<T> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
return callback.doWithConnection(connection);
} finally {
connection.close();
}
}
private <T> T executeWithoutLockCheck(ConnectionCallback<T> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
return callback.doWithConnection(connection);
} finally {
connection.close();
}
}
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
if (isLockingCacheWriter()) {
long timeout = sleepTime.toMillis();
while (doCheckLock(name, connection)) {
try {
Thread.sleep(timeout);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
private boolean shouldExpireWithin(Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}
byte[] createCacheLockKey(String name) {
return (name + "~lock").getBytes(Charset.forName("UTF-8"));
}
/**
* @param <T>
* @author Christoph Strobl
* @since 2.0
*/
interface ConnectionCallback<T> {
T doWithConnection(RedisConnection connection);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import java.time.Duration;
import java.util.Optional;
import org.springframework.cache.Cache;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.Assert;
/**
* Immutable {@link RedisCacheConfiguration} helps customizing {@link RedisCache} behaviour such as caching
* {@literal null} values, cache key prefixes and binary serialization. <br />
* Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from
* there on.
*
* @author Christoph Strobl
* @since 2.0
*/
public class RedisCacheConfiguration {
private final Duration ttl;
private final boolean cacheNullValues;
private final String keyPrefix;
private final boolean usePrefix;
private final SerializationPair<String> keySerializationPair;
private final SerializationPair<?> valueSerializationPair;
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, String keyPrefix,
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = valueSerializationPair;
}
/**
* Default {@link RedisCacheConfiguration} using the following:
* <dl>
* <dt>key expiration</dt>
* <dd>eternal</dd>
* <dt>cache null values</dt>
* <dd>yes</dd>
* <dt>prefix cache keys</dt>
* <dd>yes</dd>
* <dt>default prefix</dt>
* <dd>[the actual cache name]</dd>
* <dt>key serializer</dt>
* <dd>StringRedisSerializer.class</dd>
* <dt>value serializer</dt>
* <dd>JdkSerializationRedisSerializer.class</dd>
* </dl>
*
* @return new {@link RedisCacheConfiguration}.
*/
public static RedisCacheConfiguration defaultCacheConfig() {
return new RedisCacheConfiguration(Duration.ZERO, true, true, null,
SerializationPair.fromSerializer(new StringRedisSerializer()),
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
}
/**
* Set the ttl to apply for cache entries. Use {@link Duration#ZERO} to have an eternal cache.
*
* @param ttl must not be {@literal null}.
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration entryTtl(Duration ttl) {
Assert.notNull(ttl, "Ttl must not be null!");
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
valueSerializationPair);
}
/**
* Use the given prefix instead of the default one.
*
* @param prefix must not be {@literal null}.
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration prefixKeysWith(String prefix) {
Assert.notNull(prefix, "Prefix must not be null!");
return new RedisCacheConfiguration(ttl, cacheNullValues, true, prefix, keySerializationPair,
valueSerializationPair);
}
/**
* Disable caching {@literal null} values. <br />
* <strong>NOTE</strong> any {@link org.springframework.cache.Cache#put(Object, Object)} operation involving
* {@literal null} value will error. Nothing will be written to Redis, nothing will be removed. An already existing
* key will still be there afterwards with the very same value as before.
*
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration disableCachingNullValues() {
return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair);
}
/**
* Disable using cache key prefixes. <br />
* <strong>NOTE</strong>: {@link Cache#clear()} might result in unintended removal of {@literal key}s in Redis. Make
* sure to use a dedicated Redis instance when disabling prefixes.
*
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration disableKeyPrefix() {
return new RedisCacheConfiguration(ttl, cacheNullValues, false, keyPrefix, keySerializationPair,
valueSerializationPair);
}
/**
* Define the {@link SerializationPair} used for de-/serializing cache keys.
*
* @param keySerializationPair must not be {@literal null}.
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration serializeKeysWith(SerializationPair<String> keySerializationPair) {
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null!");
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
valueSerializationPair);
}
/**
* Define the {@link SerializationPair} used for de-/serializing cache values.
*
* @param valueSerializationPair must not be {@literal null}.
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration serializeValuesWith(SerializationPair<?> valueSerializationPair) {
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null!");
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
valueSerializationPair);
}
/**
* @return never {@literal null}.
*/
public Optional<String> getKeyPrefix() {
return Optional.ofNullable(keyPrefix);
}
/**
* @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefix()} if present or the
* default which resolves to {@link Cache#getName()}.
*/
public boolean usePrefix() {
return usePrefix;
}
/**
* @return {@literal true} if caching {@literal null} is allowed.
*/
public boolean getAllowCacheNullValues() {
return cacheNullValues;
}
/**
* @return never {@literal null}.
*/
public SerializationPair<String> getKeySerializationPair() {
return keySerializationPair;
}
/**
* @return never {@literal null}.
*/
public SerializationPair getValueSerializationPair() {
return valueSerializationPair;
}
/**
* @return The expiration time (ttl) for cache entries. Never {@literal null}.
*/
public Duration getTtl() {
return ttl != null ? ttl : Duration.ZERO;
}
}

View File

@@ -1,101 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import static org.springframework.util.Assert.*;
import org.springframework.cache.support.SimpleValueWrapper;
/**
* Element to be stored inside {@link RedisCache}.
*
* @author Christoph Strobl
* @since 1.5
*/
public class RedisCacheElement extends SimpleValueWrapper {
private final RedisCacheKey cacheKey;
private long timeToLive;
/**
* @param cacheKey the key to be used for storing value in {@link RedisCache}. Must not be {@literal null}.
* @param value
*/
public RedisCacheElement(RedisCacheKey cacheKey, Object value) {
super(value);
notNull(cacheKey, "CacheKey must not be null!");
this.cacheKey = cacheKey;
}
/**
* Get the binary key representation.
*
* @return
*/
public byte[] getKeyBytes() {
return cacheKey.getKeyBytes();
}
/**
* @return
*/
public RedisCacheKey getKey() {
return cacheKey;
}
/**
* Set the elements time to live. Use {@literal zero} to store eternally.
*
* @param timeToLive
*/
public void setTimeToLive(long timeToLive) {
this.timeToLive = timeToLive;
}
/**
* @return
*/
public long getTimeToLive() {
return timeToLive;
}
/**
* @return true in case {@link RedisCacheKey} is prefixed.
*/
public boolean hasKeyPrefix() {
return cacheKey.hasPrefix();
}
/**
* @return true if timeToLive is 0
*/
public boolean isEternal() {
return 0 == timeToLive;
}
/**
* Expire the element after given seconds.
*
* @param seconds
* @return
*/
public RedisCacheElement expireAfter(long seconds) {
setTimeToLive(seconds);
return this;
}
}

View File

@@ -1,116 +0,0 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import static org.springframework.util.Assert.*;
import java.util.Arrays;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* @author Christoph Strobl
* @since 1.5
*/
public class RedisCacheKey {
private final Object keyElement;
private byte[] prefix;
@SuppressWarnings("rawtypes")//
private RedisSerializer serializer;
/**
* @param keyElement must not be {@literal null}.
*/
public RedisCacheKey(Object keyElement) {
notNull(keyElement, "KeyElement must not be null!");
this.keyElement = keyElement;
}
/**
* Get the {@link Byte} representation of the given key element using prefix if available.
*/
public byte[] getKeyBytes() {
byte[] rawKey = serializeKeyElement();
if (!hasPrefix()) {
return rawKey;
}
byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);
return prefixedKey;
}
/**
* @return
*/
public Object getKeyElement() {
return keyElement;
}
@SuppressWarnings("unchecked")
private byte[] serializeKeyElement() {
if (serializer == null && keyElement instanceof byte[]) {
return (byte[]) keyElement;
}
return serializer.serialize(keyElement);
}
/**
* Set the {@link RedisSerializer} used for converting the key into its {@link Byte} representation.
*
* @param serializer can be {@literal null}.
*/
public void setSerializer(RedisSerializer<?> serializer) {
this.serializer = serializer;
}
/**
* @return true if prefix is not empty.
*/
public boolean hasPrefix() {
return (prefix != null && prefix.length > 0);
}
/**
* Use the given prefix when generating key.
*
* @param prefix can be {@literal null}.
* @return
*/
public RedisCacheKey usePrefix(byte[] prefix) {
this.prefix = prefix;
return this;
}
/**
* Use {@link RedisSerializer} for converting the key into its {@link Byte} representation.
*
* @param serializer can be {@literal null}.
* @return
*/
public RedisCacheKey withKeySerializer(RedisSerializer serializer) {
this.serializer = serializer;
return this;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2016 the original author or authors.
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,332 +15,306 @@
*/
package org.springframework.data.redis.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.NullValue;
import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* {@link CacheManager} implementation for Redis. By default saves the keys directly, without appending a prefix (which
* acts as a namespace). To avoid clashes, it is recommended to change this (by setting 'usePrefix' to 'true'). <br>
* By default {@link RedisCache}s will be lazily initialized for each {@link #getCache(String)} request unless a set of
* predefined cache names is provided. <br>
* <br>
* Setting {@link #setTransactionAware(boolean)} to {@code true} will force Caches to be decorated as
* {@link TransactionAwareCacheDecorator} so values will only be written to the cache after successful commit of
* surrounding transaction.
*
* @author Costin Leau
* @author Christoph Strobl
* @author Thomas Darimont
* @since 2.0
*/
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
private final Log logger = LogFactory.getLog(RedisCacheManager.class);
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
@SuppressWarnings("rawtypes") //
private final RedisOperations redisOperations;
public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
private boolean usePrefix = false;
private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
private boolean loadRemoteCachesOnStartup = false;
private boolean dynamic = true;
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
// 0 - never expire
private long defaultExpiration = 0;
private Map<String, Long> expires = null;
private Set<String> configuredCacheNames;
private final boolean cacheNullValues;
/**
* Construct a {@link RedisCacheManager}.
*
* @param redisOperations
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations) {
this(redisOperations, Collections.<String> emptyList());
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
this.initialCacheConfiguration = new LinkedHashMap<>();
}
/**
* Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only.
*
* @param redisOperations
* @param cacheNames
* @since 1.2
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
this(redisOperations, cacheNames, false);
}
/**
* Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only. <br />
* <br />
* <strong>NOTE</strong> When enabling {@code cacheNullValues} please make sure the {@link RedisSerializer} used by
* {@link RedisOperations} is capable of serializing {@link NullValue}.
* Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default
* {@link RedisCacheConfiguration}.
*
* @param redisOperations {@link RedisOperations} to work upon.
* @param cacheNames {@link Collection} of known cache names.
* @param cacheNullValues set to {@literal true} to allow caching {@literal null}.
* @since 1.8
* @param cacheWriter must not be {@literal null}.
* @param defaultCacheConfiguration must not be {@literal null}. Maybe just use
* {@link RedisCacheConfiguration#defaultCacheConfig()}.
* @param initialCacheNames optional set of known cache names that will be created with given
* {@literal defaultCacheConfiguration}.
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean cacheNullValues) {
public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
String... initialCacheNames) {
this.redisOperations = redisOperations;
this.cacheNullValues = cacheNullValues;
setCacheNames(cacheNames);
}
this(cacheWriter, defaultCacheConfiguration);
/**
* Specify the set of cache names for this CacheManager's 'static' mode. <br>
* The number of caches and their names will be fixed after a call to this method, with no creation of further cache
* regions at runtime. <br>
* Calling this with a {@code null} or empty collection argument resets the mode to 'dynamic', allowing for further
* creation of caches again.
*/
public void setCacheNames(Collection<String> cacheNames) {
Set<String> newCacheNames = CollectionUtils.isEmpty(cacheNames) ? Collections.<String> emptySet()
: new HashSet<String>(cacheNames);
this.configuredCacheNames = newCacheNames;
this.dynamic = newCacheNames.isEmpty();
}
public void setUsePrefix(boolean usePrefix) {
this.usePrefix = usePrefix;
}
/**
* Sets the cachePrefix. Defaults to 'DefaultRedisCachePrefix').
*
* @param cachePrefix the cachePrefix to set
*/
public void setCachePrefix(RedisCachePrefix cachePrefix) {
this.cachePrefix = cachePrefix;
}
/**
* Sets the default expire time (in seconds).
*
* @param defaultExpireTime time in seconds.
*/
public void setDefaultExpiration(long defaultExpireTime) {
this.defaultExpiration = defaultExpireTime;
}
/**
* Sets the expire time (in seconds) for cache regions (by key).
*
* @param expires time in seconds
*/
public void setExpires(Map<String, Long> expires) {
this.expires = (expires != null ? new ConcurrentHashMap<String, Long>(expires) : null);
}
/**
* If set to {@code true} {@link RedisCacheManager} will try to retrieve cache names from redis server using
* {@literal KEYS} command and initialize {@link RedisCache} for each of them.
*
* @param loadRemoteCachesOnStartup
* @since 1.2
*/
public void setLoadRemoteCachesOnStartup(boolean loadRemoteCachesOnStartup) {
this.loadRemoteCachesOnStartup = loadRemoteCachesOnStartup;
}
/*
* (non-Javadoc)
* @see org.springframework.cache.support.AbstractCacheManager#loadCaches()
*/
@Override
protected Collection<? extends Cache> loadCaches() {
Assert.notNull(this.redisOperations, "A redis template is required in order to interact with data store");
Set<Cache> caches = new LinkedHashSet<Cache>(
loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : new ArrayList<Cache>());
Set<String> cachesToLoad = new LinkedHashSet<String>(this.configuredCacheNames);
cachesToLoad.addAll(this.getCacheNames());
if (!CollectionUtils.isEmpty(cachesToLoad)) {
for (String cacheName : cachesToLoad) {
caches.add(createCache(cacheName));
}
for (String cacheName : initialCacheNames) {
this.initialCacheConfiguration.put(cacheName, defaultCacheConfiguration);
}
}
/**
* Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default
* {@link RedisCacheConfiguration}.
*
* @param cacheWriter must not be {@literal null}.
* @param defaultCacheConfiguration must not be {@literal null}. Maybe just use
* {@link RedisCacheConfiguration#defaultCacheConfig()}.
* @param initialCacheConfigurations Map of known cache names along with the configuration to use for those caches.
* Must not be {@literal null}.
*/
public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
this(cacheWriter, defaultCacheConfiguration);
Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null!");
this.initialCacheConfiguration.putAll(initialCacheConfigurations);
}
/**
* Create a new {@link RedisCacheManager} with defaults applied.
* <dl>
* <dt>locking</dt>
* <dd>disabled</dd>
* <dt>cache configuration</dt>
* <dd>{@link RedisCacheConfiguration#defaultCacheConfig()}</dd>
* <dt>initial caches</dt>
* <dd>none</dd>
* <dt>transaction aware</dt>
* <dd>no</dd>
* </dl>
*
* @param connectionFactory must not be {@literal null}.
* @return new instance of {@link RedisCacheManager}.
*/
public static RedisCacheManager defaultCacheManager(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return new RedisCacheManager(new DefaultRedisCacheWriter(connectionFactory),
RedisCacheConfiguration.defaultCacheConfig());
}
/**
* Entry point for builder style {@link RedisCacheManager} configuration.
*
* @param connectionFactory must not be {@literal null}.
* @return new {@link RedisCacheManagerConfigurator}.
*/
public static RedisCacheManagerConfigurator usingRawConnectionFactory(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return RedisCacheManagerConfigurator.usingRawFactory(connectionFactory);
}
/**
* Entry point for builder style {@link RedisCacheManager} configuration.
*
* @param cacheWriter must not be {@literal null}.
* @return new {@link RedisCacheManagerConfigurator}.
*/
public static RedisCacheManagerConfigurator usingCacheWriter(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
return RedisCacheManagerConfigurator.usingCacheWriter(cacheWriter);
}
@Override
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
caches.add(createRedisCache(entry.getKey(), entry.getValue()));
}
return caches;
}
/**
* Returns a new {@link Collection} of {@link Cache} from the given caches collection and adds the configured
* {@link Cache}s of they are not already present.
*
* @param caches must not be {@literal null}
* @return
*/
protected Collection<? extends Cache> addConfiguredCachesIfNecessary(Collection<? extends Cache> caches) {
Assert.notNull(caches, "Caches must not be null!");
Collection<Cache> result = new ArrayList<Cache>(caches);
for (String cacheName : getCacheNames()) {
boolean configuredCacheAlreadyPresent = false;
for (Cache cache : caches) {
if (cache.getName().equals(cacheName)) {
configuredCacheAlreadyPresent = true;
break;
}
}
if (!configuredCacheAlreadyPresent) {
result.add(getCache(cacheName));
}
}
return result;
}
/**
* Will no longer add the cache to the set of
*
* @param cacheName
* @return
* @deprecated since 1.8 - please use {@link #getCache(String)}.
*/
@Deprecated
protected Cache createAndAddCache(String cacheName) {
Cache cache = super.getCache(cacheName);
return cache != null ? cache : createCache(cacheName);
}
/*
* (non-Javadoc)
* @see org.springframework.cache.support.AbstractCacheManager#getMissingCache(java.lang.String)
*/
@Override
protected Cache getMissingCache(String name) {
return this.dynamic ? createCache(name) : null;
protected RedisCache getMissingCache(String name) {
return createRedisCache(name, defaultCacheConfig);
}
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
long expiration = computeExpiration(cacheName);
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
cacheNullValues);
}
/**
* @return unmodifiable {@link Map} containing cache name / configuration pairs. Never {@literal null}.
*/
public Map<String, RedisCacheConfiguration> getCacheConfigurations() {
protected long computeExpiration(String name) {
Long expiration = null;
if (expires != null) {
expiration = expires.get(name);
}
return (expiration != null ? expiration.longValue() : defaultExpiration);
}
Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(getCacheNames().size());
getCacheNames().forEach(it -> {
protected List<Cache> loadAndInitRemoteCaches() {
List<Cache> caches = new ArrayList<Cache>();
try {
Set<String> cacheNames = loadRemoteCacheKeys();
if (!CollectionUtils.isEmpty(cacheNames)) {
for (String cacheName : cacheNames) {
if (null == super.getCache(cacheName)) {
caches.add(createCache(cacheName));
}
}
}
} catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to initialize cache with remote cache keys.", e);
}
}
return caches;
}
@SuppressWarnings("unchecked")
protected Set<String> loadRemoteCacheKeys() {
return (Set<String>) redisOperations.execute(new RedisCallback<Set<String>>() {
@Override
public Set<String> doInRedis(RedisConnection connection) throws DataAccessException {
// we are using the ~keys postfix as defined in RedisCache#setName
Set<byte[]> keys = connection.keys(redisOperations.getKeySerializer().serialize("*~keys"));
Set<String> cacheKeys = new LinkedHashSet<String>();
if (!CollectionUtils.isEmpty(keys)) {
for (byte[] key : keys) {
cacheKeys.add(redisOperations.getKeySerializer().deserialize(key).toString().replace("~keys", ""));
}
}
return cacheKeys;
}
RedisCache cache = RedisCache.class.cast(lookupCache(it));
configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
});
return Collections.unmodifiableMap(configurationMap);
}
@SuppressWarnings("rawtypes")
protected RedisOperations getRedisOperations() {
return redisOperations;
/**
* Configuration hook for creating {@link RedisCache} with given name and cacheConfig.
*
* @param name must not be {@literal null}.
* @param cacheConfig can be {@literal null}.
* @return never {@literal null}.
*/
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
protected RedisCachePrefix getCachePrefix() {
return cachePrefix;
}
/**
* Configurator for creating {@link RedisCacheManager}.
*
* @author Christoph Strobl
* @since 2.0
*/
public static class RedisCacheManagerConfigurator {
protected boolean isUsePrefix() {
return usePrefix;
}
private final RedisCacheConfiguration defaultCacheConfiguration;
private final RedisCacheWriter cacheWriter;
private final boolean enableTransactions;
private final Map<String, RedisCacheConfiguration> intialCaches = new LinkedHashMap<>();
/* (non-Javadoc)
* @see
org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager#decorateCache(org.springframework.cache.Cache)
*/
@Override
protected Cache decorateCache(Cache cache) {
private RedisCacheManagerConfigurator(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> intialCaches,
boolean enableTransactions) {
if (isCacheAlreadyDecorated(cache)) {
return cache;
this.cacheWriter = cacheWriter;
this.defaultCacheConfiguration = defaultCacheConfiguration;
if (!CollectionUtils.isEmpty(intialCaches)) {
this.intialCaches.putAll(intialCaches);
}
this.enableTransactions = enableTransactions;
}
return super.decorateCache(cache);
}
/**
* Entry point for builder style {@link RedisCacheManager} configuration.
*
* @param connectionFactory must not be {@literal null}.
* @return new {@link RedisCacheManagerConfigurator}.
*/
public static RedisCacheManagerConfigurator usingRawFactory(RedisConnectionFactory connectionFactory) {
protected boolean isCacheAlreadyDecorated(Cache cache) {
return isTransactionAware() && cache instanceof TransactionAwareCacheDecorator;
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return usingCacheWriter(new DefaultRedisCacheWriter(connectionFactory));
}
/**
* Entry point for builder style {@link RedisCacheManager} configuration.
*
* @param cacheWriter must not be {@literal null}.
* @return new {@link RedisCacheManagerConfigurator}.
*/
public static RedisCacheManagerConfigurator usingCacheWriter(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
return new RedisCacheManagerConfigurator(cacheWriter, RedisCacheConfiguration.defaultCacheConfig(), null, false);
}
/**
* Define a default {@link RedisCacheConfiguration} applied to dynamically created {@link RedisCache}s.
*
* @param defaultCacheConfiguration must not be {@literal null}.
* @return new instance of {@link RedisCacheManagerConfigurator}.
*/
public RedisCacheManagerConfigurator withCacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) {
Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, intialCaches,
enableTransactions);
}
/**
* Enable {@link RedisCache}s to synchronize cache put/evict operations with ongoing Spring-managed transactions.
*
* @return new instance of {@link RedisCacheManagerConfigurator}.
*/
public RedisCacheManagerConfigurator transactionAware() {
return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, intialCaches, true);
}
/**
* Append a {@link Set} of cache names to be pre initialized with current {@link RedisCacheConfiguration}.
* <strong>NOTE:</strong> This calls depends on {@link #withCacheDefaults(RedisCacheConfiguration)} using whatever
* default {@link RedisCacheConfiguration} is present at the time of invoking this method.
*
* @param cacheNames must not be {@literal null}.
* @return new instance of {@link RedisCacheManagerConfigurator}.
*/
public RedisCacheManagerConfigurator withInitialCacheNames(Set<String> cacheNames) {
Assert.notNull(cacheNames, "CacheNames must not be null!");
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, defaultCacheConfiguration));
return withInitialCacheConfigurations(cacheConfigMap);
}
/**
* Append a {@link Map} of cache name/{@link RedisCacheConfiguration} pairs to be pre initialized.
*
* @param cacheConfigurations must not be {@literal null}.
* @return new instance of {@link RedisCacheManagerConfigurator}.
*/
public RedisCacheManagerConfigurator withInitialCacheConfigurations(
Map<String, RedisCacheConfiguration> cacheConfigurations) {
Assert.notNull(cacheConfigurations, "CacheConfigurations must not be null!");
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(intialCaches);
cacheConfigMap.putAll(cacheConfigurations);
return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, cacheConfigMap,
enableTransactions);
}
/**
* Create new instance of {@link RedisCacheManager} with configuration options applied.
*
* @return new instance of {@link RedisCacheManager}.
*/
public RedisCacheManager createAndGet() {
RedisCacheManager cm = new RedisCacheManager(cacheWriter, defaultCacheConfiguration, intialCaches) {
boolean hasAlreadyBeenSet;
@Override
public void setTransactionAware(boolean transactionAware) {
if (hasAlreadyBeenSet) {
throw new IllegalStateException(
String.format("CacheManager transaction awareness has already been set to %s.", isTransactionAware()));
}
super.setTransactionAware(transactionAware);
hasAlreadyBeenSet = true;
}
};
cm.setTransactionAware(enableTransactions);
cm.afterPropertiesSet(); // is this a good idea? Still need to think about it.
return cm;
}
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2011-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
/**
* Contract for generating 'prefixes' for Cache keys saved in Redis. Due to the 'flat' nature of the Redis storage, the
* prefix is used as a 'namespace' for grouping the key/values inside a cache (and to avoid collision with other caches
* or keys inside Redis).
*
* @author Costin Leau
*/
public interface RedisCachePrefix {
/**
* Returns the prefix for the given cache (identified by name). Note the prefix is returned in raw form so it can be
* saved directly to Redis without any serialization.
*
* @param cacheName the name of the cache using the prefix
* @return the prefix for the given cache.
*/
byte[] prefix(String cacheName);
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import java.time.Duration;
/**
* {@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.
*
* @author Christoph Strobl
* @since 2.0
*/
public interface RedisCacheWriter {
/**
* Write the given key/value pair to Redis an set the expiration time if defined.
*
* @param name The cache name must not be {@literal null}.
* @param key The key for the cache entry. Must not be {@literal null}.
* @param value The value stored for the key. Must not be {@literal null}.
* @param ttl Optional expiration time. Can be {@literal null}.
*/
void put(String name, byte[] key, byte[] value, Duration ttl);
/**
* Get the binary value representation from Redis stored for the given key.
*
* @param name must not be {@literal null}.
* @param key must not be {@literal null}.
* @return {@literal null} if key does not exist.
*/
byte[] get(String name, byte[] key);
/**
* Write the given value to Redis if the key does not already exist.
*
* @param name The cache name must not be {@literal null}.
* @param key The key for the cache entry. Must not be {@literal null}.
* @param value The value stored for the key. Must not be {@literal null}.
* @param ttl Optional expiration time. Can be {@literal null}.
* @return {@literal null} if the value has been written, the value stored for the key if it already exists.
*/
byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl);
/**
* Remove the given key from Redis.
*
* @param name The cache name must not be {@literal null}.
* @param key The key for the cache entry. Must not be {@literal null}.
*/
void remove(String name, byte[] key);
/**
* Remove all keys following the given pattern.
*
* @param name The cache name must not be {@literal null}.
* @param pattern The pattern for the keys to remove. Must not be {@literal null}.
*/
void clean(String name, byte[] pattern);
}

View File

@@ -101,6 +101,13 @@ public class LettuceConnectionFactory
this(new MutableLettuceClientConfiguration());
}
/**
* Constructs a new {@link LettuceConnectionFactory} instance with default settings.
*/
public LettuceConnectionFactory(RedisStandaloneConfiguration config) {
this(config, new MutableLettuceClientConfiguration());
}
/**
* Constructs a new {@link LettuceConnectionFactory} instance given {@link LettuceClientConfiguration}.
*