/* * Copyright 2011-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.lang.reflect.Constructor; import java.util.Arrays; import java.util.Set; import java.util.concurrent.Callable; import org.springframework.cache.support.AbstractValueAdaptingCache; import org.springframework.cache.support.NullValue; import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DecoratedRedisConnection; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * Cache implementation on top of Redis. * * @author Costin Leau * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch */ @SuppressWarnings("unchecked") public class RedisCache extends AbstractValueAdaptingCache { @SuppressWarnings("rawtypes") // private final RedisOperations redisOperations; private final RedisCacheMetadata cacheMetadata; private final CacheValueAccessor cacheValueAccessor; /** * Constructs a new {@link RedisCache} instance. * * @param name cache name * @param prefix * @param redisOperations * @param expiration */ public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration) { this(name, prefix, redisOperations, expiration, false); } /** * Constructs a new {@link RedisCache} instance. * * @param name cache name * @param prefix must not be {@literal null} or empty. * @param redisOperations * @param expiration * @param allowNullValues * @since 1.8 */ public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, boolean allowNullValues) { super(allowNullValues); Assert.hasText(name, "CacheName must not be null or empty!"); RedisSerializer<?> serializer = redisOperations.getValueSerializer() != null ? redisOperations.getValueSerializer() : (RedisSerializer<?>) new JdkSerializationRedisSerializer(); this.cacheMetadata = new RedisCacheMetadata(name, prefix); this.cacheMetadata.setDefaultExpiration(expiration); this.redisOperations = redisOperations; this.cacheValueAccessor = new CacheValueAccessor(serializer); if (allowNullValues) { if (redisOperations.getValueSerializer() instanceof StringRedisSerializer || redisOperations.getValueSerializer() instanceof GenericToStringSerializer || redisOperations.getValueSerializer() instanceof Jackson2JsonRedisSerializer) { throw new IllegalArgumentException(String.format( "Redis does not allow keys with null value ¯\\_(ツ)_/¯. " + "The chosen %s does not support generic type handling and therefore cannot be used with allowNullValues enabled. " + "Please use a different RedisSerializer or disable null value support.", ClassUtils.getShortName(redisOperations.getValueSerializer().getClass()))); } } } /** * Return the value to which this cache maps the specified key, generically specifying a type that return value will * be cast to. * * @param key * @param type * @return */ public <T> T get(Object key, Class<T> type) { ValueWrapper wrapper = get(key); return wrapper == null ? null : (T) wrapper.get(); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#get(java.lang.Object) */ @Override public ValueWrapper get(Object key) { return get(getRedisCacheKey(key)); } /* * @see org.springframework.cache.Cache#get(java.lang.Object, java.util.concurrent.Callable) * introduced in springframework 4.3.0.RC1 */ public <T> T get(final Object key, final Callable<T> valueLoader) { RedisCacheElement cacheElement = new RedisCacheElement(getRedisCacheKey(key), new StoreTranslatingCallable(valueLoader)).expireAfter(cacheMetadata.getDefaultExpiration()); BinaryRedisCacheElement rce = new BinaryRedisCacheElement(cacheElement, cacheValueAccessor); ValueWrapper val = get(key); if (val != null) { return (T) val.get(); } RedisWriteThroughCallback callback = new RedisWriteThroughCallback(rce, cacheMetadata); try { byte[] result = (byte[]) redisOperations.execute(callback); return (T) (result == null ? null : fromStoreValue(cacheValueAccessor.deserializeIfNecessary(result))); } catch (RuntimeException e) { throw CacheValueRetrievalExceptionFactory.INSTANCE.create(key, valueLoader, e); } } /** * Return the value to which this cache maps the specified key. * * @param cacheKey the key whose associated value is to be returned via its binary representation. * @return the {@link RedisCacheElement} stored at given key or {@literal null} if no value found for key. * @since 1.5 */ public RedisCacheElement get(final RedisCacheKey cacheKey) { Assert.notNull(cacheKey, "CacheKey must not be null!"); Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.exists(cacheKey.getKeyBytes()); } }); if (!exists.booleanValue()) { return null; } return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey))); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object) */ @Override public void put(final Object key, final Object value) { put(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value)) .expireAfter(cacheMetadata.getDefaultExpiration())); } /* * (non-Javadoc) * @see org.springframework.cache.support.AbstractValueAdaptingCache#fromStoreValue(java.lang.Object) */ @Override protected Object fromStoreValue(Object storeValue) { // we need this override for the GenericJackson2JsonRedisSerializer support. if (isAllowNullValues() && storeValue instanceof NullValue) { return null; } return super.fromStoreValue(storeValue); } /** * Add the element by adding {@link RedisCacheElement#get()} at {@link RedisCacheElement#getKeyBytes()}. If the cache * previously contained a mapping for this {@link RedisCacheElement#getKeyBytes()}, the old value is replaced by * {@link RedisCacheElement#get()}. * * @param element must not be {@literal null}. * @since 1.5 */ public void put(RedisCacheElement element) { Assert.notNull(element, "Element must not be null!"); redisOperations .execute(new RedisCachePutCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata)); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object) */ public ValueWrapper putIfAbsent(Object key, final Object value) { return putIfAbsent(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value)) .expireAfter(cacheMetadata.getDefaultExpiration())); } /** * Add the element as long as no element exists at {@link RedisCacheElement#getKeyBytes()}. If a value is present for * {@link RedisCacheElement#getKeyBytes()} this one is returned. * * @param element must not be {@literal null}. * @return * @since 1.5 */ public ValueWrapper putIfAbsent(RedisCacheElement element) { Assert.notNull(element, "Element must not be null!"); new RedisCachePutIfAbsentCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata); return toWrapper(cacheValueAccessor.deserializeIfNecessary((byte[]) redisOperations.execute( new RedisCachePutIfAbsentCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata)))); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#evict(java.lang.Object) */ public void evict(Object key) { evict(new RedisCacheElement(getRedisCacheKey(key), null)); } /** * @param element {@link RedisCacheElement#getKeyBytes()} * @since 1.5 */ public void evict(final RedisCacheElement element) { Assert.notNull(element, "Element must not be null!"); redisOperations .execute(new RedisCacheEvictCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata)); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#clear() */ public void clear() { redisOperations.execute(cacheMetadata.usesKeyPrefix() ? new RedisCacheCleanByPrefixCallback(cacheMetadata) : new RedisCacheCleanByKeysCallback(cacheMetadata)); } /* * (non-Javadoc) * @see org.springframework.cache.Cache#getName() */ public String getName() { return cacheMetadata.getCacheName(); } /** * {@inheritDoc} This implementation simply returns the RedisTemplate used for configuring the cache, giving access to * the underlying Redis store. */ public Object getNativeCache() { return redisOperations; } private ValueWrapper toWrapper(Object value) { return (value != null ? new SimpleValueWrapper(value) : null); } /* * (non-Javadoc) * @see org.springframework.cache.support.AbstractValueAdaptingCache#lookup(java.lang.Object) */ @Override protected Object lookup(Object key) { RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key); byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>( new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) { @Override public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException { return connection.get(element.getKeyBytes()); } }); return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes); } private RedisCacheKey getRedisCacheKey(Object key) { return new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix()) .withKeySerializer(redisOperations.getKeySerializer()); } /** * {@link Callable} to transform a value obtained from another {@link Callable} to its store value. * * @author Mark Paluch * @since 1.8 * @see #toStoreValue(Object) */ private class StoreTranslatingCallable implements Callable<Object> { private Callable<?> valueLoader; public StoreTranslatingCallable(Callable<?> valueLoader) { this.valueLoader = valueLoader; } @Override public Object call() throws Exception { return toStoreValue(valueLoader.call()); } } /** * Metadata required to maintain {@link RedisCache}. Keeps track of additional data structures required for processing * cache operations. * * @author Christoph Strobl * @since 1.5 */ static class RedisCacheMetadata { private final String cacheName; private final byte[] keyPrefix; private final byte[] setOfKnownKeys; private final byte[] cacheLockName; private long defaultExpiration = 0; /** * @param cacheName must not be {@literal null} or empty. * @param keyPrefix can be {@literal null}. */ public RedisCacheMetadata(String cacheName, byte[] keyPrefix) { Assert.hasText(cacheName, "CacheName must not be null or empty!"); this.cacheName = cacheName; this.keyPrefix = keyPrefix; StringRedisSerializer stringSerializer = new StringRedisSerializer(); // name of the set holding the keys this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys"); this.cacheLockName = stringSerializer.serialize(cacheName + "~lock"); } /** * @return true if the {@link RedisCache} uses a prefix for key ranges. */ public boolean usesKeyPrefix() { return (keyPrefix != null && keyPrefix.length > 0); } /** * Get the binary representation of the key prefix. * * @return never {@literal null}. */ public byte[] getKeyPrefix() { return this.keyPrefix; } /** * Get the binary representation of the key identifying the data structure used to maintain known keys. * * @return never {@literal null}. */ public byte[] getSetOfKnownKeysKey() { return setOfKnownKeys; } /** * Get the binary representation of the key identifying the data structure used to lock the cache. * * @return never {@literal null}. */ public byte[] getCacheLockKey() { return cacheLockName; } /** * Get the name of the cache. * * @return */ public String getCacheName() { return cacheName; } /** * Set the default expiration time in seconds * * @param seconds */ public void setDefaultExpiration(long seconds) { this.defaultExpiration = seconds; } /** * Get the default expiration time in seconds. * * @return */ public long getDefaultExpiration() { return defaultExpiration; } } /** * @author Christoph Strobl * @since 1.5 */ static class CacheValueAccessor { @SuppressWarnings("rawtypes") // private final RedisSerializer valueSerializer; @SuppressWarnings("rawtypes") CacheValueAccessor(RedisSerializer valueRedisSerializer) { valueSerializer = valueRedisSerializer; } byte[] convertToBytesIfNecessary(Object value) { if (value == null) { return new byte[0]; } if (valueSerializer == null && value instanceof byte[]) { return (byte[]) value; } return valueSerializer.serialize(value); } Object deserializeIfNecessary(byte[] value) { if (valueSerializer != null) { return valueSerializer.deserialize(value); } return value; } } /** * @author Christoph Strobl * @since 1.6 */ static class BinaryRedisCacheElement extends RedisCacheElement { private byte[] keyBytes; private byte[] valueBytes; private RedisCacheElement element; private boolean lazyLoad; private CacheValueAccessor accessor; public BinaryRedisCacheElement(RedisCacheElement element, CacheValueAccessor accessor) { super(element.getKey(), element.get()); this.element = element; this.keyBytes = element.getKeyBytes(); this.accessor = accessor; lazyLoad = element.get() instanceof Callable; this.valueBytes = lazyLoad ? null : accessor.convertToBytesIfNecessary(element.get()); } @Override public byte[] getKeyBytes() { return keyBytes; } public long getTimeToLive() { return element.getTimeToLive(); } public boolean hasKeyPrefix() { return element.hasKeyPrefix(); } public boolean isEternal() { return element.isEternal(); } public RedisCacheElement expireAfter(long seconds) { return element.expireAfter(seconds); } @Override public byte[] get() { if (lazyLoad && valueBytes == null) { try { valueBytes = accessor.convertToBytesIfNecessary(((Callable<?>) element.get()).call()); } catch (Exception e) { throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e.getMessage(), e); } } return valueBytes; } } /** * @author Christoph Strobl * @since 1.5 * @param <T> */ static abstract class AbstractRedisCacheCallback<T> implements RedisCallback<T> { private long WAIT_FOR_LOCK_TIMEOUT = 300; private final BinaryRedisCacheElement element; private final RedisCacheMetadata cacheMetadata; public AbstractRedisCacheCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) { this.element = element; this.cacheMetadata = metadata; } /* * (non-Javadoc) * @see org.springframework.data.redis.core.RedisCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection) */ @Override public T doInRedis(RedisConnection connection) throws DataAccessException { waitForLock(connection); return doInRedis(element, connection); } public abstract T doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException; protected void processKeyExpiration(RedisCacheElement element, RedisConnection connection) { if (!element.isEternal()) { connection.expire(element.getKeyBytes(), element.getTimeToLive()); } } protected void maintainKnownKeys(RedisCacheElement element, RedisConnection connection) { if (!element.hasKeyPrefix()) { connection.zAdd(cacheMetadata.getSetOfKnownKeysKey(), 0, element.getKeyBytes()); if (!element.isEternal()) { connection.expire(cacheMetadata.getSetOfKnownKeysKey(), element.getTimeToLive()); } } } protected void cleanKnownKeys(RedisCacheElement element, RedisConnection connection) { if (!element.hasKeyPrefix()) { connection.zRem(cacheMetadata.getSetOfKnownKeysKey(), element.getKeyBytes()); } } protected boolean waitForLock(RedisConnection connection) { boolean retry; boolean foundLock = false; do { retry = false; if (connection.exists(cacheMetadata.getCacheLockKey())) { foundLock = true; try { Thread.sleep(WAIT_FOR_LOCK_TIMEOUT); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } retry = true; } } while (retry); return foundLock; } protected void lock(RedisConnection connection) { waitForLock(connection); connection.set(cacheMetadata.getCacheLockKey(), "locked".getBytes()); } protected void unlock(RedisConnection connection) { connection.del(cacheMetadata.getCacheLockKey()); } } /** * @author Christoph Strobl * @param <T> * @since 1.5 */ static abstract class LockingRedisCacheCallback<T> implements RedisCallback<T> { private final RedisCacheMetadata metadata; public LockingRedisCacheCallback(RedisCacheMetadata metadata) { this.metadata = metadata; } /* * (non-Javadoc) * @see org.springframework.data.redis.core.RedisCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection) */ @Override public T doInRedis(RedisConnection connection) throws DataAccessException { if (connection.exists(metadata.getCacheLockKey())) { return null; } try { connection.set(metadata.getCacheLockKey(), metadata.getCacheLockKey()); return doInLock(connection); } finally { connection.del(metadata.getCacheLockKey()); } } public abstract T doInLock(RedisConnection connection); } /** * @author Christoph Strobl * @since 1.5 */ static class RedisCacheCleanByKeysCallback extends LockingRedisCacheCallback<Void> { private static final int PAGE_SIZE = 128; private final RedisCacheMetadata metadata; RedisCacheCleanByKeysCallback(RedisCacheMetadata metadata) { super(metadata); this.metadata = metadata; } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCache.LockingRedisCacheCallback#doInLock(org.springframework.data.redis.connection.RedisConnection) */ @Override public Void doInLock(RedisConnection connection) { int offset = 0; boolean finished = false; do { // need to paginate the keys Set<byte[]> keys = connection.zRange(metadata.getSetOfKnownKeysKey(), (offset) * PAGE_SIZE, (offset + 1) * PAGE_SIZE - 1); finished = keys.size() < PAGE_SIZE; offset++; if (!keys.isEmpty()) { connection.del(keys.toArray(new byte[keys.size()][])); } } while (!finished); connection.del(metadata.getSetOfKnownKeysKey()); return null; } } /** * @author Christoph Strobl * @since 1.5 */ static class RedisCacheCleanByPrefixCallback extends LockingRedisCacheCallback<Void> { private static final byte[] REMOVE_KEYS_BY_PATTERN_LUA = new StringRedisSerializer().serialize( "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;"); private static final byte[] WILD_CARD = new StringRedisSerializer().serialize("*"); private final RedisCacheMetadata metadata; public RedisCacheCleanByPrefixCallback(RedisCacheMetadata metadata) { super(metadata); this.metadata = metadata; } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCache.LockingRedisCacheCallback#doInLock(org.springframework.data.redis.connection.RedisConnection) */ @Override public Void doInLock(RedisConnection connection) throws DataAccessException { byte[] prefixToUse = Arrays.copyOf(metadata.getKeyPrefix(), metadata.getKeyPrefix().length + WILD_CARD.length); System.arraycopy(WILD_CARD, 0, prefixToUse, metadata.getKeyPrefix().length, WILD_CARD.length); if (isClusterConnection(connection)) { // load keys to the client because currently Redis Cluster connections do not allow eval of lua scripts. Set<byte[]> keys = connection.keys(prefixToUse); if (!keys.isEmpty()) { connection.del(keys.toArray(new byte[keys.size()][])); } } else { connection.eval(REMOVE_KEYS_BY_PATTERN_LUA, ReturnType.INTEGER, 0, prefixToUse); } return null; } } /** * @author Christoph Strobl * @since 1.5 */ static class RedisCacheEvictCallback extends AbstractRedisCacheCallback<Void> { public RedisCacheEvictCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) { super(element, metadata); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisCacheCallback#doInRedis(org.springframework.data.redis.cache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection) */ @Override public Void doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException { connection.del(element.getKeyBytes()); cleanKnownKeys(element, connection); return null; } } /** * @author Christoph Strobl * @since 1.5 */ static class RedisCachePutCallback extends AbstractRedisCacheCallback<Void> { public RedisCachePutCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) { super(element, metadata); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisPutCallback#doInRedis(org.springframework.data.redis.cache.RedisCache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection) */ @Override public Void doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException { if (!isClusterConnection(connection)) { connection.multi(); } if (element.get().length == 0) { connection.del(element.getKeyBytes()); } else { connection.set(element.getKeyBytes(), element.get()); processKeyExpiration(element, connection); maintainKnownKeys(element, connection); } if (!isClusterConnection(connection)) { connection.exec(); } return null; } } /** * @author Christoph Strobl * @since 1.5 */ static class RedisCachePutIfAbsentCallback extends AbstractRedisCacheCallback<byte[]> { public RedisCachePutIfAbsentCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) { super(element, metadata); } /* * (non-Javadoc) * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisPutCallback#doInRedis(org.springframework.data.redis.cache.RedisCache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection) */ @Override public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException { waitForLock(connection); byte[] keyBytes = element.getKeyBytes(); byte[] value = element.get(); if (!connection.setNX(keyBytes, value)) { return connection.get(keyBytes); } maintainKnownKeys(element, connection); processKeyExpiration(element, connection); return null; } } /** * @author Christoph Strobl * @since 1.7 */ static class RedisWriteThroughCallback extends AbstractRedisCacheCallback<byte[]> { public RedisWriteThroughCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) { super(element, metadata); } @Override public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException { try { lock(connection); try { byte[] value = connection.get(element.getKeyBytes()); if (value != null) { return value; } if (!isClusterConnection(connection)) { connection.watch(element.getKeyBytes()); connection.multi(); } value = element.get(); if (value.length == 0) { connection.del(element.getKeyBytes()); } else { connection.set(element.getKeyBytes(), value); processKeyExpiration(element, connection); maintainKnownKeys(element, connection); } if (!isClusterConnection(connection)) { connection.exec(); } return value; } catch (RuntimeException e) { if (!isClusterConnection(connection)) { connection.discard(); } throw e; } } finally { unlock(connection); } } }; /** * @author Christoph Strobl * @since 1.7 (TODO: remove when upgrading to spring 4.3) */ private static enum CacheValueRetrievalExceptionFactory { INSTANCE; private static boolean isSpring43; static { isSpring43 = ClassUtils.isPresent("org.springframework.cache.Cache$ValueRetrievalException", ClassUtils.getDefaultClassLoader()); } public RuntimeException create(Object key, Callable<?> valueLoader, Throwable cause) { if (isSpring43) { try { Class<?> execption = ClassUtils.forName("org.springframework.cache.Cache$ValueRetrievalException", this.getClass().getClassLoader()); Constructor<?> c = ClassUtils.getConstructorIfAvailable(execption, Object.class, Callable.class, Throwable.class); return (RuntimeException) c.newInstance(key, valueLoader, cause); } catch (Exception ex) { // ignore } } return new RedisSystemException( String.format("Value for key '%s' could not be loaded using '%s'.", key, valueLoader), cause); } } private static boolean isClusterConnection(RedisConnection connection) { while (connection instanceof DecoratedRedisConnection) { connection = ((DecoratedRedisConnection) connection).getDelegate(); } return connection instanceof RedisClusterConnection; } }