Spring Boot本机缓存:如何通过单个键/元素过期/删除缓存数据

时间:2019-04-02 19:17:53

标签: spring-boot caching spring-cache

我们正在调用身份联合身份验证服务来非常频繁地获取用户令牌,并且几乎在身份服务上运行负载测试。

一种潜在的解决方案是在现有应用程序中缓存用户令牌,但是使用本机spring-cache,我们可以使单个缓存项到期吗?

在下面的示例中,我能够清除缓存,删除所有条目,但是我试图使单个条目失效。

@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应用程序类如下:

@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application {

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

2 个答案:

答案 0 :(得分:1)

您可以在获取缓存密钥的方法上使用@CacheEvict使单个缓存条目失效。另外,通过使用Spring的缓存和@Cacheable,不需要HashMap代码(因为这实际上只是辅助缓存)。

简单缓存

@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");
    }
}

例如:

  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的缓存

如果您希望令牌在一定时间内被缓存,除了本地Spring令牌之外,还需要另一个CacheManager。 Spring的@Cacheable可以使用多种缓存选项。我将给出一个使用Caffeine的示例,Caffeine是Java 8的高性能缓存库。例如,如果您知道要在30分钟内缓存令牌,那么您可能会选择这种方法。

首先,将以下依赖项添加到build.gradle中(或者,如果使用Maven,请翻译以下内容并将其放入pom.xml中)。请注意,您将要使用最新版本,或者使用与当前Spring Boot版本匹配的版本。

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

添加了这两个依赖项后,您要做的就是在caffeine文件中配置application.properties规范:

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

expireAfterWrite=30m更改为您希望令牌持续存在的任何值。例如,如果您需要400秒,则可以将其更改为expireAfterWrite=400s

有用的链接:

答案 1 :(得分:1)

Spring Cache Abstraction是一个抽象而非实现,因此它根本不支持显式设置TTL,因为这是特定于实现的功能。例如,如果您的缓存由ConcurrentHashMap支持,则它不支持现成的TTL。

在您的情况下,您有2个选择。如果您需要的是本地缓存(即每个微服务实例都管理自己的缓存),则可以用Caffeine替换Spring Cache Abstraction,它是由Spring Boot提供和管理的官方依赖项。只需声明而无需提及版本。

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

然后,您可以如下创建缓存的实例。您放入缓存中的每个令牌都会根据您的配置自动删除。

@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
    }
}

同样,这是一个本地缓存,这意味着每个微服务都将管理自己的令牌集。因此,如果您有5个运行同一微服务的实例,则同一用户可能在所有5个缓存中都有5个令牌,具体取决于哪个实例处理了他的请求。

另一方面,如果您需要分布式缓存(即多个微服务实例共享同一集中式缓存),则需要查看EHCacheHazelcast。在这种情况下,您可以继续使用Spring Cache Abstraction,并通过从这些库中声明CacheManager(例如HazelcastCacheManager)来选择其中一个库作为实现。

然后,您可以查看相应的文档,以使用TTL为特定的缓存(例如,CacheManager)进一步配置所选的tokenCache。我以下面的示例为Hazelcast提供了一个简单的配置。

@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);
    }
}