文章目录
  1. 1. 缓存的使用
  2. 2. 与ConcurrentMap区别
  3. 3. 两种实现方式
    1. 3.1. CacheLoader
    2. 3.2. Callable
  4. 4. 缓存回收
    1. 4.1. 基于容量回收
    2. 4.2. 定时回收
    3. 4.3. 基于引用回收
  5. 5. 显式清除
  6. 6. 监听器
  7. 7. 刷新
  8. 8. 最新的本地缓存实现

上一篇Blog我们讲了如何在spring项目启动时将数据加载到缓存中,其中缓存我们是自己使用map来实现的,后来老大推荐使用guava中的缓存来实现本地缓存。

缓存的使用

缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用。在日常开发的很多场合,由于受限于硬盘IO的性能或者我们自身业务系统的数据处理和获取可能非常费时,当我们发现我们的系统这个数据请求量很大的时候,频繁的IO和频繁的逻辑处理会导致硬盘和CPU资源的瓶颈出现。缓存的作用就是将这些来自不易的数据保存在内存中,当有其他线程或者客户端需要查询相同的数据资源时,直接从缓存的内存块中返回数据,这样不但可以提高系统的响应时间,同时也可以节省对这些数据的处理流程的资源消耗,整体上来说,系统性能会有大大的提升。

缓存在很多场景下都是相当有用的。例如:

  1. CPU缓存
  2. 操作系统缓存
  3. 本地缓存
  4. 分布式缓存
  5. HTTP缓存
  6. 数据库缓存

与ConcurrentMap区别

很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

两种实现方式

  1. cacheLoader
  2. callable callback

两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。但不同的在于cacheloader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在get的时候指定。

CacheLoader

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

...
try {
  return graphs.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

Callable

Cache<Key, Value> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(); // look Ma, no CacheLoader
...
try {
  // If the key wasn't in the "easy to compute" group, we need to
  // do things the hard way.
  cache.get(key, new Callable<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式”如果有缓存则返回;否则运算、缓存、然后返回”。

缓存回收

Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

基于容量回收

如果要规定缓存项的数目不超过固定值,只需使用

CacheBuilder.maximumSize(long)

在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。

另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用

CacheBuilder.weigher(Weigher)

指定一个权重函数,并且用

CacheBuilder.maximumWeight(long)

指定最大总重。

定时回收

CacheBuilder提供两种定时回收的方法:

  1. expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
  2. expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

定时回收周期性地在写操作中执行,偶尔在读操作中执行。

基于引用回收

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  1. CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
  2. CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
  3. CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。

显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

  1. 个别清除:Cache.invalidate(key)
  2. 批量清除:Cache.invalidateAll(keys)
  3. 清除所有缓存项:Cache.invalidateAll()

监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

刷新

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。

最新的本地缓存实现

@Component
public class AppCache {
    private static final Logger logger = LoggerFactory.getLogger(AppCache.class);

    @Autowired
    private AppDao appDao;
    private LoadingCache<Long, App> cacheBuilder;

    public AppCache() {
        init();
    }

    private void init() {
        cacheBuilder = CacheBuilder
                .newBuilder()
                .maximumSize(1000)
                .softValues()
                .refreshAfterWrite(120, TimeUnit.SECONDS)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(new RemovalListener<Long, App>() {
                    @Override
                    public void onRemoval(RemovalNotification<Long, App> rn) {
                        logger.info("campaign {} is removed from cache", rn.getKey());
                    }
                })
                .build(new CacheLoader<Long, App>() {
                    @Override
                    public App load(Long key) throws Exception {
                        return appDao.getAppById(key);
                    }

                });
    }

    public App getApp(Long id) {
        try {
            App app = cacheBuilder.get(id);
            return app;
        } catch (ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }
}
文章目录
  1. 1. 缓存的使用
  2. 2. 与ConcurrentMap区别
  3. 3. 两种实现方式
    1. 3.1. CacheLoader
    2. 3.2. Callable
  4. 4. 缓存回收
    1. 4.1. 基于容量回收
    2. 4.2. 定时回收
    3. 4.3. 基于引用回收
  5. 5. 显式清除
  6. 6. 监听器
  7. 7. 刷新
  8. 8. 最新的本地缓存实现