Java缓存技术可分为远端缓存和本地缓存,远端缓存常用的方案有著名的redis和memcache,而本地缓存的代表技术主要有HashMap,Guava Cache,Caffeine和Encahche。本篇博文仅覆盖了本地缓存,且突出探讨高性能的本地缓存。

本篇博文将首先介绍常见的本地缓存技术,对本地缓存有个大概的了解;其次介绍本地缓存中号称性能最好的Cache,可以探讨看看到底有多好?怎么做到这么好?最后通过几个实战样例,在日常工作中应用高性能的本地缓存。

一、 Java本地缓存技术介绍

1.1 使用List集合contains方法循环遍历(有序) 1.1 HashMap

通过Map的底层方式,直接将需要缓存的对象放在内存中。

  • 优点:简单粗暴,不需要引入第三方包,比较适合一些比较简单的场景。

  • 缺点:没有缓存淘汰策略,定制化开发成本高。

public class LRUCache extends LinkedHashMap {/** * 可重入读写锁,保证并发读写安全性 */private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();private Lock readLock = readWriteLock.readLock();private Lock writeLock = readWriteLock.writeLock();/** * 缓存大小限制 */private int maxSize;public LRUCache(int maxSize) {super(maxSize + 1, 1.0f, true);this.maxSize = maxSize;}@Overridepublic Object get(Object key) {readLock.lock();try {return super.get(key);} finally {readLock.unlock();}}@Overridepublic Object put(Object key, Object value) {writeLock.lock();try {return super.put(key, value);} finally {writeLock.unlock();}}@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return this.size() > maxSize;}}

1.2 Guava Cache

Guava Cache是由Google开源的基于LRU替换算法的缓存技术。但Guava Cache由于被下面即将介绍的Caffeine全面超越而被取代,因此不特意编写示例代码了,有兴趣的读者可以访问Guava Cache主页。

  • 优点:支持最大容量限制,两种过期删除策略(插入时间和访问时间),支持简单的统计功能。

  • 缺点:springboot2和spring5都放弃了对Guava Cache的支持。

1.3 Caffeine

Caffeine采用了W-TinyLFU(LUR和LFU的优点结合)开源的缓存技术。缓存性能接近理论最优,属于是Guava Cache的增强版。

public class CaffeineCacheTest {public static void main(String[] args) throws Exception {//创建guava cacheCache loadingCache = Caffeine.newBuilder()//cache的初始容量.initialCapacity(5)//cache最大缓存数.maximumSize(10)//设置写缓存后n秒钟过期.expireAfterWrite(17, TimeUnit.SECONDS)//设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite//.expireAfterAccess(17, TimeUnit.SECONDS).build();String key = "key";// 往缓存写数据loadingCache.put(key, "v");// 获取value的值,如果key不存在,获取value后再返回String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);// 删除keyloadingCache.invalidate(key);}private static String getValueFromDB(String key) {return "v";}}

1.4 Encache

Ehcache是一个纯java的进程内缓存框架,具有快速、精干的特点。是hibernate默认的cacheprovider。

  • 优点:支持多种缓存淘汰算法,包括LFU,LRU和FIFO;缓存支持堆内缓存,堆外缓存和磁盘缓存;支持多种集群方案,解决数据共享问题。

  • 缺点:性能比Caffeine差

public class EncacheTest {public static void main(String[] args) throws Exception {// 声明一个cacheBuilderCacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().withCache("encacheInstance", CacheConfigurationBuilder//声明一个容量为20的堆内缓存.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20))).build(true);// 获取Cache实例Cache myCache =cacheManager.getCache("encacheInstance", String.class, String.class);// 写缓存myCache.put("key","v");// 读缓存String value = myCache.get("key");// 移除换粗cacheManager.removeCache("myCache");cacheManager.close();}}

在Caffeine的官网介绍中,Caffeine在性能和功能上都与其他几种方案相比具有优势,因此接下来主要探讨Caffeine的性能和实现原理。

二、高性能缓存Caffeine

2.1 缓存类型

2.1.1 Cache

Cache cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(10_000).build();// 查找一个缓存元素, 没有查找到的时候返回nullGraph graph = cache.getIfPresent(key);// 查找缓存,如果缓存不存在则生成缓存元素,如果无法生成则返回nullgraph = cache.get(key, k -> createExpensiveGraph(key));// 添加或者更新一个缓存元素cache.put(key, graph);// 移除一个缓存元素cache.invalidate(key);

Cache接口提供了显式搜索查找、更新和移除缓存元素的能力。当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get也许会返回null。

2.1.2 Loading Cache

LoadingCache cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));// 查找缓存,如果缓存不存在则生成缓存元素,如果无法生成则返回nullGraph graph = cache.get(key);// 批量查找缓存,如果缓存不存在则生成缓存元素Map graphs = cache.getAll(keys);

一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。
如果缓存不错在,则会通过CacheLoader.load来生成对应的缓存元素。

2.1.3 Loading Cache 2.1.3 Async Cache

AsyncCache cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(10_000).buildAsync();// 查找一个缓存元素, 没有查找到的时候返回nullCompletableFuture graph = cache.getIfPresent(key);// 查找缓存元素,如果不存在,则异步生成graph = cache.get(key, k -> createExpensiveGraph(key));// 添加或者更新一个缓存元素cache.put(key, graph);// 移除一个缓存元素cache.synchronous().invalidate(key);

AsyncCache就是Cache的异步形式,提供了Executor生成缓存元素并返回CompletableFuture的能力。默认的线程池实现是ForkJoinPool.commonPool(),当然你也可以通过覆盖并实现Caffeine.executor(Executor)方法来自定义你的线程池选择。

2.1.4 Async Loading Cache

AsyncLoadingCache cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// 你可以选择: 去异步的封装一段同步操作来生成缓存元素.buildAsync(key -> createExpensiveGraph(key));// 你也可以选择: 构建一个异步缓存元素操作并返回一个future.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));// 查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture graph = cache.get(key);// 批量查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Map> graphs = cache.getAll(keys);

AsyncLoadingCache就是LoadingCache的异步形式,提供了异步load生成缓存元素的功能。

2.2 驱逐策略

  • 基于容量

// 基于缓存内的元素个数进行驱逐LoadingCache graphs = Caffeine.newBuilder().maximumSize(10_000).build(key -> createExpensiveGraph(key));// 基于缓存内元素权重进行驱逐LoadingCache graphs = Caffeine.newBuilder().maximumWeight(10_000).weigher((Key key, Graph graph) -> graph.vertices().size()).build(key -> createExpensiveGraph(key));
  • 基于时间

// 基于固定的过期时间驱逐策略LoadingCache graphs = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));LoadingCache graphs = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));// 基于不同的过期驱逐策略LoadingCache graphs = Caffeine.newBuilder().expireAfter(new Expiry() {public long expireAfterCreate(Key key, Graph graph, long currentTime) {// Use wall clock time, rather than nanotime, if from an external resourcelong seconds = graph.creationDate().plusHours(5).minus(System.currentTimeMillis(), MILLIS).toEpochSecond();return TimeUnit.SECONDS.toNanos(seconds);}public long expireAfterUpdate(Key key, Graph graph, long currentTime, long currentDuration) {return currentDuration;}public long expireAfterRead(Key key, Graph graph,long currentTime, long currentDuration) {return currentDuration;}}).build(key -> createExpensiveGraph(key));
  • 基于引用

// 当key和缓存元素都不再存在其他强引用的时候驱逐LoadingCache graphs = Caffeine.newBuilder().weakKeys().weakValues().build(key -> createExpensiveGraph(key));// 当进行GC的时候进行驱逐LoadingCache graphs = Caffeine.newBuilder().softValues().build(key -> createExpensiveGraph(key));

2.3 刷新机制

LoadingCache graphs = Caffeine.newBuilder().maximumSize(10_000).refreshAfterWrite(1, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));

只有在LoadingCache中可以使用刷新策略,与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。

2.4 统计

Cache graphs = Caffeine.newBuilder().maximumSize(10_000).recordStats().build();

通过使用Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个CacheStats对象,其将会含有一些统计指标,比如:

  • hitRate():查询缓存的命中率

  • evictionCount():被驱逐的缓存数量

  • averageLoadPenalty():新值被载入的平均耗时

配合SpringBoot提供的RESTful Controller,能很方便的查询Cache的使用情况。

三、Caffeine在SpringBoot的实战

按照Caffeine Github官网文档的描述,Caffeine是基于Java8的高性能缓存库。并且在Spring5(SpringBoot2.x)官方放弃了Guava,而使用了性能更优秀的Caffeine作为默认的缓存方案。

SpringBoot使用Caffeine有两种方式:

  • 方式一:直接引入Caffeine依赖,然后使用Caffeine的函数实现缓存

  • 方式二:引入Caffeine和Spring Cache依赖,使用SpringCache注解方法实现缓存
    下面分别介绍两种使用方式。

方式一:使用Caffeine依赖

首先引入maven相关依赖:

com.github.ben-manes.caffeinecaffeine

其次,设置缓存的配置选项

@Configurationpublic class CacheConfig {@Beanpublic Cache caffeineCache() {return Caffeine.newBuilder()// 设置最后一次写入或访问后经过固定时间过期.expireAfterWrite(60, TimeUnit.SECONDS)// 初始的缓存空间大小.initialCapacity(100)// 缓存的最大条数.maximumSize(1000).build();}}

最后给服务添加缓存功能

@Slf4j@Servicepublic class UserInfoServiceImpl {/** * 模拟数据库存储数据 */private HashMap userInfoMap = new HashMap();@AutowiredCache caffeineCache;public void addUserInfo(UserInfo userInfo) {userInfoMap.put(userInfo.getId(), userInfo);// 加入缓存caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);}public UserInfo getByName(Integer id) {// 先从缓存读取caffeineCache.getIfPresent(id);UserInfo userInfo = (UserInfo) caffeineCache.asMap().get(String.valueOf(id));if (userInfo != null){return userInfo;}// 如果缓存中不存在,则从库中查找userInfo = userInfoMap.get(id);// 如果用户信息不为空,则加入缓存if (userInfo != null){caffeineCache.put(String.valueOf(userInfo.getId()),userInfo);}return userInfo;}public UserInfo updateUserInfo(UserInfo userInfo) {if (!userInfoMap.containsKey(userInfo.getId())) {return null;}// 取旧的值UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());// 替换内容if (!StringUtils.isEmpty(oldUserInfo.getAge())) {oldUserInfo.setAge(userInfo.getAge());}if (!StringUtils.isEmpty(oldUserInfo.getName())) {oldUserInfo.setName(userInfo.getName());}if (!StringUtils.isEmpty(oldUserInfo.getSex())) {oldUserInfo.setSex(userInfo.getSex());}// 将新的对象存储,更新旧对象信息userInfoMap.put(oldUserInfo.getId(), oldUserInfo);// 替换缓存中的值caffeineCache.put(String.valueOf(oldUserInfo.getId()),oldUserInfo);return oldUserInfo;}@Overridepublic void deleteById(Integer id) {userInfoMap.remove(id);// 从缓存中删除caffeineCache.asMap().remove(String.valueOf(id));}}

方式二:使用Spring Cache注解

首先引入maven相关依赖

org.springframework.bootspring-boot-starter-cachecom.github.ben-manes.caffeinecaffeine

其次,配置缓存管理类

@Configurationpublic class CacheConfig {/** * 配置缓存管理器 * * @return 缓存管理器 */@Bean("caffeineCacheManager")public CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder()// 设置最后一次写入或访问后经过固定时间过期.expireAfterAccess(60, TimeUnit.SECONDS)// 初始的缓存空间大小.initialCapacity(100)// 缓存的最大条数.maximumSize(1000));return cacheManager;}}

最后给服务添加缓存功能

@Slf4j@Service@CacheConfig(cacheNames = "caffeineCacheManager")public class UserInfoServiceImpl {/** * 模拟数据库存储数据 */private HashMap userInfoMap = new HashMap();@CachePut(key = "#userInfo.id")public void addUserInfo(UserInfo userInfo) {userInfoMap.put(userInfo.getId(), userInfo);}@Cacheable(key = "#id")public UserInfo getByName(Integer id) {return userInfoMap.get(id);}@CachePut(key = "#userInfo.id")public UserInfo updateUserInfo(UserInfo userInfo) {if (!userInfoMap.containsKey(userInfo.getId())) {return null;}// 取旧的值UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());// 替换内容if (!StringUtils.isEmpty(oldUserInfo.getAge())) {oldUserInfo.setAge(userInfo.getAge());}if (!StringUtils.isEmpty(oldUserInfo.getName())) {oldUserInfo.setName(userInfo.getName());}if (!StringUtils.isEmpty(oldUserInfo.getSex())) {oldUserInfo.setSex(userInfo.getSex());}// 将新的对象存储,更新旧对象信息userInfoMap.put(oldUserInfo.getId(), oldUserInfo);// 返回新对象信息return oldUserInfo;}@CacheEvict(key = "#id")public void deleteById(Integer id) {userInfoMap.remove(id);}}