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
@Configuration- 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.
@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() {
@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() {
}
@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.
This is the simplest implementation of expiring cache using pure Spring technologies.
Comments
Post a Comment