diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java index 131af40ee..0fc28869e 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -16,6 +16,9 @@ package org.springframework.data.redis.cache; +import static org.springframework.util.Assert.*; +import static org.springframework.util.ObjectUtils.*; + import java.util.Arrays; import java.util.Set; @@ -28,7 +31,6 @@ import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.util.Assert; /** * Cache implementation on top of Redis. @@ -40,16 +42,10 @@ import org.springframework.util.Assert; @SuppressWarnings("unchecked") public class RedisCache implements Cache { - private static final byte[] REMOVE_KEYS_BY_PATTERN_LUA = "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(); - private static final int PAGE_SIZE = 128; - private final String name; - @SuppressWarnings("rawtypes") private final RedisTemplate template; - private final byte[] prefix; - private final byte[] setName; - private final byte[] cacheLockName; - private long WAIT_FOR_LOCK = 300; - private final long expiration; + @SuppressWarnings("rawtypes")// + private final RedisTemplate template; + private final RedisCacheMetadata cacheMetadata; + private final CacheValueAccessor cacheValueAccessor; /** * Constructs a new RedisCache instance. @@ -62,41 +58,12 @@ public class RedisCache implements Cache { public RedisCache(String name, byte[] prefix, RedisTemplate template, long expiration) { - Assert.hasText(name, "non-empty cache name is required"); - this.name = name; + hasText(name, "non-empty cache name is required"); + this.cacheMetadata = new RedisCacheMetadata(name, prefix); + this.cacheMetadata.setDefaultExpiration(expiration); + this.template = template; - this.prefix = prefix; - this.expiration = expiration; - - StringRedisSerializer stringSerializer = new StringRedisSerializer(); - - // name of the set holding the keys - this.setName = stringSerializer.serialize(name + "~keys"); - this.cacheLockName = stringSerializer.serialize(name + "~lock"); - } - - public String getName() { - return name; - } - - /** - * {@inheritDoc} This implementation simply returns the RedisTemplate used for configuring the cache, giving access to - * the underlying Redis store. - */ - public Object getNativeCache() { - return template; - } - - public ValueWrapper get(final Object key) { - return (ValueWrapper) template.execute(new RedisCallback() { - - public ValueWrapper doInRedis(RedisConnection connection) throws DataAccessException { - waitForLock(connection); - byte[] bs = connection.get(computeKey(key)); - Object value = template.getValueSerializer() != null ? template.getValueSerializer().deserialize(bs) : bs; - return (bs == null ? null : new SimpleValueWrapper(value)); - } - }, true); + this.cacheValueAccessor = new CacheValueAccessor(template.getValueSerializer()); } /** @@ -114,196 +81,538 @@ public class RedisCache implements Cache { return wrapper == null ? null : (T) wrapper.get(); } - public void put(final Object key, final Object value) { + /* + * (non-Javadoc) + * @see org.springframework.cache.Cache#get(java.lang.Object) + */ + @Override + public ValueWrapper get(Object key) { + return get(new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix()).withKeySerializer( + template.getKeySerializer())); + } - final byte[] keyBytes = computeKey(key); - final byte[] valueBytes = convertToBytesIfNecessary(template.getValueSerializer(), value); + /** + * 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) { - template.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { + notNull(cacheKey, "CacheKey must not be null!"); + return (RedisCacheElement) template.execute(new AbstractRedisCacheCallback( + new RedisCacheElement(cacheKey, null), cacheMetadata) { - waitForLock(connection); + @Override + public RedisCacheElement doInRedis(RedisCacheElement element, RedisConnection connection) + throws DataAccessException { - connection.multi(); - - connection.set(keyBytes, valueBytes); - - if (shouldTrackKeys()) { - connection.zAdd(setName, 0, keyBytes); - } - if (expiration > 0) { - connection.expire(keyBytes, expiration); - // update the expiration of the set of keys as well - - if (shouldTrackKeys()) { - connection.expire(setName, expiration); - } - } - connection.exec(); - - return null; + byte[] bs = connection.get(element.getKeyBytes()); + Object value = template.getValueSerializer() != null ? template.getValueSerializer().deserialize(bs) : bs; + return (bs == null ? null : new RedisCacheElement(element.getKey(), value)); } }, true); } + /* + * (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(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix()).withKeySerializer( + template.getKeySerializer()), value).expireAfter(cacheMetadata.getDefaultExpiration())); + } + + /** + * 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) { + + notNull(element, "Element must not be null!"); + template.execute(new RedisCachePutCallback(element, cacheValueAccessor, cacheMetadata), true); + } + + /* + * (non-Javadoc) + * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object) + */ public ValueWrapper putIfAbsent(Object key, final Object value) { - final byte[] keyBytes = computeKey(key); - final byte[] valueBytes = convertToBytesIfNecessary(template.getValueSerializer(), value); + return putIfAbsent(new RedisCacheElement(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix()) + .withKeySerializer(template.getKeySerializer()), value).expireAfter(cacheMetadata.getDefaultExpiration())); + } - return toWrapper(template.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { + /** + * 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) { - waitForLock(connection); + notNull(element, "Element must not be null!"); + return toWrapper(template.execute(new RedisCachePutIfAbsentCallback(element, cacheValueAccessor, cacheMetadata), + true)); + } - Object resultValue = value; - boolean valueWasSet = connection.setNX(keyBytes, valueBytes); - if (valueWasSet) { + /* + * (non-Javadoc) + * @see org.springframework.cache.Cache#evict(java.lang.Object) + */ + public void evict(Object key) { + evict(new RedisCacheElement(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix()).withKeySerializer( + template.getKeySerializer()), null)); + } - if (shouldTrackKeys()) { - connection.zAdd(setName, 0, keyBytes); - } - if (expiration > 0) { + /** + * @param element {@link RedisCacheElement#getKeyBytes()} + * @since 1.5 + */ + public void evict(final RedisCacheElement element) { - connection.expire(keyBytes, expiration); - // update the expiration of the set of keys as well + notNull(element, "Element must not be null!"); + template.execute(new RedisCacheEvictCallback(element, cacheMetadata)); + } - if (shouldTrackKeys()) { - connection.expire(setName, expiration); - } - } - } else { - resultValue = deserializeIfNecessary(template.getValueSerializer(), connection.get(keyBytes)); - } + /* + * (non-Javadoc) + * @see org.springframework.cache.Cache#clear() + */ + public void clear() { + template.execute(cacheMetadata.usesKeyPrefix() ? new RedisCacheCleanByPrefixCallback(cacheMetadata) + : new RedisCacheCleanByKeysCallback(cacheMetadata), true); + } - return resultValue; - } - }, true)); + /* + * (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 template; } private ValueWrapper toWrapper(Object value) { return (value != null ? new SimpleValueWrapper(value) : null); } - public void evict(Object key) { - final byte[] k = computeKey(key); + /** + * 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 { - template.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - connection.del(k); - // remove key from set - if (shouldTrackKeys()) { - connection.zRem(setName, k); - } - return null; - } - }, true); - } + private final String cacheName; + private final byte[] keyPrefix; + private final byte[] setOfKnownKeys; + private final byte[] cacheLockName; + private long defaultExpiration = 0; - public void clear() { - // need to del each key individually - template.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - // another clear is on-going - if (connection.exists(cacheLockName)) { - return null; - } + /** + * @param cacheName must not be {@literal null} or empty. + * @param keyPrefix can be {@literal null}. + */ + public RedisCacheMetadata(String cacheName, byte[] keyPrefix) { - try { - connection.set(cacheLockName, cacheLockName); + hasText(cacheName, "CacheName must not be null or empty!"); + this.cacheName = cacheName; + this.keyPrefix = keyPrefix; - int offset = 0; - boolean finished = false; + StringRedisSerializer stringSerializer = new StringRedisSerializer(); - if (shouldTrackKeys()) { - do { - // need to paginate the keys - Set keys = connection.zRange(setName, (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(setName); - } else { - - byte[] wildCard = "*".getBytes(); - byte[] prefixToUse = Arrays.copyOf(prefix, prefix.length + wildCard.length); - System.arraycopy(wildCard, 0, prefixToUse, prefix.length, wildCard.length); - - connection.eval(REMOVE_KEYS_BY_PATTERN_LUA, ReturnType.INTEGER, 0, prefixToUse); - } - return null; - } finally { - connection.del(cacheLockName); - } - } - }, true); - } - - private byte[] computeKey(Object key) { - - byte[] keyBytes = convertToBytesIfNecessary(template.getKeySerializer(), key); - - if (prefix == null || prefix.length == 0) { - return keyBytes; + // name of the set holding the keys + this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys"); + this.cacheLockName = stringSerializer.serialize(cacheName + "~lock"); } - byte[] result = Arrays.copyOf(prefix, prefix.length + keyBytes.length); - System.arraycopy(keyBytes, 0, result, prefix.length, keyBytes.length); - - return result; - } - - private boolean waitForLock(RedisConnection connection) { - - boolean retry; - boolean foundLock = false; - do { - retry = false; - if (connection.exists(cacheLockName)) { - foundLock = true; - try { - Thread.sleep(WAIT_FOR_LOCK); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - retry = true; - } - } while (retry); - - return foundLock; - } - - private byte[] convertToBytesIfNecessary(RedisSerializer serializer, Object value) { - - if (serializer == null && value instanceof byte[]) { - return (byte[]) value; + /** + * @return true if the {@link RedisCache} uses a prefix for key ranges. + */ + public boolean usesKeyPrefix() { + return (keyPrefix != null && keyPrefix.length > 0); } - return serializer.serialize(value); - } - - private Object deserializeIfNecessary(RedisSerializer serializer, byte[] value) { - - if (serializer != null) { - return serializer.deserialize(value); + /** + * 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 defaultExpiration + */ + public void setDefaultExpiration(long seconds) { + this.defaultExpiration = seconds; + } + + /** + * Get the default expiration time in seconds. + * + * @return + */ + public long getDefaultExpiration() { + return defaultExpiration; } - return value; } /** - * A set of keys needs to be maintained in case no prefix is provided. Clear cache depends on either a pattern to - * identify keys to remove or a maintained set of known keys, otherwise the entire db would be flushed potentially - * removing non cache entry values. - * - * @return + * @author Christoph Strobl + * @since 1.5 */ - private boolean shouldTrackKeys() { - return (prefix == null || prefix.length == 0); + static class CacheValueAccessor { + + @SuppressWarnings("rawtypes")// + private final RedisSerializer valueSerializer; + + @SuppressWarnings("rawtypes") + CacheValueAccessor(RedisSerializer valueRedisSerializer) { + valueSerializer = valueRedisSerializer; + } + + byte[] convertToBytesIfNecessary(Object value) { + + 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.5 + * @param + */ + static abstract class AbstractRedisCacheCallback implements RedisCallback { + + private long WAIT_FOR_LOCK_TIMEOUT = 300; + private final RedisCacheElement element; + private final RedisCacheMetadata cacheMetadata; + + public AbstractRedisCacheCallback(RedisCacheElement 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(RedisCacheElement 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()); + 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; + } + } + + /** + * @author Christoph Strobl + * @param + * @since 1.5 + */ + static abstract class LockingRedisCacheCallback implements RedisCallback { + + 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 { + + 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 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 { + + 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); + + connection.eval(REMOVE_KEYS_BY_PATTERN_LUA, ReturnType.INTEGER, 0, prefixToUse); + + return null; + } + } + + /** + * @author Christoph Strobl + * @since 1.5 + */ + static class RedisCacheEvictCallback extends AbstractRedisCacheCallback { + + public RedisCacheEvictCallback(RedisCacheElement 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(RedisCacheElement 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 { + + private final CacheValueAccessor valueAccessor; + + public RedisCachePutCallback(RedisCacheElement element, CacheValueAccessor valueAccessor, + RedisCacheMetadata metadata) { + + super(element, metadata); + this.valueAccessor = valueAccessor; + } + + /* + * (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(RedisCacheElement element, RedisConnection connection) throws DataAccessException { + + connection.multi(); + + connection.set(element.getKeyBytes(), valueAccessor.convertToBytesIfNecessary(element.get())); + + processKeyExpiration(element, connection); + maintainKnownKeys(element, connection); + + connection.exec(); + return null; + } + } + + /** + * @author Christoph Strobl + * @since 1.5 + */ + static class RedisCachePutIfAbsentCallback extends AbstractRedisCacheCallback { + + private final CacheValueAccessor valueAccessor; + + public RedisCachePutIfAbsentCallback(RedisCacheElement element, CacheValueAccessor valueAccessor, + RedisCacheMetadata metadata) { + + super(element, metadata); + this.valueAccessor = valueAccessor; + } + + /* + * (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 Object doInRedis(RedisCacheElement element, RedisConnection connection) throws DataAccessException { + + waitForLock(connection); + Object resultValue = put(element, connection); + + if (nullSafeEquals(element.get(), resultValue)) { + processKeyExpiration(element, connection); + maintainKnownKeys(element, connection); + } + + return resultValue; + } + + private Object put(RedisCacheElement element, RedisConnection connection) { + + boolean valueWasSet = connection.setNX(element.getKeyBytes(), + valueAccessor.convertToBytesIfNecessary(element.get())); + return valueWasSet ? element.get() : valueAccessor.deserializeIfNecessary(connection.get(element.getKeyBytes())); + } } } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java new file mode 100644 index 000000000..b9b7a1568 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java @@ -0,0 +1,101 @@ +/* + * 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; + } +} diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java new file mode 100644 index 000000000..8185894ed --- /dev/null +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java @@ -0,0 +1,116 @@ +/* + * 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; + } + +}