How to enable caching in Spring Boot 2 using Redis

  Reading Time:

Why Caching ?

Caching layer is added to improve the performance of your system. We’ll enable simple caching in our project using caching abstraction in Spring and will use Redis to cache data

Benefits of Caching

  • Improve Application Performance
  • Reduce data retrieval time
  • Improve page speed
  • Better user experience
  • Reduce the Load on DB

Caching using Spring framework

Since version 3.1, the Spring Framework provides support for transparently adding caching to an existing Spring application. The caching abstraction allows consistent use of various caching solutions with minimal impact on the code.

As from Spring 4.1, the cache abstraction has been significantly extended with the support of   JSR-107 annotations and more customization options.

We are going to use the one supported in Spring 4.1 and above

To use the cache abstraction, you need to take care of two aspects:

  • Caching declaration: Identify the methods that need to be cached and their policy.
  • Cache configuration: The backing cache where the data is stored and from which it is read. In our case it is Redis cache

In this example we are going to use the repository fetch methods for caching declaration and Redis as the caching layer , data is stored in Redis cache running on localhost

Caching annotations

For caching declaration, Spring’s caching abstraction provides a set of Java annotations:

@Cacheable

As the name implies, @Cacheable is used to demarcate methods that are cacheable - that is, methods for whom the result is stored into the cache so on subsequent invocations (with the same arguments), the value in the cache is returned without having to actually execute the method.

@CacheEvict

The cache abstraction allows not just population of a cache store but also eviction. This process is useful for removing stale or unused data from the cache. Opposed to‘Cacheable’  annotation‘CacheEvict’ demarcates methods that perform cache eviction, that is methods that act as triggers for removing data from the cache.

@CachePut

For cases where the cache needs to be updated without interferring with the method execution, one can use the @CachePut annotation. That is, the method will always be executed and its result placed into the cache (according to the @CachePut options).

@Caching

There are cases when multiple annotations of the same type, such as @CacheEvict or @CachePut need to be specified.

For eg : @Caching(evict = { @CacheEvict("first"), @CacheEvict(value = "second", key = "#id") })


In this example we are going to explain the spring cache using a blog application

We will be caching the latest blog in redis and the blog content will be servered from redis after the first call from DB

The project source code is available on GITHUB

Lets first create a simple spring boot project using ‘Spring Initializr’

Create simple spring boot using‘Spring Initializr’

Spring Redis config

The main two dependency in pom file mentioned below to enable spring cache with redis in spring boot 2


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

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

First we need to create the Redis Spring cache config and we need to enable caching using spring annotation @EnableCaching


@Configuration
@EnableCaching
public class RedisCacheConfig {

        @Value("${redis.hostname:localhost}")
        private String redisHostName;

        @Value("${redis.port:6379}")
        private int redisPort;

        @Value("${redis.ttl.hours:1}")
        private int redisDataTTL;

        @Bean
        public LettuceConnectionFactory redisConnectionFactory() {
                return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHostName, redisPort));
        }

        @Bean
        public RedisTemplate<Object, Object> redisTemplate() {
                RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
                redisTemplate.setConnectionFactory(redisConnectionFactory());
                return redisTemplate;
        }

        @Bean
        public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) {
               /**
                * If we want to use JSON Serialized then use the below config snippet 
                */
//                RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
//                                .entryTtl(Duration.ofHours(redisDataTTL))
//                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
                
              /**
                * If we want to use JAVA Serialized then use the below config snippet 
                */
                
                RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
                                .entryTtl(Duration.ofHours(redisDataTTL))
                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java()));

                redisCacheConfiguration.usePrefix();

                return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
                                .cacheDefaults(redisCacheConfiguration).build();

        }
}

Add the following property to application.properties file



redis.hostname = localhost
redis.port = 6379
redis.ttl.hours = 24

Blog sample application

In this example we are going to create blog application DB entity objects

  • BaseEntity.java
  • Article.java
  • User.java

We will cache the Article HTTP GET API response in Redis cache

Caching results of an API

Here we try to cache the result of get all article API response

HTTP GET : /api/v1/article/list

The first call will hit the DB and get the Article list response , this response is cached in Redis and returned , subsequent calls don’t fetch data from DB but would fetch it from Redis cache till it expires

We can verify this but checking the console for SQL statements if log enabled else we can delete the data in H2 db directly and hit the URL and we will still get data from cache till it expires


@RestController
@RequestMapping("/api/v1/article")
public class ArticleController {

.......

@GetMapping("/list")
@Cacheable("Article_Response_List")
public List<Article> findAllAsList(Pageable pageable) {
        return articleRepository.findAll(pageable).getContent();
}

.......

In this example we have enabled cache using the annotation

@Cacheable("Article_Response_List")

Here the ‘Article_Response_List’ denotes the cache name in redis the Article list is stored  to avoid data overlap it is better to give unique cache names.

Full repository class shown below with all possible annotations to put ,update and evict data to cache


@RestController
@RequestMapping("/api/v1/article")
public class ArticleController {

        @Autowired
        private ArticleRepository articleRepository;

        @GetMapping("/list")
        @Cacheable("Article_Response_List")
        public List<Article> findAllAsList(Pageable pageable) {
                return articleRepository.findAll(pageable).getContent();
        }

        @GetMapping("/find")
        @Cacheable("Article_Response")
        public Article findByTitle(@RequestParam String title) {
                return articleRepository.findByTitle(title);
        }

        @GetMapping("/{id}")
        @Cacheable("Article_Response")
        public Article findOne(@PathVariable Long id) throws BlogAppException {
                Optional<Article> result = articleRepository.findById(id);
                if (result.isPresent())
                        return result.get();
                else
                        throw new BlogAppException("Article with given id not found");
        }

        @PostMapping
        @Caching(put = {@CachePut(value = "Article_Response")}, evict = {@CacheEvict(value = "Article_Response_List", allEntries = true)})
        public Article create(Article article) {
                return articleRepository.save(article);
        }
        
        @PutMapping
        @Caching(put = {@CachePut(value = "Article_Response")}, evict = {@CacheEvict(value = "Article_Response_List", allEntries = true)})
        public Article update(Article article) {
                return articleRepository.save(article);
        }

        @DeleteMapping("/{id}")
        @Caching(evict = {@CacheEvict(value = "Article_Response"),@CacheEvict(value = "Article_Response_List", allEntries = true)})
        public void delete(@PathVariable Long id) {
                articleRepository.deleteById(id);
        }

What if Redis server is down ? How do we handle such exceptions

If the Redis server becomes unavailable or spring not able to connect to redis cluster or server , we want the application to continue to operate as if data is uncached, rather than throwing exceptions and failing to return the result to end user

We need to handle cache error so the new enhanced RedisCacheConfig shown below


@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport implements CachingConfigurer {

        @Value("${redis.hostname:localhost}")
        private String redisHost;

        @Value("${redis.port:6379}")
        private int redisPort;

        @Value("${redis.timeout.secs:1}")
        private int redisTimeoutInSecs;

        @Value("${redis.socket.timeout.secs:1}")
        private int redisSocketTimeoutInSecs;

        @Value("${redis.ttl.hours:1}")
        private int redisDataTTL;


        @Bean
        public LettuceConnectionFactory redisConnectionFactory() {

                final SocketOptions socketOptions = SocketOptions.builder().connectTimeout(Duration.ofSeconds(redisSocketTimeoutInSecs)).build();
                
                final ClientOptions clientOptions = ClientOptions.builder().socketOptions(socketOptions).build();

                LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                                .commandTimeout(Duration.ofSeconds(redisTimeoutInSecs)).clientOptions(clientOptions).build();
                RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(redisHost, redisPort);

                final LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(serverConfig, clientConfig);
                lettuceConnectionFactory.setValidateConnection(true);
                return lettuceConnectionFactory;

        }

        @Bean
        public RedisTemplate<Object, Object> redisTemplate() {
                RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
                redisTemplate.setConnectionFactory(redisConnectionFactory());
                return redisTemplate;
        }

        @Bean
        public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) {

                RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
                                .entryTtl(Duration.ofHours(redisDataTTL))
                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java()));

                redisCacheConfiguration.usePrefix();

                RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
                                .cacheDefaults(redisCacheConfiguration).build();

                redisCacheManager.setTransactionAware(true);
                return redisCacheManager;
        }


        @Override
        public CacheErrorHandler errorHandler() {
                return new RedisCacheErrorHandler();
        }
}

Here we have added a few enhancement to the config , we have added timout options and added CacheErrorHandler

The CacheErrorHandler is the one that handles connection time out issue .


public class RedisCacheErrorHandler implements CacheErrorHandler {

        private static final Logger log = LoggerFactory.getLogger(RedisCacheErrorHandler.class);

        @Override
        public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                handleTimeOutException(exception);
                log.info("Unable to get from cache " + cache.getName() + " : " + exception.getMessage());
        }

        @Override
        public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                handleTimeOutException(exception);
                log.info("Unable to put into cache " + cache.getName() + " : " + exception.getMessage());
        }

        @Override
        public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                handleTimeOutException(exception);
                log.info("Unable to evict from cache " + cache.getName() + " : " + exception.getMessage());
        }

        @Override
        public void handleCacheClearError(RuntimeException exception, Cache cache) {
                handleTimeOutException(exception);
                log.info("Unable to clean cache " + cache.getName() + " : " + exception.getMessage());
        }

       /**
         * We handle redis connection timeout exception , if the exception is handled then it is treated as a cache miss and
         * gets the data from actual storage
         * 
         * @param exception
         */
        private void handleTimeOutException(RuntimeException exception) {

                if (exception instanceof RedisCommandTimeoutException)
                        return;
        }
}

The key method here is handleTimeOutException , here we handle the RedisCommandTimeoutException  exception , if the exception is handled then it is treated as a cache miss and gets the data from actual storage

The additional properties added in application.properties


redis.hostname = localhost
redis.port = 6379
redis.ttl.hours = 24
redis.timeout.secs= 2
redis.socket.timeout.secs= 2

Summary

We saw how to enable spring cache with Redis and how to handle redis server downtime without impacting the application

The code is available on  GITHUB

How to enable Basic authentication on Swagger UI using spring security

Enable Basic Authentication on Swagger UI using spring security. Assuming we have already configured Swagger in our project , we shall turn on basic authentication using spring security on spring boot v2...

App & Geek   Never miss a story from App & Geek, get updates in your inbox.