自命为缓存之王的Caffeine(6)
您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~
之前用Caffeine替代Redis的时候,发现先保存KV,再获取key,过期时间为3秒。但即使过了3秒,还是能获取到保存的数据。这是为什么呢?因为之前在整合SpringBoot时,使用的是注解方式,在配置文件中已经定死了Caffeine的过期时间。
## Caffeine
spring.cache.cache-names=test
spring.cache.type=caffeine
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=300s
就是因为这里的expireAfterWrite=300s导致数据3秒后不能清除。经过测试,发现果然是300秒后Caffeine过期。
使用注解式的Caffeine,应用一旦启动,是无法动态调整过期时间的,必然与MongoDB时间不同步。
进一步延伸思考:Caffeine是没有持久化功能的,所以当应用重新启动的时候,上一次为Caffeine设置的过期时间会被重置。因此Caffeine + MongoDB替代Redis存储Token其实需要解决一个很关键的问题:MongoDB和Caffeine过期时间的同步问题,也就是Caffeine的过期时间要能够灵活调整的问题。
所以,需要放弃注解式Caffeine,使用自定义LoadingCache。当MongoDB保存时,就要同步到Caffeine。而当应用重启时,就要重新同步Caffeine。
修改CacheDao,增加LoadingCache定义:
1 private static LoadingCache<String, String> loadingCache = null; 2 3 /** 4 * 自定义LoadingCache,指定过期时间expiretime 5 * 6 */ 7 private LoadingCache<String, String> initCache(long expiretime) { 8 return Caffeine.newBuilder() 9 .initialCapacity(1) 10 .maximumSize(100) 11 .expireAfterWrite(expiretime, TimeUnit.MILLISECONDS) 12 .build(key -> { 13 // 没有数据或过期时返回null 14 return null; 15 }); 16 }
注意:时间单位是TimeUnit.MILLISECONDS,搞错了就看不到效果了。
修改saveObject()方法:
1 /** 2 * 保存时,需要增加过期时间,方便同步到Caffeine 3 * 4 * @param key 5 * @param value 6 * @param expiretime 7 * @return 8 */ 9 public boolean saveObject(final String key, final String value, final long expiretime) { 10 Query query = new Query(Criteria.where("key").is(key)); 11 Update update = new Update(); 12 long time = System.currentTimeMillis(); 13 update.set("key", key); 14 update.set("value", value); 15 update.set("time", time); 16 try { 17 UpdateResult result = mongoTemplate.upsert(query, update, Cache.class); 18 if (result.wasAcknowledged()) { 19 // 同步到Caffeie 20 loadingCache = initCache(expiretime * 1000); 21 loadingCache.put(key, value); 22 return true; 23 } 24 } catch (Exception e) { 25 e.printStackTrace(); 26 } 27 return false; 28 }
注意其中的同步到Caffeine那两行。
最后,修改getObject()——重点来了!这是最关键的一步,应用重启之后还能否和MongoDB保持时间同步,就在于它了:
1 // expiretime指的是从存储到失效之间的时间间隔,单位毫秒 2 public String getObject(final String key, final long expiretime) { 3 String result = null; 4 // loadingCache不为空说明之前已经同步过了,可以直接读取它的值 5 if (null != loadingCache) { 6 result = loadingCache.get(key); 7 if (null != result) { 8 // 读取到值时,直接返回,读取不到就去mongodb读取 9 return result; 10 } 11 } 12 Query query = new Query(Criteria.where("key").is(key)); 13 Cache cache = (Cache) mongoTemplate.findOne(query, Cache.class); 14 System.out.println("getObject(" + key + ", " + expiretime + ") from mongo"); 15 16 if (null != cache) { 17 // -1表示永不过期 18 if (-1 == expiretime) { 19 return cache.getValue(); 20 } 21 // 如果当前时间 - 存储cache时的时间 >= 过期间隔 22 long currentTtime = System.currentTimeMillis(); 23 if (currentTtime - cache.getTime() >= expiretime * 1000) { 24 // 删除key,并返回null 25 removeObject(key); 26 } else { 27 /** 28 * 需要计算出当前时间与过期时间之间的差值,并赋予Caffeine的失效时间 29 * 计算过程分析: 30 * 保存时间:00:00 31 * 当前时间:00:03 32 * 过期时间:10秒 33 * 那么第一次读取时需要将剩余的7秒赋给Caffeine 34 */ 35 if (null == loadingCache) {// loadingCache==null说明loadingCache需要同步 36 loadingCache = initCache(expiretime * 1000 - (currentTtime - cache.getTime())); 37 loadingCache.put(key, cache.getValue()); 38 } 39 return cache.getValue(); 40 } 41 } 42 return null; 43 }
由于保存时增加了过期时间,Service和Controller也要修改:
1 /** 2 * 缓存Service接口 3 * 4 * @author 湘王 5 */ 6 @Service 7 public class CacheService { 8 @Autowired 9 private CacheDao<Cache> cacheDao; 10 11 public String getObject(final String key, final long expiretime) { 12 return cacheDao.getObject(key, expiretime); 13 } 14 15 /** 16 * 增加了过期时间expiretime 17 * 18 * @param key 19 * @param value 20 * @param expiretime 21 * @return 22 */ 23 public boolean saveObject(final String key, final String value, final long expiretime) { 24 return cacheDao.saveObject(key, value, expiretime); 25 } 26 27 public boolean removeObject(final String key) { 28 return cacheDao.removeObject(key); 29 } 30 }
1 /** 2 * Cache控制器 3 * 4 * 湘王 5 */ 6 @RestController 7 public class CacheController { 8 @Autowired 9 private CacheService cacheService; 10 11 /** 12 * 增加了过期时间expiretime 13 * 14 * @param key 15 * @param value 16 * @param expiretime 17 */ 18 @GetMapping("/cache/save") 19 public void save(final String key, final String value, final int expiretime) { 20 cacheService.saveObject(key, value, expiretime); 21 } 22 23 // 获取数据,过期时间为秒(会转换为毫秒) 24 @GetMapping("/cache/get") 25 public String get(final String key, final int expiretime) { 26 String result = cacheService.getObject(key, expiretime); 27 if (null == result) { 28 return "expire value"; 29 } 30 return result; 31 } 32 }
修改后测试:
1、启动应用,通过save()保存,再通过get()读取,有效;
2、启动应用,通过get()读取,读取不到值(因为未设置),有效;
3、启动应用,通过save()保存,停止服务并稍后重启(可以在过期时间内重启,也可以在过期时间外重启):
3.1、通过get()读取,如果是在有效期内,能够读取到值,有效;
3.2、通过get()读取,如果超过有效期,就读取不到值了,有效。
感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~