Spring 启动本机缓存:如何 expire/remove 单独缓存数据 keys/elements
Spring Boot Native cache : How to expire/remove cache data by individual keys/elements
我们正在调用身份联合服务以非常频繁地获取用户令牌,并且几乎 运行 对身份服务进行负载测试。
一个潜在的解决方案是在现有应用程序中缓存用户令牌,但是使用本机 spring-缓存,我们可以使单个缓存条目过期吗?
在下面的示例中,我能够清除缓存,删除所有条目,但是我试图使单个条目过期。
@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如下:
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
您可以通过在获取缓存键的方法上使用 @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");
}
}
例如:
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
基于 TTL 的缓存
如果您希望将您的令牌缓存一定时间,除了本机 Spring 之外,您还需要另一个 CacheManager
。 Spring 的 @Cacheable
有多种缓存选项。我将举一个使用 Caffeine 的示例,Caffeine 是 Java 8 的高性能缓存库。例如,如果您知道要将令牌缓存 30 分钟,您可能希望采用这种方式。
首先,将以下依赖项添加到您的 build.gradle
(或者如果使用 Maven,请翻译以下内容并将其放入您的 pom.xml
)。请注意,您需要使用最新版本,或者与当前 Spring 启动版本相匹配的版本。
compile 'org.springframework.boot:spring-boot-starter-cache:2.1.4'
compile 'com.github.ben-manes.caffeine:caffeine:2.7.0'
添加这两个依赖项后,您所要做的就是在 application.properties
文件中配置 caffeine
规范:
spring.cache.cache-names=userTokens
spring.cache.caffeine.spec=expireAfterWrite=30m
将 expireAfterWrite=30m
更改为您希望代币赖以生存的任何价值。例如,如果您想要 400 秒,可以将其更改为 expireAfterWrite=400s
.
有用的链接:
Spring 缓存抽象是一种抽象而非实现,因此它根本不支持显式设置 TTL,因为这是特定于实现的功能。例如,如果您的缓存由 ConcurrentHashMap
支持,它不能支持开箱即用的 TTL。
对于您的情况,您有 2 个选择。如果您需要的是本地缓存(即每个微服务实例管理自己的缓存),您可以将 Spring 缓存抽象替换为 Caffeine,这是由 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
}
}
同样,这是一个 local 缓存,这意味着每个微服务都将管理自己的令牌集。因此,如果您有同一微服务的 5 个实例 运行,则同一用户可能在所有 5 个缓存中都有 5 个令牌,具体取决于哪些实例处理了他的请求。
另一方面,如果你需要分布式缓存(即多个微服务实例共享同一个集中式缓存),你需要看看EHCache or Hazelcast。在这种情况下,您可以继续使用 Spring 缓存抽象,并通过从这些库中声明 CacheManager
(例如 HazelcastCacheManager
)来选择其中一个库作为您的实现。
然后您可以查看相应的文档以进一步配置您选择的 CacheManager
和特定缓存的 TTL(例如您的 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);
}
}
我们正在调用身份联合服务以非常频繁地获取用户令牌,并且几乎 运行 对身份服务进行负载测试。
一个潜在的解决方案是在现有应用程序中缓存用户令牌,但是使用本机 spring-缓存,我们可以使单个缓存条目过期吗?
在下面的示例中,我能够清除缓存,删除所有条目,但是我试图使单个条目过期。
@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如下:
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
您可以通过在获取缓存键的方法上使用 @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");
}
}
例如:
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
基于 TTL 的缓存
如果您希望将您的令牌缓存一定时间,除了本机 Spring 之外,您还需要另一个 CacheManager
。 Spring 的 @Cacheable
有多种缓存选项。我将举一个使用 Caffeine 的示例,Caffeine 是 Java 8 的高性能缓存库。例如,如果您知道要将令牌缓存 30 分钟,您可能希望采用这种方式。
首先,将以下依赖项添加到您的 build.gradle
(或者如果使用 Maven,请翻译以下内容并将其放入您的 pom.xml
)。请注意,您需要使用最新版本,或者与当前 Spring 启动版本相匹配的版本。
compile 'org.springframework.boot:spring-boot-starter-cache:2.1.4'
compile 'com.github.ben-manes.caffeine:caffeine:2.7.0'
添加这两个依赖项后,您所要做的就是在 application.properties
文件中配置 caffeine
规范:
spring.cache.cache-names=userTokens
spring.cache.caffeine.spec=expireAfterWrite=30m
将 expireAfterWrite=30m
更改为您希望代币赖以生存的任何价值。例如,如果您想要 400 秒,可以将其更改为 expireAfterWrite=400s
.
有用的链接:
Spring 缓存抽象是一种抽象而非实现,因此它根本不支持显式设置 TTL,因为这是特定于实现的功能。例如,如果您的缓存由 ConcurrentHashMap
支持,它不能支持开箱即用的 TTL。
对于您的情况,您有 2 个选择。如果您需要的是本地缓存(即每个微服务实例管理自己的缓存),您可以将 Spring 缓存抽象替换为 Caffeine,这是由 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
}
}
同样,这是一个 local 缓存,这意味着每个微服务都将管理自己的令牌集。因此,如果您有同一微服务的 5 个实例 运行,则同一用户可能在所有 5 个缓存中都有 5 个令牌,具体取决于哪些实例处理了他的请求。
另一方面,如果你需要分布式缓存(即多个微服务实例共享同一个集中式缓存),你需要看看EHCache or Hazelcast。在这种情况下,您可以继续使用 Spring 缓存抽象,并通过从这些库中声明 CacheManager
(例如 HazelcastCacheManager
)来选择其中一个库作为您的实现。
然后您可以查看相应的文档以进一步配置您选择的 CacheManager
和特定缓存的 TTL(例如您的 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);
}
}