Redian新闻
>
Java本地高性能缓存实践

Java本地高性能缓存实践

科技

Java缓存技术可分为远端缓存和本地缓存,远端缓存常用的方案有著名的redis和memcache,而本地缓存的代表技术主要有HashMap,Guava Cache,Caffeine和Encahche。远端缓存将在后面的博文中进行深入探讨,此处挖个坑,因此本篇博文仅覆盖了本地缓存,且突出探讨高性能的本地缓存。
本篇博文将首先介绍常见的本地缓存技术,对本地缓存有个大概的了解;其次介绍本地缓存中号称性能最好的Cache,可以探讨看看到底有多好?怎么做到这么好?最后通过几个实战样例,在日常工作中应用高性能的本地缓存。

一、 Java本地缓存技术介绍

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; }
@Override public Object get(Object key) { readLock.lock(); try { return super.get(key); } finally { readLock.unlock(); } }
@Override public Object put(Object key, Object value) { writeLock.lock(); try { return super.put(key, value); } finally { writeLock.unlock(); } }
@Override protected 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 cache Cache<String, String> 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);
// 删除key loadingCache.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 { // 声明一个cacheBuilder CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder() .withCache("encacheInstance", CacheConfigurationBuilder //声明一个容量为20的堆内缓存 .newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20))) .build(true); // 获取Cache实例 Cache<String,String> 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<Key, Graph> 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<Key, Graph> cache = Caffeine.newBuilder()    .maximumSize(10_000)    .expireAfterWrite(10, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回nullGraph graph = cache.get(key);// 批量查找缓存,如果缓存不存在则生成缓存元素Map<Key, Graph> graphs = cache.getAll(keys);
一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。
如果缓存不错在,则会通过CacheLoader.load来生成对应的缓存元素。

2.1.3 Async Cache

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .maximumSize(10_000)    .buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回nullCompletableFuture<Graph> 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<Key, Graph> cache = Caffeine.newBuilder()    .maximumSize(10_000)    .expireAfterWrite(10, TimeUnit.MINUTES)    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素    .buildAsync(key -> createExpensiveGraph(key));    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Graph> graph = cache.get(key);// 批量查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
AsyncLoadingCache就是LoadingCache的异步形式,提供了异步load生成缓存元素的功能。

2.2 驱逐策略

  • 基于容量
// 基于缓存内的元素个数进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .maximumSize(10_000)    .build(key -> createExpensiveGraph(key));
// 基于缓存内元素权重进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .maximumWeight(10_000) .weigher((Key key, Graph graph) -> graph.vertices().size()) .build(key -> createExpensiveGraph(key));
  • 基于时间
// 基于固定的过期时间驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterAccess(5, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
// 基于不同的过期驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .expireAfter(new Expiry<Key, Graph>() { public long expireAfterCreate(Key key, Graph graph, long currentTime) { // Use wall clock time, rather than nanotime, if from an external resource long 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<Key, Graph> graphs = Caffeine.newBuilder()    .weakKeys()    .weakValues()    .build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .softValues() .build(key -> createExpensiveGraph(key));

2.3 刷新机制

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .maximumSize(10_000)    .refreshAfterWrite(1, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
只有在LoadingCache中可以使用刷新策略,与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。

2.4 统计

Cache<Key, Graph> 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相关依赖:
<dependency>    <groupId>com.github.ben-manes.caffeine</groupId>      <artifactId>caffeine</artifactId>  </dependency>
其次,设置缓存的配置选项
@Configurationpublic class CacheConfig {
@Bean public Cache<String, Object> caffeineCache() { return Caffeine.newBuilder() // 设置最后一次写入或访问后经过固定时间过期 .expireAfterWrite(60, TimeUnit.SECONDS) // 初始的缓存空间大小 .initialCapacity(100) // 缓存的最大条数 .maximumSize(1000) .build(); }
}
最后给服务添加缓存功能
@Slf4j@Servicepublic class UserInfoServiceImpl {
/** * 模拟数据库存储数据 */ private HashMap<Integer, UserInfo> userInfoMap = new HashMap<>();
@Autowired Cache<String, Object> 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; }
@Override public void deleteById(Integer id) { userInfoMap.remove(id); // 从缓存中删除 caffeineCache.asMap().remove(String.valueOf(id)); }
}

方式二:使用Spring Cache注解

首先引入maven相关依赖
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-cache</artifactId></dependency><dependency>    <groupId>com.github.ben-manes.caffeine</groupId>    <artifactId>caffeine</artifactId></dependency>
其次,配置缓存管理类
@Configuration  public 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<Integer, UserInfo> 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); }
}

四、Caffeine在Reactor的实战

Caffeine和Reactor的结合是通过CacheMono和CacheFlux来使用的,Caffine会存储一个Flux或Mono作为缓存的结果。
首先定义Caffeine的缓存:
final Cache<String, String> caffeineCache = Caffeine.newBuilder()      .expireAfterWrite(Duration.ofSeconds(30))      .recordStats()      .build();

CacheMono

final Mono<String> cachedMonoCaffeine = CacheMono      .lookup(          k -> Mono.justOrEmpty(caffeineCache.getIfPresent(k)).map(Signal::next),          key      )      .onCacheMissResume(this.handleCacheMiss(key))      .andWriteWith((k, sig) -> Mono.fromRunnable(() ->          caffeineCache.put(k, Objects.requireNonNull(sig.get()))      ));
lookup方法查询cache中是否已存在,如果不存在,则通过onCacheMissResume重新生成一个Mono,并通过andWriteWith方法将结果存入缓存中。

CacheFlux

final Flux<Integer> cachedFluxCaffeine = CacheFlux      .lookup(          k -> {            final List<Integer> cached = caffeineCache.getIfPresent(k);             if (cached == null) {              return Mono.empty();            }             return Mono.just(cached)                .flatMapMany(Flux::fromIterable)                .map(Signal::next)                .collectList();          },          key      )      .onCacheMissResume(this.handleCacheMiss(key))      .andWriteWith((k, sig) -> Mono.fromRunnable(() ->          caffeineCache.put(              k,              sig.stream()                  .filter(signal -> signal.getType() == SignalType.ON_NEXT)                  .map(Signal::get)                  .collect(Collectors.toList())          )      ));
同理CacheFlux的用法也类似。

参考:

  • https://www.javadevjournal.com/spring-boot/spring-boot-with-caffeine-cache/
  • https://sunitc.dev/2020/08/27/springboot-implement-caffeine-cache/
  • https://github.com/ben-manes/caffeine/wiki/Population-zh-CN
  • Guava Cache主页:https://github.com/google/guava/wiki/CachesExplained
  • Caffeine的官网:https://github.com/ben-manes/caffeine/wiki/Benchmarks


🚀🚀🚀Serverless 应用引擎 SAE产品评测征集令发布!


写下你的使用体验,就有机会获得CHERRY机械键盘、Serverless卫衣、SAE专业版季包1份(价值8300元)、30元天猫超市卡多重好礼🎁~


点击阅读原文查看详情。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
能缓解新冠症状?“东北神药”黄桃罐头卖断货,网友:你不懂!怪人自动增量计算:构建高性能数据分析系统的任务编排数据如何助力企业发展?从高性能计算、商业智能、数据库三个领域分享搬到美东,在美国的第二份工作黄桃罐头能缓解新冠症状?“可能加重”2.4K star,一个高性能、无侵入的Java性能监控和统计工具,有点东西!高性能计算的次世代,要移平三座大山三天两款国产药上市,能缓解新冠重症吗?性能提升 2.5 倍!字节开源高性能 C++ JSON 库 sonic-cpp「砺算科技」完成过亿Pre A轮融资,专注高性能图形GPU,面向元宇宙、云渲染、新能源车应用丨早起看早期龙卷风健康快递 216「中科融合」获数千万A+轮融资,自研高性能低功耗3D SOC算力芯片将于年底发布丨36氪首发布达佩斯还是哈尔滨? 大快朵颐出了洋相 奥匈帝国4日游(3)提倡就地过节:假日消费聚焦本地客源,企业挖掘本地商机风头直逼连花清瘟,黄桃罐头遭抢购!能缓解新冠症状?人民日报辟谣:咳嗽时食用或加重症状2.5K屏标压轻薄本双十一特惠,45W高性能释放,最低4499元入手辟谣 |欧洲能源焦虑,真的只有中国电热毯才能缓解吗?安谋科技智能物联及汽车业务线负责人赵永超:高性能融合计算IP平台助力车规级芯片突破|GTIC 2022演讲预告针对特种兵实验证明:高强度运动后体液流失 补充电解质饮料能缓解疲劳秋冬运动速干衣 | 高弹亲肤的高性能运动服,让秋冬季也能舒适运动12代酷睿+高性能独显本,华硕灵耀Pro系列首发抢购气愤!8旬华裔老人遭围殴!凶手竟是一群未成年少年!殴打尾随,被抓后又只能缓刑释放...美国的鼎泰丰感染新冠「水泥封鼻」怎么办?一个方法能缓解预告 | 高性能秋冬运动速干衣 & 再也没有了的Modarri高颜值组装模型车高性能计算:RoCE技术分析及应用「砺算科技」完成过亿Pre A轮融资,专注高性能图形GPU,面向元宇宙、云渲染、新能源车应用|36氪首发临聘教师生存实录:“我和结婚只差一个编制”被美国学校“封杀”的漫画名著,时代悲剧下的生存实录能缓解新冠症状?东北“神秘力量”黄桃罐头火了…「光梓科技」完成数千万C1轮融资,专注高性能光电通讯和传感芯片|36氪首发高性能延迟服务实现之路高性能!超轻量!AI模型编译器MegCC开源了!让推理引擎体积变小小小小小…[照片] 分享一些最近的拍摄的照片,希望能缓解症状中的你
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。