Spring Boot Native cache : How to expire/remove cache data by individual keys/elements

Amit Kaneria picture Amit Kaneria · Apr 2, 2019 · Viewed 11.2k times · Source

We are calling Identity federation service to acquire user tokens very frequently and almost running load test on Identity service.

A potential solution is to cache the user tokens in existing application, however with native spring-cache, can we expire individual cache entries ?

With below example, I was able to clear the cache, removing all entries, however I am trying to expire individual entries.

@Service
@CacheConfig(cacheNames =  {"userTokens"})
public class UserTokenManager {

    static HashMap<String, String> userTokens = new HashMap<>();

    @Cacheable
    public String getUserToken(String userName){
        String userToken = userTokens.get(userName);
        if(userToken == null){
            // call Identity service to acquire tokens
            System.out.println("Adding UserName:" + userName + " Token:" + userToken);
            userTokens.put(userName, userToken);
        }
        return userToken;
    }

    @CacheEvict(allEntries = true, cacheNames = { "userTokens"})
    @Scheduled(fixedDelay = 3600000)
    public void removeUserTokens() {
        System.out.println("##############CACHE CLEANING##############, " +
            "Next Cleanup scheduled at : " + new Date(System.currentTimeMillis()+ 3600000));
        userTokens.clear();
    }
}

Spring-boot application class is as below:

@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application {

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

Answer

devshawn picture devshawn · Apr 9, 2019

You can expire a single cache entry by using @CacheEvict on a method that takes your cache key. Also, by using Spring's cache and @Cacheable, there's no need for the HashMap code (as that's really just a secondary cache).

Simple Cache

@Service
@CacheConfig(cacheNames = {"userTokens"})
public class UserTokenManager {

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

    @Cacheable(cacheNames = {"userTokens"})
    public String getUserToken(String userName) {
        log.info("Fetching user token for: {}", userName);
        String token = ""; //replace with call for token
        return token;
    }

    @CacheEvict(cacheNames = {"userTokens"})
    public void evictUserToken(String userName) {
        log.info("Evicting user token for: {}", userName);
    }

    @CacheEvict(cacheNames = {"userTokens"}, allEntries = true)
    public void evictAll() {
        log.info("Evicting all user tokens");
    }
}

For example:

  1. getUserToken("Joe") -> no cache, calls API
  2. getUserToken("Alice") -> no cache, calls API
  3. getUserToken("Joe") -> cached
  4. evictUserToken("Joe") -> evicts cache for user "Joe"
  5. getUserToken("Joe") -> no cache, calls API
  6. getUserToken("Alice") -> cached (as it has not been evicted)
  7. evictAll() -> evicts all cache
  8. getUserToken("Joe") -> no cache, calls API
  9. getUserToken("Alice") -> no cache, calls API

TTL-based Cache

If you want your tokens to be cached for a certain amount of time, you'll need another CacheManager besides the native Spring one. There are a variety of cache options that work with Spring's @Cacheable. I'll give an example using Caffeine, a high performance caching library for Java 8. For example, if you know you want to cache a token for 30 minutes, you'll likely want to go with this route.

First, add the following dependencies to your build.gradle (or if using Maven, translate the following and put it in your pom.xml). Note that you'll want to use the latest versions, or the ones that match with your current Spring Boot version.

compile 'org.springframework.boot:spring-boot-starter-cache:2.1.4'
compile 'com.github.ben-manes.caffeine:caffeine:2.7.0'

Once you've added those two dependencies, all you have to do is configure the caffeine spec in your application.properties file:

spring.cache.cache-names=userTokens
spring.cache.caffeine.spec=expireAfterWrite=30m

Change expireAfterWrite=30m to whatever value you'd like tokens to live for. For example, if you wanted 400 seconds, you could change it to expireAfterWrite=400s.

Useful links: