自命为缓存之王的Caffeine

Posted 湘王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自命为缓存之王的Caffeine相关的知识,希望对你有一定的参考价值。

您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~

 


 

 

之前用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定义:

 private static LoadingCache<String, String> loadingCache = null;
 
 /**
  * 自定义LoadingCache,指定过期时间expiretime
  *
  */
 private LoadingCache<String, String> initCache(long expiretime) 
     return Caffeine.newBuilder()
             .initialCapacity(1)
             .maximumSize(100)
             .expireAfterWrite(expiretime, TimeUnit.MILLISECONDS)
             .build(key -> 
                 // 没有数据或过期时返回null
                 return null;
             );
 

 

注意:时间单位是TimeUnit.MILLISECONDS,搞错了就看不到效果了。

修改saveObject()方法:

 /**
  * 保存时,需要增加过期时间,方便同步到Caffeine
  *
  * @param key
  * @param value
  * @param expiretime
  * @return
  */
 public boolean saveObject(final String key, final String value, final long expiretime) 
     Query query = new Query(Criteria.where("key").is(key));
     Update update = new Update();
     long time = System.currentTimeMillis();
     update.set("key", key);
     update.set("value", value);
     update.set("time", time);
     try 
         UpdateResult result = mongoTemplate.upsert(query, update, Cache.class);
         if (result.wasAcknowledged()) 
             // 同步到Caffeie
             loadingCache = initCache(expiretime * 1000);
             loadingCache.put(key, value);
             return true;
         
      catch (Exception e) 
         e.printStackTrace();
     
     return false;
 

 

注意其中的同步到Caffeine那两行

最后,修改getObject()——重点来了!这是最关键的一步,应用重启之后还能否和MongoDB保持时间同步,就在于它了:

 // expiretime指的是从存储到失效之间的时间间隔,单位毫秒
 public String getObject(final String key, final long expiretime) 
     String result = null;
     // loadingCache不为空说明之前已经同步过了,可以直接读取它的值
     if (null != loadingCache) 
         result = loadingCache.get(key);
         if (null != result) 
             // 读取到值时,直接返回,读取不到就去mongodb读取
             return result;
         
     
     Query query = new Query(Criteria.where("key").is(key));
     Cache cache = (Cache) mongoTemplate.findOne(query, Cache.class);
     System.out.println("getObject(" + key + ", " + expiretime + ") from mongo");
 
     if (null != cache) 
         // -1表示永不过期
         if (-1 == expiretime) 
             return cache.getValue();
         
         // 如果当前时间 - 存储cache时的时间 >= 过期间隔
         long currentTtime = System.currentTimeMillis();
         if (currentTtime - cache.getTime() >= expiretime * 1000) 
             // 删除key,并返回null
             removeObject(key);
          else 
             /**
              * 需要计算出当前时间与过期时间之间的差值,并赋予Caffeine的失效时间
              * 计算过程分析:
              * 保存时间:00:00
              * 当前时间:00:03
              * 过期时间:10秒
              * 那么第一次读取时需要将剩余的7秒赋给Caffeine
              */
             if (null == loadingCache) // loadingCache==null说明loadingCache需要同步
                 loadingCache = initCache(expiretime * 1000 - (currentTtime - cache.getTime()));
                 loadingCache.put(key, cache.getValue());
             
             return cache.getValue();
         
     
     return null;
 

 

由于保存时增加了过期时间,Service和Controller也要修改:

 /**
  * 缓存Service接口
  *
  * @author 湘王
  */
 @Service
 public class CacheService 
    @Autowired
    private CacheDao<Cache> cacheDao;
 
    public String getObject(final String key, final long expiretime) 
       return cacheDao.getObject(key, expiretime);
    
 
    /**
     * 增加了过期时间expiretime
     *
     * @param key
     * @param value
     * @param expiretime
     * @return
     */
    public boolean saveObject(final String key, final String value, final long expiretime) 
       return cacheDao.saveObject(key, value, expiretime);
    
 
    public boolean removeObject(final String key) 
       return cacheDao.removeObject(key);
    
 

 

 /**
  * Cache控制器
  *
  * 湘王
  */
 @RestController
 public class CacheController 
     @Autowired
     private CacheService cacheService;
 
     /**
      * 增加了过期时间expiretime
      *
      * @param key
      * @param value
      * @param expiretime
      */
     @GetMapping("/cache/save")
     public void save(final String key, final String value, final int expiretime) 
         cacheService.saveObject(key, value, expiretime);
     
 
     // 获取数据,过期时间为秒(会转换为毫秒)
     @GetMapping("/cache/get")
     public String get(final String key, final int expiretime) 
         String result = cacheService.getObject(key, expiretime);
         if (null == result) 
             return "expire value";
         
         return result;
     
 

 

修改后测试:

1、启动应用,通过save()保存,再通过get()读取,有效;

2、启动应用,通过get()读取,读取不到值(因为未设置),有效; 

3、启动应用,通过save()保存,停止服务并稍后重启(可以在过期时间内重启,也可以在过期时间外重启):

3.1、通过get()读取,如果是在有效期内,能够读取到值,有效;

3.2、通过get()读取,如果超过有效期,就读取不到值了,有效。

 

 


 

 

感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~

以上是关于自命为缓存之王的Caffeine的主要内容,如果未能解决你的问题,请参考以下文章

自命为缓存之王的Caffeine

缓存之王Caffeine Cache,性能比Guava更强,命中率更高!

本地缓存之王——Caffeine 组件最强讲解!

SpringBoot 集成缓存性能之王 Caffeine

万字详解本地缓存之王 Caffeine 的高性能设计之道!

本地缓存性能之王Caffeine