likes
comments
collection

什么时候要用到本地缓存,比Redis还要快?怎么用?

作者站长头像
站长
· 阅读数 9

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

大家好,我是日暮与星辰之间,创作不易,如果觉得有用,求关注,求点赞,求评论,求收藏,求转发,谢谢。

导言

试想一下这么一个场景,一用户想要把他看了好长时间的极速版视频积攒的余额提现,于是他点击了提现按钮,哗啦声一响,他的钱就到银行卡了。这样一个对于用户很简单的动作但是对于后台往往牵扯到十几个服务(公司规模越大、规范性要求越高,整个调用链路的服务就越多),而你负责了一个交叉验证的服务,主要负责校验上游传递给你的记账标识、资金流标识、付款方账号、收款方账号是否和最初申请配置的一样。

为了产品的良好体验,大老板要求请求耗时最多1s要让用户看到结果,于是各个服务的负责人battle了一圈,给你的这个服务只预留了50ms的时间。你一想,这还不简单,直接Redis缓存走起来。Redis那一套霹雳啪撒一顿输出,测试环境也没有一点问题,结果上线后傻眼了,由于网络波动等原因你的服务经常超时,组长责令你尽快解决,再因为你的服务超时导致他被大老板骂,你的绩效就别想了。这种时候,你该怎么优化呢?

理论

要想做到比Redis还要快,首先要知道Redis为什么快,最直接的原因就是Redis所有的数据都在内存中,从内存中取数据库比从硬盘中取数据要快几个数量级。

那么想比Redis还要快,只能在数据传输上下功夫,把不同服务器之间、甚至不同进程之间的数据传输都省略掉,直接把数据放在JVM中,写个ConcurrentMap用于保存数据,但是既然是缓存,肯定还要整一套删除策略、最大空间限制、刷新策略等等。自己手撸一套代价太大了,肯定有大公司有类似的场景把这样的工作已经给做了,而且他还想赚个好名声,github里搜一搜,肯定有现成的解决方案。于是今天我们的主角就出场了,Guava Cache.

实践

首先用一段代码整体介绍一下Guava Cache的使用方式,Cache整体分为CacheBuilder和CacheLoader两个部分,CacheBuilder负责创建缓存对象,再创建的时候配置最大容量、过期方式、移除监听器,CacheLoader负责根据key来加载value。

LoadingCache<Key, Config> configs = CacheBuilder.newBuilder()
       .maximumSize(5000)
       .expireAfterWrite(30, TimeUnit.MINUTES)
       .removalListener(MY_LISTENER)
       .build(
           new CacheLoader<Key, Config>() {
             @Override
             public Graph load(Key key) throws AnyException {
               return loadFromRedis(key);
             }
           });

适用场景

  1. 凡事都是有代价的,你愿意接受占用内存空间的代价来提升速度
  2. 存储占据的数据量不至于太大,太大会导致Out of Memory异常
  3. 同一个Key会被访问很多次。

CacheLoader

CacheLoader并不是一定要在build的指定,如果你的数据有多种加载方式,可以使用callable的方式。

  cache.get(key, new Callable<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });

过期策略

过期策略分为大小基准和时间基准,大小基准可以通过CacheBuilder.maximumSize(long)CacheBuilder.maximumWeight(long).指定,maximumSize(long)适用于每个值占用的空间基本上相等或者差异可以忽略不计,只看key的数量,而maximumWeight(long)则会计算出每个值所占据的权重,并保证总权重不大于设置值,其中每个值的计算方式可以通过weigher(Weigher)进行设置。

时间基础就很好理解,分为expireAfterAccess(long, TimeUnit)expireAfterWrite(long, TimeUnit),分别是读多久后失效和写入缓存后多久失效,读失效事每次读缓存都会为该值续命。

刷新策略

CacheBuilder.refreshAfterWrite(long, TimeUnit) 方法提供了自动刷新的能力,需要注意的是,如果没有重写reload方法,那么只有当重新查到该key的时候,才会进行刷新操作。

总结

抛出了问题总要给出答案,不然就太监了,那么导言中的问题我是如何做的呢,我通过guava cache和redis整了一套二级缓存,并且在服务启动时进行了扫表操作,将所有的配置内容都预先放到guava cache中。guava的刷新时间设置为五分钟,并重写了刷新操作强制进行刷新,redis的过期时间设置为一天,并且在数据库内容更新后,删除对应Redis缓存中的值。如此便可以保证,绝大多数情况下都能命中本地中的guava 缓存,且最多有5分钟的数据不一致(业务可以接受)。 凡事必有代价,作为一名后端开发就是要在各种选择之间进行选择,选出一条代价可接受、业务能接受的方案。