(Quick Reference)

20 Redis

Version: 8.0.0-SNAPSHOT

20 Redis

The Grails Redis plugin provides access to Redis and a number of utilities (a service, taglib, and memoization annotations) for using Redis as a flexible, high-performance cache within a Grails application.

What is Redis?

Redis is often described as a "collection of data structures exposed over the network". It is an insanely fast key/value store, in some ways similar to Memcached, but the values it stores are not just dumb blobs of data — they are data structures such as strings, lists, hash maps, sets, and sorted sets. Redis can also act as a lightweight pub/sub or message-queueing system.

Redis is used in production by a number of very popular websites. It is commonly used as a caching layer, but because it provides network-available data structures it is very flexible and able to solve all kinds of problems. The creator of Redis, Salvatore Sanfilippo, has a nice post on taking advantage of Redis by adding it to your stack. With the Grails Redis plugin, adding Redis to your Grails application is very easy.

For a gentle start there is an introduction to Redis using Groovy that shows how to install Redis and use some basic Groovy commands, as well as a presentation given at gr8conf 2011. The official Redis documentation is excellent and includes a comprehensive list of Redis commands; each command page also has an embedded REPL that lets you test the command against a live Redis server.

What is Jedis?

The plugin connects to Redis using Jedis, a fast and actively-maintained Java client. One of the nice things about Jedis is that it does not munge the Redis command names but follows them as closely as possible. This means that for almost all commands the Redis command documentation can also be used to understand how to use the Jedis connection objects — you do not need to translate the Redis documentation into Jedis commands.

20.1 Installation and Configuration

Add the plugin to the dependencies block of your build.gradle. The version is managed by the Grails BOM, so it does not need to be specified:

dependencies {
    implementation 'org.apache.grails:grails-redis'
}

Out of the box, the plugin expects Redis to be running on localhost:6379. The easiest way to run a local instance for development and testing is with Docker:

docker run -d --name redis -p 6379:6379 redis

The connection — along with any pool configuration options — can be customized under the grails.redis key in grails-app/conf/application.yml. A connection requires either a host and port combination or a sentinels and masterName combination:

grails:
    redis:
        poolConfig:
            # any property exposed by the Jedis `JedisPoolConfig` may be set here, for example:
            # testWhileIdle: true
            # maxTotal: 10
            # maxIdle: 8
        timeout: 2000              # connection timeout in milliseconds (default)
        password: somepassword     # defaults to no password
        useSSL: false              # set to true to connect over SSL

        # --- Option 1: a single Redis server (use only if NOT using a sentinel cluster) ---
        host: localhost
        port: 6379
        database: 5                # selects the Redis database to use

        # --- Option 2: a redis-sentinel cluster (use only if NOT using host/port) ---
        sentinels:                 # list of sentinel instance host/ports
            - host1:6379
            - host2:6379
            - host3:6379
        masterName: mymaster       # the name of the master the sentinel cluster is configured to monitor

The poolConfig section lets you tweak any of the setter values exposed by Jedis' JedisPoolConfig, which implements the Apache Commons GenericObjectPool.

See the Redis Sentinel documentation for more information on using redis-sentinel for high availability.

20.2 Using the redisService

The redisService bean

The redisService bean wraps the connection pool. It provides caching/memoization helpers, template methods, and basic Redis commands, and it will be your primary interface to Redis.

The service overrides propertyMissing and methodMissing to delegate any unknown request to a Redis connection, so any method you would normally call on a Redis connection can be called directly on redisService:

def redisService

redisService.foo = 'bar'
assert 'bar' == redisService.foo

redisService.sadd('months', 'february')
assert true == redisService.sismember('months', 'february')

The withRedis template method takes a closure and passes it a Jedis connection. The pool connection is automatically obtained and always returned to the pool when the closure finishes, even if an error occurs. Using withRedis for a sequence of commands uses a single connection rather than one per command:

redisService.withRedis { Jedis redis ->
    redis.set('foo', 'bar')
}

Redis supports pipelining, which lets you send commands without waiting for each response. The withPipeline template works like withRedis:

redisService.withPipeline { Pipeline pipeline ->
    pipeline.set('foo', 'bar')
}

Redis transactions guarantee that all commands in the transaction execute as an atomic unit (Redis does not, however, support rolling back modifications). The withTransaction template opens and closes the transaction automatically, telling Redis to execute it if the closure does not throw:

redisService.withTransaction { Transaction transaction ->
    transaction.set('foo', 'bar')
}

Utility methods

The service also provides a few utility methods:

redisService.flushDB() // dangerous! - should generally only be used for test cleanup

// deletes all keys matching a pattern. This is relatively expensive as it uses the
// `keys` operation; if you do this frequently, consider aggregating your own set of
// keys to delete instead.
redisService.deleteKeysWithPattern('key:pattern:*')

The redisPool bean

You can access the pool of Redis connection objects directly by injecting the redisPool bean. Normally you will interact with redisService instead, but the pool is available if you need to work with it directly:

def redisPool

The redis:memoize taglib

The redis:memoize tag lets you leverage memoization within your GSP files. Wrap it around any expensive-to-generate content and it will be cached in Redis and served quickly on subsequent requests until the key expires:

<redis:memoize key="mykey" expire="3600">
    <div id="header">
        ... expensive header content that can be cached ...
    </div>
</redis:memoize>

20.3 Memoization

Memoization is a write-through caching technique. The plugin provides a number of methods that take a key and a closure. Each method first checks Redis for the key: if it exists, the cached value is returned and the closure is not executed; if it does not, the closure is executed and its result is stored in Redis under the key. Subsequent calls are served the cached value rather than recalculating it.

This is very useful for values that are frequently requested but expensive to calculate.

String memoization

redisService.memoize("user:$userId:helloMessage") {
    // expensive calculation that returns a String
    "Hello ${security.currentLoggedInUser().firstName}"
}

By default the key/value is cached indefinitely. You can refresh it by deleting the key, by including a date or timestamp in the key, or by using the optional expire parameter (the number of seconds before Redis expires the key):

def ONE_HOUR = 3600
redisService.memoize("user:$userId:helloMessage", [expire: ONE_HOUR]) {
    "Hello ${security.currentLoggedInUser().firstName}. The temperature this hour is ${currentTemperature()}"
}

Domain object memoization

You can memoize a single domain object. The plugin caches the id of the object returned by the closure and, on subsequent cache hits, returns a proxy via DomainObject.load(cachedId):

String key = 'user:42:favorite:author'
Author author = redisService.memoizeDomainObject(Author, key) {
    Author a = ... // expensive calculation of user 42's favorite author
    return a
}

Because a proxy is returned, you can query with it without hydrating the full object:

def recommendedBooks = Book.findByAuthor(author)

The id field is populated and the remaining fields are loaded lazily only when requested, so author.name still resolves correctly.

Domain list memoization

You can also memoize a list of domain object identifiers. Only the database ids are cached, not the full objects, so you still fetch the freshest objects from the database while avoiding the cost of repeatedly building an expensive list:

def key = "user:$id:friends-books-user-does-not-own"
redisService.memoizeDomainList(Book, key, ONE_HOUR) { redis ->
    // expensive process to determine the relevant Book ids; the ids are stored in
    // Redis, but the Book objects are hydrated from the database
}

Other memoization methods

The plugin provides memoization methods for the other Redis data types:

// Redis hash
redisService.memoizeHash('saved-hash') { return [foo: 'bar'] }
redisService.memoizeHashField('saved-hash', 'foo') { return 'bar' }

// Redis list
redisService.memoizeList('saved-list') { return ['foo', 'bar', 'baz'] }

// Redis set
redisService.memoizeSet('saved-set') { return ['foo', 'bar', 'baz'] as Set }

// Redis sorted set
redisService.memoizeScore('saved-sorted-set', 'set-item') { return score }

20.4 Memoization Annotations

In addition to the concrete redisService.memoize* methods, you may annotate a method with the corresponding @Memoize* annotation. The annotation performs an AST transformation at compile time that wraps the body of the method with the matching memoization method. Parameters such as key and expire are passed to the annotation and used in the redisService memoize call.

The following annotations are available:

Annotation Description

@Memoize

Memoizes methods that return a String — redisService.memoize

@MemoizeObject

Memoizes methods that return an object (stored as JSON) — redisService.memoizeObject

@MemoizeDomainObject

Memoizes methods that return a domain object — redisService.memoizeDomainObject

@MemoizeDomainList

Memoizes methods that return a list of domain objects — redisService.memoizeDomainList

@MemoizeHash

Memoizes methods that return a hash — redisService.memoizeHash

@MemoizeHashField

Memoizes methods that return a hash field — redisService.memoizeHashField

@MemoizeList

Memoizes methods that return a list — redisService.memoizeList

@MemoizeSet

Memoizes methods that return a set — redisService.memoizeSet

@MemoizeScore

Memoizes methods that return a score from a hash — redisService.memoizeScore

Annotation keys

Because the key value is passed in and also transformed by the AST, you cannot use $-style GString values in the keys. Instead, use the sign to represent a GString value, e.g. @Memoize(key = "{book.title}:#{book.id}"). During the AST transformation these are replaced with $ and evaluate at runtime as redisService.memoize("${book.title}:${book.id}"){…​}.

Anything not in the format key='#text' or key="${text}" is treated as a string literal — key="text" is equivalent to using the literal string "text" as the memoize key. Any variable referenced in the key must be in scope; an out-of-scope reference produces a runtime error. The same rules apply to the expire field.

Notes

You do not need to import grails.plugins.redis.RedisService or declare def redisService on objects that use these annotations — the AST transform detects whether the field is present and adds it if needed. The entire annotated method body is wrapped in the Redis service call, so any computation it contains is skipped on a cache hit.

If compilation succeeds but the method fails at runtime, verify that your key or value is configured correctly and that the key uses #{} for every variable you want referenced. If compilation does not succeed, check the stack trace — each annotation type validates its AST transform (required properties are provided, expire is a valid Integer, value is a valid closure, and key is a valid String).

@Memoize

Used for objects stored in Redis as strings. Parameters:

  • value — a closure (key OR value required)

  • key — a unique cache key (key OR value required)

  • expire — expiry in seconds; defaults to never

You may specify either a closure OR a key and expire. When using the closure-style key @Memoize({"#{text}"}) you may not also pass a key or expire, because the closure is evaluated directly and used as the key. This is a limitation of how Java handles closure annotation parameters.
@Memoize({"#{text}"})
def getAnnotatedTextUsingClosure(String text, Date date) {
    return "$text $date"
}

@Memoize(key = '#{text}')
def getAnnotatedTextUsingKey(String text, Date date) {
    return "$text $date"
}

@Memoize(key = '#{text}', expire = '3600')
def getAnnotatedTextUsingKeyAndExpire(String text, Date date) {
    return "$text $date"
}

@Memoize(key = "#{book.title}:#{book.id}")
def getAnnotatedBook(Book book) {
    return book.toString()
}

@MemoizeObject

Used for simple (non-domain) objects whose contents are stored in Redis as JSON. The returned object is converted to JSON, stored, and converted back, so the object class must be convertible to and from JSON. Parameters: key (required), expire, and clazz (the object class, required).

@MemoizeObject(key = "#{title}", expire = "#{grailsApplication.config.cache.ttl}", clazz = Book.class)
def createObject(String title, Date date) {
    new Book(title: title, createDate: date)
}

This is also available directly from the service:

redisService.memoizeObject(Book.class, title, grailsApplication.config.cache.ttl) {
    new Book(title: title, createDate: date)
}

@MemoizeDomainObject

Used for domain objects whose ids are stored in Redis (see domain object memoization). Parameters: key (required), expire, and clazz (required).

@MemoizeDomainObject(key = "#{title}", clazz = Book.class)
def createDomainObject(String title, Date date) {
    Book.build(title: title, createDate: date)
}

@MemoizeDomainList

Used for lists of domain objects whose ids are stored in Redis (see domain list memoization). Parameters: key (required), expire, and clazz (required).

@MemoizeDomainList(key = "getDomainListWithKeyClass:#{title}", clazz = Book.class)
def getDomainListWithKeyClass(String title, Date date) {
    Book.findAllByTitle(title)
}

@MemoizeList

Used for list-type objects. Parameters: value / key (one required) and expire.

@MemoizeList(key = "#{list[0]}")
def getAnnotatedList(List list) {
    return list
}

@MemoizeScore

Used for scores in hashes. Parameters: key (required), expire, and member (the hash property to store, required).

@MemoizeScore(key = "#{map.key}", member = "foo")
def getAnnotatedScore(Map map) {
    return map.foo
}

@MemoizeHash

Used for map/hash-type objects. Parameters: value / key (one required) and expire.

@MemoizeHash(key = "#{map.foo}")
def getAnnotatedHash(Map map) {
    return map
}

20.5 Multiple Redis Servers

If you use multiple, non-clustered Redis servers and want to perform discrete operations on each from a single application, you can configure additional named connections under the connections block. The standard configuration block (described in Installation and Configuration) describes the default connection; each named connection accepts the same options, including its own poolConfig and its own host/port or sentinels/masterName combination.

grails:
    redis:
        poolConfig:
            # pool-specific tweaks for the default connection
            # numTestsPerEvictionRun: 4

        # the default connection requires either a host & port combo, or a sentinels & masterName combo
        host: localhost
        port: 6379
        # sentinels:
        #     - host1:6379
        #     - host2:6379
        # masterName: mymaster

        connections:
            cache:
                poolConfig:
                    # pool-specific tweaks for the 'cache' connection
                host: localhost
                port: 6380
                # sentinels / masterName may be used here instead of host/port
            search:
                poolConfig:
                    # pool-specific tweaks for the 'search' connection
                host: localhost
                port: 6381

Connection names must be unique. For each named connection, a service bean is wired in addition to the default redisService bean, with the capitalized connection name appended — the example above creates redisServiceCache and redisServiceSearch. The connection-specific pool beans (redisPoolCache and redisPoolSearch) are created using the same convention.

Alternatively, you can keep using the default redisService bean and refer to a connection by name with withConnection, e.g. redisService.withConnection('cache').withRedis { …​ }. Whether you inject the connection-specific beans or use withConnection on the default bean, the result is the same — withConnection is simply a pass-through to the named beans.

class FooService {

    def redisService
    // connection-specific beans; you can use these or the `withConnection` method
    def redisServiceCache
    def redisServiceSearch

    def doWork() {
        redisService.withRedis { Jedis redis ->
            redis.set('foo', 'bar')
        }

        redisService.withConnection('cache').withTransaction { Transaction transaction ->
            transaction.set('foo', 'bar')
        }

        redisServiceSearch.withPipeline { Pipeline pipeline ->
            pipeline.set('foo', 'bar')
        }

        redisServiceCache.memoize('somecachekey') {
            return cacheData
        }

        redisService.withConnection('search').memoizeDomainList(Book, 'domainkey') {
            return Book.findAllByTitleInList(['book1', 'book3'])
        }
    }
}

The following external resources are useful when working with Redis and the Jedis client: