Redis Cache Annotation

In modern Spring Boot applications, managing a cache manually using RedisTemplate can lead to verbose, repetitive, and error-prone code. To streamline this process, Spring provides a powerful abstraction layer known as Spring Cache. When paired with Redis as the underlying storage engine, developers can implement a declarative caching strategy. This means instead of writing logic to check, fetch, and save data to Redis, you simply apply annotations to your methods.

By leveraging this abstraction, you decouple your business logic from the caching implementation, making your code cleaner and easier to maintain. This article provides a comprehensive guide to the essential cache annotations, configuration best practices, and common pitfalls to avoid.

Getting Started: Dependencies and Activation

To begin using Spring Cache with Redis, you must first include the necessary starters in your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Once the dependencies are in place, you must explicitly enable the caching capability by adding the @EnableCaching annotation to your main application class or a configuration class:

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Core Annotations

1. @Cacheable: The Read-Through Strategy

The @Cacheable annotation is used on methods intended for data retrieval. It implements a "read-through" pattern: before the method executes, Spring checks if the result is already present in the cache. If a cache hit occurs, the cached value is returned immediately. If a cache miss occurs, the method is executed, and its return value is stored in Redis for future use.

@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
    return userMapper.selectById(id);
}

In this example, the key in Redis will follow the pattern user::[id] (e.g., user::1).

Key Attributes:

  • value / cacheNames: Defines the namespace for the cache. This is essential for separating different business domains (e.g., "user", "product", "order").
  • key: Uses SpEL (Spring Expression Language) to define the unique identifier for the cached entry. You can reference method parameters using #parameterName.
  • condition: A SpEL expression that determines whether the cache should be checked/populated. For example, condition = "#id > 0" ensures only valid IDs are cached.
  • unless: A SpEL expression used to prevent caching based on the result of the method. For instance, unless = "#result == null" prevents storing null values in the cache.
  • sync = true: This is a critical attribute for high-concurrency environments. It ensures that only one thread executes the method to fetch data from the source (the "back-end") while other threads wait for the result, effectively preventing cache breakdown (cache stampede).

2. @CachePut: The Update Strategy

Unlike @Cacheable, the @CachePut annotation always executes the method body. Its primary purpose is to update the cache with the method's return value, ensuring that the cached data remains synchronized with the database.

@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) {
    userMapper.updateById(user);
    return user;
}

Critical Requirements:

  • The method must return the updated object. If the method returns void, the cache entry will be overwritten with nothing.
  • The key used in @CachePut must match the key used in the corresponding @Cacheable method to ensure the existing entry is updated rather than a new one being created.

3. @CacheEvict: The Invalidation Strategy

When data is deleted or significantly changed, the corresponding cache entries must be removed to prevent stale data. @CacheEvict handles this invalidation.

@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
    userMapper.deleteById(id);
}

Key Attributes:

  • allEntries = true: Instead of removing a specific key, this clears all entries within the specified value (namespace). This is particularly useful for clearing entire lists or collections when a single item changes.
  • beforeInvocation: By default (false), the cache is evicted only after the method completes successfully. If set to true, the cache is evicted before the method runs. This is useful if you want to ensure the cache is cleared even if the subsequent database operation fails.

4. @Caching: Combining Operations

Sometimes, a single business operation requires multiple cache actions—for example, updating a specific user record while simultaneously clearing a cached list of all users. The @Caching annotation allows you to group multiple cache annotations together.

@Caching(
    put = @CachePut(value = "user", key = "#user.id"),
    evict = @CacheEvict(value = "userList", allEntries = true)
)
public User saveUser(User user) {
    userMapper.insert(user);
    return user;
}

Advanced Configuration: TTL and Serialization

By default, Spring Cache does not set an expiration time (TTL), and it uses standard JDK serialization. JDK serialization produces binary data that is difficult for humans to read in Redis and can lead to compatibility issues.

To build a production-ready system, you should customize the RedisCacheManager to use JSON serialization and define a TTL.

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30)) // Set default TTL to 30 minutes
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())); // Use JSON
    
    return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
}

Alternatively, for simpler setups in Spring Boot 2.3+, you can set the TTL directly in your application.yml:

spring:
  cache:
    redis:
      time-to-live: 30m

Best Practices and Common Pitfalls

To ensure your caching layer is robust and reliable, keep the following professional insights in mind:

1. The Self-Invocation Problem

Spring Cache is implemented using Spring AOP (Aspect-Oriented Programming). This means the annotations are intercepted by a proxy object. If you call a @Cacheable method from another method within the same class (e.g., this.getUserById(id)), the call bypasses the proxy, and the annotation will be ignored. To fix this, move the cached method to a separate service or inject the bean into itself.

2. Method Visibility

Annotations only work on public methods. If you attempt to use them on private or protected methods, the AOP proxy will not be able to intercept the call, and no caching will occur.

3. Handling Cache Penetration

Cache penetration occurs when requests are made for keys that do not exist in either the cache or the database, forcing the system to hit the database every time. To mitigate this, use the unless attribute to cache "empty" results (nulls) with a very short TTL, or implement a Bloom Filter.

4. Maintaining Data Consistency

A common mistake is updating the cache before the database. The industry standard for high consistency is the "Update Database, then Delete Cache" strategy. Using @CacheEvict after a database update is generally safer than @CachePut, as it avoids race conditions where multiple concurrent updates might leave the cache in an inconsistent state.

5. Strategic Key Design

Avoid using generic keys. Always use a combination of the cache name and a unique identifier. For complex objects, consider implementing a custom KeyGenerator to ensure that keys are unique, predictable, and easy to debug during manual Redis inspections.

Conclusion

Spring Cache annotations provide a sophisticated, declarative way to implement caching with Redis. By mastering @Cacheable, @CachePut, and @CacheEvict, and understanding how to configure serialization and TTL, you can significantly improve the performance of your Spring Boot applications. However, always remain vigilant regarding AOP limitations, data consistency strategies, and key design to ensure your caching layer remains a performance asset rather than a source of bugs.