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);
}
}
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).
@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:
getUserToken("Joe") -> no cache, calls API
getUserToken("Alice") -> no cache, calls API
getUserToken("Joe") -> cached
evictUserToken("Joe") -> evicts cache for user "Joe"
getUserToken("Joe") -> no cache, calls API
getUserToken("Alice") -> cached (as it has not been evicted)
evictAll() -> evicts all cache
getUserToken("Joe") -> no cache, calls API
getUserToken("Alice") -> no cache, calls API
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:
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With