All Downloads are FREE. Search and download functionalities are using the official Maven repository.

grails.plugins.redis.RedisService.groovy Maven / Gradle / Ivy

Go to download

This Plugin provides access to Redis and various utilities(service, annotations, etc) for caching.

There is a newer version: 5.0.0-M3
Show newest version
/* Copyright (C) 2011 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.plugins.redis

import com.google.gson.Gson
import grails.core.GrailsApplication
import grails.gorm.transactions.Transactional
import groovy.util.logging.Commons
import redis.clients.jedis.Jedis
import redis.clients.jedis.Pipeline
import redis.clients.jedis.Transaction
import redis.clients.jedis.exceptions.JedisConnectionException

@Commons
class RedisService {

    public static final int NO_EXPIRATION_TTL = -1
    public static final int KEY_DOES_NOT_EXIST = -2  // added in redis 2.8

    def redisPool
    GrailsApplication grailsApplication

    boolean transactional = false

    RedisService withConnection(String connectionName) {
        if (grailsApplication.mainContext.containsBean("redisService${connectionName.capitalize()}")) {
            return (RedisService) grailsApplication.mainContext.getBean("redisService${connectionName.capitalize()}")
        }
        if (log.errorEnabled) log.error("Connection with name redisService${connectionName.capitalize()} could not be found, returning default redis instead")
        return this
    }

    def withPipeline(Closure closure, Boolean returnAll = false) {
        withRedis { Jedis redis ->
            Pipeline pipeline = redis.pipelined()
            closure(pipeline)
            returnAll ? pipeline.syncAndReturnAll() : pipeline.sync()
        }
    }

    def withOptionalPipeline(Closure clos, Boolean returnAll = false) {
        withOptionalRedis { Jedis redis ->
            if (redis) {
                Pipeline pipeline = redis.pipelined()
                clos(pipeline)
                returnAll ? pipeline.syncAndReturnAll() : pipeline.sync()
            } else {
                return clos()
            }

        }
    }

    def withTransaction(Closure closure) {
        withRedis { Jedis redis ->
            Transaction transaction = redis.multi()
            try {
                closure(transaction)
            } catch (Exception exception) {
                transaction.discard()
                throw exception
            }

            transaction.exec()
        }
    }

    def methodMissing(String name, args) {
        if (log.debugEnabled) log.debug "methodMissing $name"
        withRedis { Jedis redis ->
            redis.invokeMethod(name, args)
        }
    }

    void propertyMissing(String name, Object value) {
        withRedis { Jedis redis ->
            redis.set(name, value.toString())
        }
    }

    Object propertyMissing(String name) {
        withRedis { Jedis redis ->
            redis.get(name)
        }
    }

    def withRedis(Closure closure) {
        Jedis redis = redisPool.resource
        try {
            return closure(redis)
        } catch (JedisConnectionException jce) {
            throw jce
        } catch (Exception e) {
            throw e
        } finally {
            if (redis) {
                redis.close()
            }
        }
    }

    /**
     * An implementation of withRedis that suppresses JedisConnectException to support the memoization model
     * @param clos
     * @return
     */
    def withOptionalRedis(Closure clos) {
        Jedis redis
        try {
            redis = redisPool.resource
        }
        catch (JedisConnectionException jce) {
            log.info('Unreachable redis store trying to retrieve redis resource.  Please check redis server and/or config!')
        }

        try {
            return clos(redis)
        } catch (JedisConnectionException jce) {
            log.error('Unreachable redis store trying to return redis pool resource.  Please check redis server and/or config!', jce)
        } catch (Throwable t) {
            throw t
        } finally {
            if (redis) {
                redis.close()
            }
        }
    }

    def memoize(String key, Integer expire, Closure closure) {
        memoize(key, [expire: expire], closure)
    }

    // SET/GET a value on a Redis key
    def memoize(String key, Map options = [:], Closure closure) {
        if (log.debugEnabled) log.debug "using key $key"
        def result = withOptionalRedis { Jedis redis ->
            if (redis) return redis.get(key)
        }

        if (!result) {
            if (log.debugEnabled) log.debug "cache miss: $key"
            result = closure()
            if (result) withOptionalRedis { Jedis redis ->
                if (redis) {
                    if (options?.expire) {
                        redis.setex(key, options.expire, result as String)
                    } else {
                        redis.set(key, result as String)
                    }
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cache hit : $key = $result"
        }
        result
    }

    def memoizeHash(String key, Integer expire, Closure closure) {
        memoizeHash(key, [expire: expire], closure)
    }

    def memoizeHash(String key, Map options = [:], Closure closure) {
        def hash = withOptionalRedis { Jedis redis ->
            if (redis) return redis.hgetAll(key)
        }

        if (!hash) {
            if (log.debugEnabled) log.debug "cache miss: $key"
            hash = closure()
            if (hash) withOptionalRedis { Jedis redis ->
                if (redis) {
                    redis.hmset(key, hash)
                    if (options?.expire) redis.expire(key, options.expire)
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cache hit : $key = $hash"
        }
        hash
    }

    def memoizeHashField(String key, String field, Integer expire, Closure closure) {
        memoizeHashField(key, field, [expire: expire], closure)
    }

    // HSET/HGET a value on a Redis hash at key.field
    // if expire is not null it will be the expire for the whole hash, not this value
    // and will only be set if there isn't already a TTL on the hash
    def memoizeHashField(String key, String field, Map options = [:], Closure closure) {
        def result = withOptionalRedis { Jedis redis ->
            if (redis) return redis.hget(key, field)
        }

        if (!result) {
            if (log.debugEnabled) log.debug "cache miss: $key.$field"
            result = closure()
            if (result) withOptionalRedis { Jedis redis ->
                if (redis) {
                    redis.hset(key, field, result as String)
                    if (options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire)
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cache hit : $key.$field = $result"
        }
        result
    }

    def memoizeScore(String key, String member, Integer expire, Closure closure) {
        memoizeScore(key, member, [expire: expire], closure)
    }

    // set/get a 'double' score within a sorted set
    // if expire is not null it will be the expire for the whole zset, not this value
    // and will only be set if there isn't already a TTL on the zset
    def memoizeScore(String key, String member, Map options = [:], Closure closure) {
        def score = withOptionalRedis { Jedis redis ->
            if (redis) redis.zscore(key, member)
        }

        if (!score) {
            if (log.debugEnabled) log.debug "cache miss: $key.$member"
            score = closure()
            if (score) withOptionalRedis { Jedis redis ->
                if (redis) {
                    redis.zadd(key, score, member)
                    if (options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire)
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cache hit : $key.$member = $score"
        }
        score
    }

    List memoizeDomainList(Class domainClass, String key, Integer expire, Closure closure) {
        memoizeDomainList(domainClass, key, [expire: expire], closure)
    }

    List memoizeDomainList(Class domainClass, String key, Map options = [:], Closure closure) {
        List idList = getIdListFor(key)
        if (idList) return hydrateDomainObjectsFrom(domainClass, idList)

        def domainList = withOptionalRedis { Jedis redis ->
            closure(redis)
        }

        saveIdListTo(key, domainList, options.expire)

        domainList
    }

    List memoizeDomainIdList(Class domainClass, String key, Integer expire, Closure closure) {
        memoizeDomainIdList(domainClass, key, [expire: expire], closure)
    }

    // used when we just want the list of Ids back rather than hydrated objects
    List memoizeDomainIdList(Class domainClass, String key, Map options = [:], Closure closure) {
        List idList = getIdListFor(key)
        if (idList) return idList

        def domainList = closure()

        saveIdListTo(key, domainList, options.expire)

        getIdListFor(key)
    }

    protected List getIdListFor(String key) {
        List idList = withOptionalRedis { Jedis redis ->
            if (redis) return redis.lrange(key, 0, -1)
        }

        if (idList) {
            if (log.debugEnabled) log.debug "$key cache hit, returning ${idList.size()} ids"
            List idLongList = idList*.toLong()
            return idLongList
        }
    }

    protected void saveIdListTo(String key, List domainList, Integer expire = null) {
        if (log.debugEnabled) log.debug "$key cache miss, memoizing ${domainList?.size() ?: 0} ids"
        withOptionalPipeline { pipeline ->
            if (pipeline) {
                for (domain in domainList) {
                    pipeline.rpush(key, domain.id as String)
                }
                if (expire) pipeline.expire(key, expire)
            }
        }
    }

    @Transactional(readOnly = true)
    protected List hydrateDomainObjectsFrom(Class domainClass, List idList) {
        if (domainClass && idList) {
            //return domainClass.findAllByIdInList(idList, [cache: true])
            return idList.collect { id -> domainClass.load(id) }
        }
        []
    }

    def memoizeDomainObject(Class domainClass, String key, Integer expire, Closure closure) {
        memoizeDomainObject(domainClass, key, [expire: expire], closure)
    }

    // closure can return either a domain object or a Long id of a domain object
    // it will be persisted into redis as the Long
    @Transactional(readOnly = true)
    def memoizeDomainObject(Class domainClass, String key, Map options = [:], Closure closure) {
        Long domainId = withOptionalRedis { redis ->
            redis?.get(key)?.toLong()
        }
        if (!domainId) domainId = persistDomainId(closure()?.id as Long, key, options.expire)
        domainClass.load(domainId)
    }

    Long persistDomainId(Long domainId, String key, Integer expire) {
        if (domainId) {
            withOptionalPipeline { pipeline ->
                if (pipeline) {
                    pipeline.set(key, domainId.toString())
                    if (expire) pipeline.expire(key, expire)
                }
            }
        }
        domainId
    }

    def memoizeObject(Class clazz, String key, Integer expire, Closure closure) {
        memoizeObject(clazz, key, [expire: expire], closure)
    }

    def memoizeObject(Class clazz, String key, Map options = [:], Closure closure) {
        Gson gson = new Gson()

        String memoizedJson = memoize(key, options) { ->
            def original = closure()
            if (original == null && options.cacheNull == false) return null
            gson.toJson(original)
        }

        gson.fromJson((String) memoizedJson, clazz)
    }

    // deletes all keys matching a pattern (see redis "keys" documentation for more)
    // OK for low traffic methods, but expensive compared to other redis commands
    // perf test before relying on this rather than storing your own set of keys to 
    // delete
    void deleteKeysWithPattern(keyPattern) {
        if (log.infoEnabled) log.info("Cleaning all redis keys with pattern  [${keyPattern}]")
        withRedis { Jedis redis ->
            String[] keys = redis.keys(keyPattern)
            if (keys) redis.del(keys)
        }
    }

    /**
     * Deletes key from redis.
     *
     * @param key The key to delete.
     */
    void deleteKey(String key){
        withRedis { Jedis redis ->
            redis.del(key)
        }
    }

    def memoizeList(String key, Integer expire, Closure closure) {
        memoizeList(key, [expire: expire], closure)
    }

    def memoizeList(String key, Map options = [:], Closure closure) {
        List list = withOptionalRedis { Jedis redis ->
            if (redis) return redis.lrange(key, 0, -1)
        }

        if (!list) {
            if (log.debugEnabled) log.debug "cache miss: $key"
            list = closure()
            if (list) withOptionalPipeline { pipeline ->
                if (pipeline) {
                    for (obj in list) {
                        pipeline.rpush(key, obj)
                    }
                    if (options?.expire) pipeline.expire(key, options.expire)
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cach hit: $key"
        }
        list
    }

    def memoizeSet(String key, Integer expire, Closure closure) {
        memoizeSet(key, [expire: expire], closure)
    }

    def memoizeSet(String key, Map options = [:], Closure closure) {
        def set = withOptionalRedis { Jedis redis ->
            if (redis) return redis.smembers(key)
        }

        if (!set) {
            if (log.debugEnabled) log.debug "cache miss: $key"
            set = closure()
            if (set) withOptionalPipeline { pipeline ->
                if (pipeline) {
                    for (obj in set) {
                        pipeline.sadd(key, obj)
                    }
                    if (options?.expire) pipeline.expire(key, options.expire)
                }
            }
        } else {
            if (log.debugEnabled) log.debug "cache hit: $key"
        }
        set
    }
    // should ONLY Be used from tests unless we have a really good reason to clear out the entire redis db
    def flushDB() {
        if (log.warnEnabled) log.warn('flushDB called!')
        withRedis { Jedis redis ->
            redis.flushDB()
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy