Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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);
    }
}
like image 307
Amit Kaneria Avatar asked Apr 02 '19 19:04

Amit Kaneria


2 Answers

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:

  • Caffeine Spec JavaDoc
  • Spring Boot Supported Cache Providers
like image 63
devshawn Avatar answered Nov 23 '22 17:11

devshawn


Spring Cache Abstraction is an abstraction and not an implementation, so it doesn't support setting TTL explicitly at all as this is an implementation-specific feature. For example, if your cache is backed by the ConcurrentHashMap, it cannot support TTL out-of-the-box.

In your case, you have 2 options. If what you need is a local cache (i.e. each microservice instance manages their own cache), you can replace Spring Cache Abstraction with Caffeine which an official dependency provided & managed by Spring Boot. Just need to declare without mentioning the version.

<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
</dependency>

Then you can create an instance of the cache as following. Each token you put into the cache will automatically get removed based on your configuration.

@Service
public class UserTokenManager {
    private static Cache<String, String> tokenCache;   

    @Autowired
    private UserTokenManager (@Value("${token.cache.time-to-live-in-seconds}") int timeToLiveInSeconds) {
        tokenCache = Caffeine.newBuilder()
                             .expireAfterWrite(timeToLiveInSeconds, TimeUnit.SECONDS)
                             // Optional listener for removal event
                             .removalListener((userName, tokenString, cause) -> System.out.println("TOKEN WAS REMOVED FOR USER: " + userName))
                             .build();
    }

    public String getUserToken(String userName){
        // If cached, return; otherwise create, cache and return
        // Guaranteed to be atomic (i.e. applied at most once per key)
        return tokenCache.get(userName, userName -> this.createToken(userName));
    }

    private String createToken(String userName) {
        // call Identity service to acquire tokens
    }
}

Again, this is a local cache, which means each microservice will manage their own set of tokens. So if you have 5 instances of the same microservice running, the same user may have 5 tokens lying in all 5 caches depending on which instances handled his requests.

On the other hand, if you need a distributed cache (i.e. multiple microservice instances share the same centralized cache), you need to take a look at EHCache or Hazelcast. In this case, you can continue to use Spring Cache Abstraction and pick one of these library as your implementation by declaring a CacheManager from these library (e.g. HazelcastCacheManager).

You can then take a look at the respective documentation to further configure your chosen CacheManager with TTL for specific caches (e.g. your tokenCache). I provided a simple configuration for Hazelcast below as an example.

@Configuration
public class DistributedCacheConfiguration {
    @Bean
    public HazelcastInstance hazelcastInstance(@Value("${token.cache.time-to-live-in-seconds}") int timeToLiveInSeconds) {
        Config config = new Config();                  
        config.setInstanceName("hazelcastInstance");

        MapConfig mapConfig = config.getMapConfig("tokenCache");
        mapConfig.setTimeToLiveSeconds(timeToLiveInSeconds);

        return Hazelcast.newHazelcastInstance(config);
    }

    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
        return new HazelcastCacheManager(hazelcastInstance);
    }
}
like image 44
Mr.J4mes Avatar answered Nov 23 '22 18:11

Mr.J4mes