grails.plugin.cache.redis.GrailsRedisCache.java Source code

Java tutorial

Introduction

Here is the source code for grails.plugin.cache.redis.GrailsRedisCache.java

Source

/* Copyright 2012 SpringSource.
 *
 * 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 grails.plugin.cache.redis;

import grails.plugin.cache.GrailsCache;
import grails.plugin.cache.GrailsValueWrapper;
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;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * Based on package-scope org.springframework.data.redis.cache.RedisCache.
 *
 * @author Costin Leau
 * @author Burt Beckwith
 */
public class GrailsRedisCache implements GrailsCache {

    public static final long NEVER_EXPIRE = 0;

    protected static final int PAGE_SIZE = 128;

    protected final String name;
    @SuppressWarnings("rawtypes")
    protected final RedisTemplate template;
    protected final byte[] prefix;
    protected final byte[] setName;
    protected final byte[] cacheLockName;
    protected long WAIT_FOR_LOCK = 300;
    protected long ttl;

    /**
     * Constructor.
     *
     * @param name        cache name
     * @param prefix
     * @param cachePrefix
     */
    public GrailsRedisCache(String name, byte[] prefix, RedisTemplate<? extends Object, ? extends Object> template,
            Long ttl) {
        Assert.hasText(name, "non-empty cache name is required");

        this.name = name;
        this.template = template;
        this.prefix = prefix;
        this.ttl = ttl == null ? NEVER_EXPIRE : ttl.longValue();

        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // name of the set holding the keys
        setName = stringSerializer.serialize(name + "~keys");
        cacheLockName = stringSerializer.serialize(name + "~lock");
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation simply returns the RedisTemplate used for configuring
     * the cache, giving access to the underlying Redis store.
     */
    @Override
    public Object getNativeCache() {
        return template;
    }

    @SuppressWarnings("unchecked")
    @Override
    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));
                return (bs == null ? null : newValueWrapper(template.getValueSerializer().deserialize(bs)));
            }
        }, true);
    }

    @Override
    public <T> T get(final Object key, Class<T> type) {
        return (T) template.execute(new RedisCallback<T>() {
            public T doInRedis(RedisConnection connection) throws DataAccessException {
                waitForLock(connection);
                byte[] bs = connection.get(computeKey(key));
                return (T) template.getValueSerializer().deserialize(bs);
            }
        }, true);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void put(final Object key, final Object value) {
        final byte[] k = computeKey(key);
        template.execute(new RedisCallback<Object>() {
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                waitForLock(connection);
                connection.multi();
                connection.set(k, template.getValueSerializer().serialize(value));
                connection.zAdd(setName, 0, k);

                // Set key time to live when expiration has been configured.
                if (ttl > NEVER_EXPIRE) {
                    connection.expire(k, ttl);
                    connection.expire(setName, ttl);
                }

                connection.exec();
                return null;
            }
        }, true);
    }

    @SuppressWarnings("unchecked")
    @Override
    public ValueWrapper putIfAbsent(final Object key, final Object value) {
        final byte[] k = computeKey(key);
        return (ValueWrapper) template.execute(new RedisCallback<ValueWrapper>() {
            public ValueWrapper doInRedis(RedisConnection connection) throws DataAccessException {
                waitForLock(connection);
                byte[] bs = connection.get(computeKey(key));

                if (bs == null) {
                    connection.multi();
                    connection.set(k, template.getValueSerializer().serialize(value));
                    connection.zAdd(setName, 0, k);

                    // Set key time to live when expiration has been configured.
                    if (ttl > NEVER_EXPIRE) {
                        connection.expire(k, ttl);
                        connection.expire(setName, ttl);
                    }

                    connection.exec();
                }

                bs = connection.get(computeKey(key));
                return (bs == null ? null : newValueWrapper(template.getValueSerializer().deserialize(bs)));
            }
        }, true);
    }

    @SuppressWarnings("unchecked")
    @Override
    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);
    }

    @SuppressWarnings("unchecked")
    @Override
    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);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Object> getAllKeys() {
        Set<byte[]> serializedKeys = (Set<byte[]>) template.execute(new RedisCallback<Set<byte[]>>() {
            public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                Set<byte[]> allKeys = new HashSet<byte[]>();
                int offset = 0;
                boolean finished = false;
                while (!finished) {
                    // need to paginate the keys
                    Set<byte[]> keys = connection.zRange(setName, (offset) * PAGE_SIZE,
                            (offset + 1) * PAGE_SIZE - 1);
                    allKeys.addAll(keys);
                    finished = keys.size() < PAGE_SIZE;
                    offset++;
                }
                return allKeys;
            }
        }, true);

        @SuppressWarnings("rawtypes")
        Collection<Object> keys = new HashSet(serializedKeys.size());
        RedisSerializer<byte[]> keySerializer = template.getKeySerializer();
        for (byte[] bytes : serializedKeys) {
            keys.add(keySerializer.deserialize(bytes));
        }

        return keys;
    }

    public byte[] computeKey(Object key) {
        @SuppressWarnings("unchecked")
        byte[] k = template.getKeySerializer().serialize(key);

        if (prefix == null || prefix.length == 0) {
            return k;
        }

        // ok to use Arrays.copyOf since spring-data-redis requires Java 6
        byte[] result = Arrays.copyOf(prefix, prefix.length + k.length);
        System.arraycopy(k, 0, result, prefix.length, k.length);
        return result;
    }

    protected GrailsValueWrapper newValueWrapper(Object value) {
        return value == null ? null : new GrailsValueWrapper(value, null);
    }

    protected boolean waitForLock(RedisConnection connection) {
        boolean foundLock = false;
        boolean retry = true;
        while (retry) {
            retry = false;
            if (connection.exists(cacheLockName)) {
                foundLock = true;
                try {
                    Thread thread = Thread.currentThread();

                    synchronized (thread) {
                        thread.wait(WAIT_FOR_LOCK);
                    }
                } catch (InterruptedException ex) {
                    // ignore
                }
                retry = true;
            }
        }
        return foundLock;
    }

}