SprintBoot expiring Cache

SpringBoot timed caching using Redis

SpringCache is a powerful cache abstraction that can work on any cache. But it lacks one feature - expiring cache. @Cacheable simplifies caching, but it doesn't allow us to specify time to evict a cache. @Scheduled to rescue.

@Scheduled helps well with in-memory cache. But I personally found other flavors of in-memory cache better e.g.
  • Guava cache
  • Caffine
But what about external cache like "redis"?

Keep in mind that combination below is deadly for caching
  • @Scheduled
  • Multiple instances of same service
  • External cache like redis
You may stare at problems below
  • race condition if you use "cron" flavor of @Scheduled
  • quicker cache invalidation if you use "fixedDelay" flavor of @Scheduled
Personally, I do not mind using "cron" with race condition in a few cases like
  • fetching data from external service which returns results in a few seconds (1-2 seconds)
  • building cache takes a very small amount of time - fraction of a second
But if building cache takes considerable time, then we should use an external scheduler like Quartz - so that only one instance rebuilds cache.

What is quicker cache invalidation? @Scheduler allows us to schedule by fixedDelay, fixedRate, initialDelay. These depend on when the timer starts per service instance. So if you configure a delay of 1 hour, this 1 hour may arrive differently for different service instances.

Below I demonstrate an example of how I use @Scheduled where I do not mind race condition.

Cache where race condition is okay

Dependency

e.g. for maven
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-data-redis</artifactid>
        </dependency>

Cache usage

User Service.java
    @Service
    public class UserService {
        @Cacheable(value = CacheNamespace.USER, unless = "#result == null")
        public User getUser(UUID userUUID) {
            ...
        }
    }
UserProgramService.java
    @Service
    public class UserProgramService {
        @Cacheable(value = CacheNamespace.USER_PROGRAMS, unless = "#result == null")
        public List getProgramsForUser(UUID userUUID) {
            ...
        }
    }

Notice that I use a CacheNamespace interface. This allows me to 
  • keep information about all the caches in a single place
  • have a namespace prefix for my cache. This is useful for external cache like redis shared by multiple applications - namespace saves from cache conflict.
Mine looks like this
    public interface CacheNamespace {
        String USER = "exampleapp_user";
        String USER_PROGRAMS = "exampleapp_user_programs";
    }

Using @Scheduled to evict cache

First we need to tell SpringBoot that we need to use a scheduler
    @Configuration
    @EnableScheduling
    public class SchedulingConfig {
    }

And finally, use of @Scheduled to evict caches. As per documentation, @Scheduled cron takes 6 params
  • second
  • minute
  • hour
  • day of month
  • month
  • day of week
Note: Linux cron has 5 fields only. Seconds field is not there.

I usually keep schedules in one place - in CacheConfig class. So I know where to look for if I wish to tweak cache.
    @Configuration
    @EnableCaching
    public class CacheConfig {
        private static Logger logger = LoggerFactory.getLogger(CacheConfig.class);
        @Scheduled(cron = "0 */5 * * * *")
        @CacheEvict(beforeInvocation = true, cacheNames = CacheNamespace.USER)
        public void evictInFiveMinutes() {
            logger.trace("Evicting cache namespace: {}", CacheNamespace. USER);
        }


        @Scheduled(cron = "0 0 */1 * * *")
        @CacheEvict(beforeInvocation = true, cacheNames = CacheNamespace.USER_PROGRAMS)
        public void evictInOneHour() {
            logger.trace("Evicting cache namespace: {}", CacheNamespace. USER_PROGRAMS);
        }
    }

This is the simplest implementation of expiring cache using pure Spring technologies.

Comments