为了账号安全,请及时绑定邮箱和手机立即绑定

自命为缓存之王的Caffeine(6)

标签:
Java

您好,我是湘王,这是我的慕课手记,欢迎您来,欢迎您再来~


之前用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的过期时间要能够灵活调整的问题。

https://img1.sycdn.imooc.com//63ef8db00001566a05110275.jpg

 

所以需要放弃注解式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()读取,如果超过有效期,就读取不到值了,有效。


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


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消