说明
之前在做项目时,需要在项目启动时将一些数据加载到内存使用,当时的做法是存在了一个map中,每个不同的缓存单独使用一个map,在使用时会在固定的时间间隔重新加载数据,这个时间的设置,是通过一个volatile类型的时间变量作为判断依据,每次在请求数据时会先判断时间是否超时,是则重新加载数据并更新这个变量。但是对这个变量是否需要设置volatile这个关键字很是纠结,通过发帖,查资料,了解了在并发修改变量时,应该要保证变量的安全性和可见性,所以使用该关键字。但是这样的做法始终觉得有问题,首先在并发请求时,会不会造成重复更新,还有在修改map时还会有请求数据的操作,这样也会造成很大的问题,这里有个严重的错误是在存在并发更新的情况下居然没有使用CouncurrentHashMap。基于以上种种问题,寻求一种良好的解决方案,于是学习了guava缓存的使用,通过spring的缓存支持,可以做到异步刷新,过期时间的设置等。在这里记录总结下guava的配置及使用方式。
正文
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
2.创建配置类
在学习时通过创建guava配置属性类来对缓存属性进行设置,没有通过spring提供的自动配置。
创建GuavaProperties类,通过该类接收属性配置值。
@ConfigurationProperties(prefix = "guava.cache.config")
public class GuavaProperties {
private long maximumSize;
private long maximumWeight;
private long expireAfterAccessDuration;
private long expireAfterWriteDuration;
private long refreshDuration;
private int initialCapacity;
private int concurrencyLevel;
....省略getter setter方法
}
同时希望通过cacheLoader进行数据的加载及在数据更新时使用线程池异步重新加载,这里创建GuavaCacheLoader类。
public class GuavaCacheLoader extends CacheLoader<String,String> {
private final ExecutorService executorService = Executors.newFixedThreadPool(4);
@Override
public String load(String s) throws Exception {
if (s.equals("hello")) {
Thread.sleep(3000);
return "world";
} else if (s.equals("world")) {
Thread.sleep(5000);
return "hello";
}
return "no value";
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
ListenableFutureTask<String> task = ListenableFutureTask.create(new Callable<String>() {
@Override
public String call() throws Exception {
if (key.equals("hello")) {
return "nihao";
} else if (key.equals("world")) {
return "shijie";
}
return "no value";
}
});
executorService.submit(task);
return task;
}
}
这里用线程睡眠来模拟真实加载数据时的耗时。在重新加载数据时对数据的返回值进行了变更。
3.配置Guava
创建配置类GuavaCacheConfig,通过创建CacheManager来管理不同的cache。
@EnableConfigurationProperties(GuavaProperties.class)
@Configuration
@EnableCaching
public class GuavaCacheConfig {
@Autowired
private GuavaProperties guavaProperties;
@Bean
public CacheBuilder<Object,Object> cacheBuilder(){
long maximumSize = guavaProperties.getMaximumSize();
long expireAfterWrite = guavaProperties.getExpireAfterWriteDuration();
long expireAfterAccess = guavaProperties.getExpireAfterAccessDuration();
long refreshDuration = guavaProperties.getRefreshDuration();
if(maximumSize <= 0){
maximumSize = 1024;
}
if(expireAfterAccess <= 0){
expireAfterAccess = 3600;
}
if(expireAfterWrite <= 0){
expireAfterWrite = 3600;
}
if(refreshDuration <= 0){
refreshDuration = 1800;
}
return CacheBuilder.newBuilder().maximumSize(maximumSize)
.expireAfterWrite(expireAfterWrite,TimeUnit.SECONDS)
.refreshAfterWrite(refreshDuration,TimeUnit.SECONDS);
}
@Bean(name = "guavaCacheLoader")
public CacheLoader cacheLoader(){
return new GuavaCacheLoader();
}
@Bean
public CacheManager cacheManager(@Qualifier("cacheBuilder")CacheBuilder cacheBuilder,
@Qualifier("guavaCacheLoader")CacheLoader cacheLoader){
GuavaCacheManager cacheManager = new GuavaCacheManager();
cacheManager.setCacheBuilder(cacheBuilder);
cacheManager.setCacheLoader(cacheLoader);
return cacheManager;
}
}
在application.propertiesp配置文件设置配置属性值,这里为了演示,去掉了最后操作数据过期的时长设置。
设置的属性为写入后10s数据过期进行刷新,20s后数据过期重新写入
guava.cache.config.expire-after-write-duration=20
#更新间隔时长
guava.cache.config.refresh-duration=10
guava.cache.config.maximumSize=1024
4.测试
编写测试controller
@RestController
public class HelloController {
@Autowired
private CacheManager cacheManager;
@RequestMapping("hello")
public String hello(){
long start = System.currentTimeMillis();
String value = cacheManager.getCache("hello").get("hello").get().toString();
long end = System.currentTimeMillis();
return String.format("value=[%s], wait time : [%d]",value,(end - start));
}
@RequestMapping("world")
public String world(){
long start = System.currentTimeMillis();
String value = cacheManager.getCache("world").get("world").get().toString();
long end = System.currentTimeMillis();
return String.format("value=[%s], wait time : [%d]",value,(end - start));
}
}
测试结果
初次请求
再次请求
过期刷新
到这里Guava缓存的使用学习总结已经结束了,这里我只根据之前的项目需要来使用guava,有很多特性,如缓存注释的使用在这里并没有体现。需要的同学可以查询资料进行学习。
在下篇博文中,会针对springboot2所支持的缓存caffeine进行总结学习。spring5后spring官方放弃了guava而使用了更优秀的caffeine缓存,同时spring的缓存支持也有较大的变动,在下篇博文中将会利用caffeine来学习缓存的使用。
参考资料:
http://ifeve.com/google-guava-cachesexplained/
https://blog.****.net/a67474506/article/details/52608855
源码地址:
https://github.com/Edenwds/springboot_study/tree/master/guava