org.springframework.data.redis.cache.RedisCache.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.redis.cache.RedisCache.java

Source

/*
 * Copyright 2011-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 java.util.Arrays;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
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.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.
 * 
 * @author Costin Leau
 * @author Christoph Strobl
 * @author Thomas Darimont
 */
@SuppressWarnings("unchecked")
class RedisCache implements Cache {

    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;

    /**
     * Constructs a new <code>RedisCache</code> instance.
     * 
     * @param name cache name
     * @param prefix
     * @param template
     * @param expiration
     */
    RedisCache(String name, byte[] prefix, RedisTemplate<? extends Object, ? extends Object> template,
            long expiration) {

        Assert.hasText(name, "non-empty cache name is required");
        this.name = name;
        this.template = template;
        this.prefix = prefix;
        this.expiration = expiration;

        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // name of the set holding the keys
        this.setName = ArrayUtils.addAll(prefix, stringSerializer.serialize(name + "~keys"));
        this.cacheLockName = ArrayUtils.addAll(prefix, 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<ValueWrapper>() {

            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);
    }

    /**
     * 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
     * @see DATAREDIS-243
     */
    public <T> T get(Object key, Class<T> type) {

        ValueWrapper wrapper = get(key);
        return wrapper == null ? null : (T) wrapper.get();
    }

    public void put(final Object key, final Object value) {

        final byte[] keyBytes = computeKey(key);
        final byte[] valueBytes = convertToBytesIfNecessary(template.getValueSerializer(), value);

        template.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) throws DataAccessException {

                waitForLock(connection);

                connection.multi();

                connection.set(keyBytes, valueBytes);
                connection.zAdd(setName, 0, keyBytes);

                if (expiration > 0) {
                    connection.expire(keyBytes, expiration);
                    // update the expiration of the set of keys as well
                    connection.expire(setName, expiration);
                }
                connection.exec();

                return null;
            }
        }, true);
    }

    public ValueWrapper putIfAbsent(Object key, final Object value) {

        final byte[] keyBytes = computeKey(key);
        final byte[] valueBytes = convertToBytesIfNecessary(template.getValueSerializer(), value);

        return toWrapper(template.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) throws DataAccessException {

                waitForLock(connection);

                Object resultValue = value;
                boolean valueWasSet = connection.setNX(keyBytes, valueBytes);
                if (valueWasSet) {
                    connection.zAdd(setName, 0, keyBytes);
                    if (expiration > 0) {
                        connection.expire(keyBytes, expiration);
                        // update the expiration of the set of keys as well
                        connection.expire(setName, expiration);
                    }
                } else {
                    resultValue = deserializeIfNecessary(template.getValueSerializer(), connection.get(keyBytes));
                }

                return resultValue;
            }
        }, true));
    }

    private ValueWrapper toWrapper(Object value) {
        return (value != null ? new SimpleValueWrapper(value) : null);
    }

    public void evict(Object key) {
        final byte[] k = computeKey(key);

        template.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                connection.del(k);
                // remove key from set
                connection.zRem(setName, k);
                return null;
            }
        }, true);
    }

    public void clear() {
        // need to del each key individually
        template.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                // another clear is on-going
                if (connection.exists(cacheLockName)) {
                    return null;
                }

                try {
                    connection.set(cacheLockName, cacheLockName);

                    int offset = 0;
                    boolean finished = false;

                    do {
                        // need to paginate the keys
                        Set<byte[]> 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);
                    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;
        }

        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<Object> serializer, Object value) {

        if (serializer == null && value instanceof byte[]) {
            return (byte[]) value;
        }

        return serializer.serialize(value);
    }

    private Object deserializeIfNecessary(RedisSerializer<byte[]> serializer, byte[] value) {

        if (serializer != null) {
            return serializer.deserialize(value);
        }

        return value;
    }

}