最佳内存缓存框架Caffeine

摘要:
Caffeine是一个基于Java 8.1的高性能缓存库和最佳(最佳)缓存框架。LoadingCachegraphs=Caffeine.newBuilder()2.maximumSize3.expireAfterWrite4.refreshAfterWrite5.build;缓存类似于ConcurrentMap,但并不相同。基本区别在于ConcurrentMap保存添加到其中的所有元素,直到它们被显式删除。另一方面,缓存通常被配置为自动删除条目以限制其内存占用。在某些情况下,LoadingCache或AsyncLoadingCache可能很有用,因为它是自动缓存的。如果超过此值,将被拒绝很长时间

Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架。

Cache(缓存),基于Google Guava,Caffeine提供一个内存缓存,大大改善了设计Guava's cache 和 ConcurrentLinkedHashMap 的体验。

1 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()2     .maximumSize(10_000)3     .expireAfterWrite(5, TimeUnit.MINUTES)4     .refreshAfterWrite(1, TimeUnit.MINUTES)5     .build(key -> createExpensiveGraph(key));

缓存类似于ConcurrentMap,但二者并不完全相同。最基本的区别是,ConcurrentMap保存添加到其中的所有元素,直到显式地删除它们。另一方面,缓存通常配置为自动删除条目,以限制其内存占用。在某些情况下,LoadingCache或AsyncLoadingCache可能很有用,因为它是自动缓存加载的。

Caffeine提供了灵活的结构来创建缓存,并且有以下特性:

  • 自动加载条目到缓存中,可选异步方式
  • 可以基于大小剔除
  • 可以设置过期时间,时间可以从上次访问或上次写入开始计算
  • 异步刷新
  • keys自动包装在弱引用中
  • values自动包装在弱引用或软引用中
  • 条目剔除通知
  • 缓存访问统计

1. 加载/填充

Caffeine提供以下四种类型的加载策略:

1.1. Manual

 1 Cache<Key, Graph> cache = Caffeine.newBuilder() 2     .expireAfterWrite(10, TimeUnit.MINUTES) 3     .maximumSize(10_000) 4     .build(); 5  6 // Lookup an entry, or null if not found 7 Graph graph = cache.getIfPresent(key); 8 // Lookup and compute an entry if absent, or null if not computable 9 graph = cache.get(key, k -> createExpensiveGraph(key));10 // Insert or update an entry11 cache.put(key, graph);12 // Remove an entry13 cache.invalidate(key);

Cache接口可以显式地控制检索、更新和删除条目。

1.2. Loading

1 LoadingCache<Key, Graph> cache = Caffeine.newBuilder()2     .maximumSize(10_000)3     .expireAfterWrite(10, TimeUnit.MINUTES)4     .build(key -> createExpensiveGraph(key));5 6 // Lookup and compute an entry if absent, or null if not computable7 Graph graph = cache.get(key);8 // Lookup and compute entries that are absent9 Map<Key, Graph> graphs = cache.getAll(keys);

LoadingCache通过关联一个CacheLoader来构建Cache

通过LoadingCache的getAll方法,可以批量查询

1.3. Asynchronous (Manual)

1 AsyncCache<Key, Graph> cache = Caffeine.newBuilder()2     .expireAfterWrite(10, TimeUnit.MINUTES)3     .maximumSize(10_000)4     .buildAsync();5 6 // Lookup and asynchronously compute an entry if absent7 CompletableFuture<Graph> graph = cache.get(key, k -> createExpensiveGraph(key));

AsyncCache是另一种Cache,它基于Executor计算条目,并返回一个CompletableFuture。

1.4. Asynchronously Loading

 1 AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder() 2     .maximumSize(10_000) 3     .expireAfterWrite(10, TimeUnit.MINUTES) 4     // Either: Build with a synchronous computation that is wrapped as asynchronous  5     .buildAsync(key -> createExpensiveGraph(key)); 6     // Or: Build with a asynchronous computation that returns a future 7     .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor)); 8  9 // Lookup and asynchronously compute an entry if absent10 CompletableFuture<Graph> graph = cache.get(key);11 // Lookup and asynchronously compute entries that are absent12 CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

AsyncLoadingCache 是关联了 AsyncCacheLoader 的 AsyncCache

2. 剔除

Caffeine提供三种剔除方式:基于大小、基于时间、基于引用

2.1. Size-based

 1 // Evict based on the number of entries in the cache 2 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 3     .maximumSize(10_000) 4     .build(key -> createExpensiveGraph(key)); 5  6 // Evict based on the number of vertices in the cache 7 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 8     .maximumWeight(10_000) 9     .weigher((Key key, Graph graph) -> graph.vertices().size())10     .build(key -> createExpensiveGraph(key));

如果缓存的条目数量不应该超过某个值,那么可以使用Caffeine.maximumSize(long)。如果超过这个值,则会剔除很久没有被访问过或者不经常使用的那个条目。

如果,不同的条目有不同的权重值的话,那么你可以用Caffeine.weigher(Weigher)来指定一个权重函数,并且使用Caffeine.maximumWeight(long)来设定最大的权重值。

简单的来说,要么限制缓存条目的数量,要么限制缓存条目的权重值,二者取其一。限制数量很好理解,限制权重的话首先你得提供一个函数来设定每个条目的权重值是多少,然后才能显示最大的权重是多少。

2.2. Time-based

 1 // Evict based on a fixed expiration policy 2 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 3     .expireAfterAccess(5, TimeUnit.MINUTES) 4     .build(key -> createExpensiveGraph(key)); 5 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 6     .expireAfterWrite(10, TimeUnit.MINUTES) 7     .build(key -> createExpensiveGraph(key)); 8  9 // Evict based on a varying expiration policy10 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()11     .expireAfter(new Expiry<Key, Graph>() {12       public long expireAfterCreate(Key key, Graph graph, long currentTime) {13         // Use wall clock time, rather than nanotime, if from an external resource14         long seconds = graph.creationDate().plusHours(5)15             .minus(System.currentTimeMillis(), MILLIS)16             .toEpochSecond();17         return TimeUnit.SECONDS.toNanos(seconds);18       }19       public long expireAfterUpdate(Key key, Graph graph, 20           long currentTime, long currentDuration) {21         return currentDuration;22       }23       public long expireAfterRead(Key key, Graph graph,24           long currentTime, long currentDuration) {25         return currentDuration;26       }27     })28     .build(key -> createExpensiveGraph(key));
  • expireAfterAccess(long, TimeUnit): 最后一次被访问(读或者写)后多久失效
  • expireAfterWrite(long, TimeUnit): 最后一次被创建或修改后多久失效
  • expireAfter(Expiry): 创建后多久失效

建议,主动维护缓存中条目,而不是等到访问的时候发现缓存条目已经失效了才去重新加载。意思就是,提前加载,定期维护。

可以在构建的时候Caffeine.scheduler(Scheduler)来指定调度线程

2.3. Reference-based

 1 // Evict when neither the key nor value are strongly reachable 2 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 3     .weakKeys() 4     .weakValues() 5     .build(key -> createExpensiveGraph(key)); 6  7 // Evict when the garbage collector needs to free memory 8 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() 9     .softValues()10     .build(key -> createExpensiveGraph(key));

Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则允许垃圾回收器回收该条目。注意,这是使用==判断key的。

Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则允许垃圾回收器回收该条目。注意,这是使用==判断key的。

Caffeine.softValues() 使用软引用存储value。

3. 删除

术语:

  • eviction 指受策略影响而被删除
  • invalidation 值被调用者手动删除
  • removal 值因eviction或invalidation而发生的一种行为

3.1. 明确地删除

1 // individual key2 cache.invalidate(key)3 // bulk keys4 cache.invalidateAll(keys)5 // all keys6 cache.invalidateAll()

3.2. 监听器

1 Cache<Key, Graph> graphs = Caffeine.newBuilder()2     .removalListener((Key key, Graph graph, RemovalCause cause) ->3         System.out.printf("Key %s was removed (%s)%n", key, cause))4     .build();

4. 刷新

1 LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()2     .maximumSize(10_000)3     .refreshAfterWrite(1, TimeUnit.MINUTES)4     .build(key -> createExpensiveGraph(key));

通过LoadingCache.refresh(K)进行异步刷新,通过覆盖CacheLoader.reload(K, V)可以自定义刷新逻辑

5. 统计

1 Cache<Key, Graph> graphs = Caffeine.newBuilder()2     .maximumSize(10_000)3     .recordStats()4     .build();

使用Caffeine.recordStats(),你可以打开统计功能。Cache.stats()方法会返回一个CacheStats对象,该对象提供以下统计信息:

  • hitRate(): 命中率
  • evictionCount(): 被剔除的条目数量
  • averageLoadPenalty(): 加载新值所花费的平均时间

6. 示例

终于要说到重点了

一般来讲,用Redis作为一级话缓存,Caffeine作为二级缓存

6.1. 示例一:单独使用

pom.xml

1 <groupId>com.github.ben-manes.caffeine</groupId>2     <artifactId>caffeine</artifactId>3     <version>2.8.0</version>4 </dependency>

config

 1 package com.cjs.example.config; 2  3 import com.alibaba.fastjson.JSON; 4 import com.cjs.example.model.Student; 5 import com.github.benmanes.caffeine.cache.CacheLoader; 6 import com.github.benmanes.caffeine.cache.Caffeine; 7 import com.github.benmanes.caffeine.cache.LoadingCache; 8 import com.github.benmanes.caffeine.cache.Scheduler; 9 import lombok.extern.slf4j.Slf4j;10 import org.checkerframework.checker.nullness.qual.NonNull;11 import org.checkerframework.checker.nullness.qual.Nullable;12 import org.springframework.beans.factory.annotation.Autowired;13 import org.springframework.context.annotation.Bean;14 import org.springframework.context.annotation.Configuration;15 import org.springframework.data.redis.core.HashOperations;16 import org.springframework.data.redis.core.StringRedisTemplate;17 import org.springframework.util.StringUtils;18 19 import java.util.concurrent.TimeUnit;20 21 /**22  * @author ChengJianSheng23  * @date 2019-09-1524  */25 @Slf4j26 @Configuration27 public class CacheConfig {28 29     @Autowired30     private StringRedisTemplate stringRedisTemplate;31 32     @Bean("studentCache")33     public LoadingCache<Integer, Student> studentCache() {34           return Caffeine.newBuilder()35                   .maximumSize(10).recordStats()36                   .expireAfterWrite(1, TimeUnit.HOURS)37 //                  .scheduler(Scheduler.systemScheduler())  // 需要自定义调度器,用定时任务去主动提前刷新38                   .build(new CacheLoader<Integer, Student>() {39                       @Nullable40                       @Override41                       public Student load(@NonNull Integer key) throws Exception {42                           log.info("从缓存中加载...key={}", key);43                           HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();44                           String value = hashOperations.get("STU_HS", String.valueOf(key));45                           if (StringUtils.isEmpty(value)) {46                               return null;47                           }48                           return JSON.parseObject(value, Student.class);49                       }50                   });51     }52 53 54 }

service

 1 package com.cjs.example.service; 2  3 import com.cjs.example.model.Student; 4 import com.github.benmanes.caffeine.cache.LoadingCache; 5 import org.springframework.stereotype.Service; 6  7 import javax.annotation.Resource; 8 import java.util.Comparator; 9 import java.util.List;10 import java.util.Map;11 import java.util.stream.Collectors;12 13 /**14  * @author ChengJianSheng15  * @date 2019-09-1516  */17 @Service18 public class StudentService {19 20     @Resource(name = "studentCache")21     private LoadingCache<Integer, Student> studentCache;22 23     public Student getById(Integer id) {24         return studentCache.get(id);25     }26 27     public List<Student> getAll(List<Integer> idList) {28         Map<Integer, Student> studentMap = studentCache.getAll(idList);29         return studentMap.values().parallelStream().sorted(Comparator.comparing(Student::getId)).collect(Collectors.toList());30     }31 32     public Double hitRate() {33         return studentCache.stats().hitRate();34     }35 36     /**37      * 不直接写到本地缓存,而是先写到Redis,然后从Redis中读到本地38      */39 }

补充一点:你都用本地缓存了,必定已经用了一级缓存了。一级缓存无法达到预期的性能,才会选择用本地缓存。

controller

 1 package com.cjs.example.controller; 2  3 import com.cjs.example.model.Student; 4 import com.cjs.example.service.StudentService; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.web.bind.annotation.GetMapping; 7 import org.springframework.web.bind.annotation.PathVariable; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RestController;10 11 import java.util.Arrays;12 import java.util.List;13 14 /**15  * @author ChengJianSheng16  * @date 2019-09-1517  */18 @RestController19 @RequestMapping("/student")20 public class StudentController {21 22     @Autowired23     private StudentService studentService;24 25     @GetMapping("/info/{studentId}")26     public Student info(@PathVariable("studentId") Integer studentId) {27         return studentService.getById(studentId);28     }29 30     @GetMapping("/getAll")31     public List<Student> getAll() {32         return studentService.getAll(Arrays.asList(101, 102, 103, 104, 105));33     }34 35     @GetMapping("/hitRate")36     public Double hitRate() {37         return studentService.hitRate();38     }39 }
最佳内存缓存框架Caffeine

6.2. 示例二:和SpringBoot一起用

SpringBoot支持Caffeine,可以简化一些步骤,但同时也有诸多限制

application.yml

 1 spring: 2   redis: 3     host: 127.0.0.1 4     password: 123456 5     port: 6379 6   cache: 7     type: caffeine 8     cache-names: teacher 9     caffeine:10       spec: maximumSize=500,expireAfterAccess=600s

service

 1 package com.cjs.example.service; 2  3 import com.alibaba.fastjson.JSON; 4 import com.cjs.example.model.Teacher; 5 import lombok.extern.slf4j.Slf4j; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.cache.annotation.Cacheable; 8 import org.springframework.data.redis.core.HashOperations; 9 import org.springframework.data.redis.core.StringRedisTemplate;10 import org.springframework.stereotype.Service;11 import org.springframework.util.StringUtils;12 13 /**14  * @author ChengJianSheng15  * @date 2019-09-1516  */17 @Slf4j18 @Service19 public class TeacherService {20 21     @Autowired22     private StringRedisTemplate stringRedisTemplate;23 24     @Cacheable(cacheNames = "teacher", key = "#teacherId")25     public Teacher getById(Integer teacherId) {26         log.info("从缓存中读取...Key={}", teacherId);27         HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();28         String value = hashOperations.get("TEA_HS", String.valueOf(teacherId));29         if (StringUtils.isEmpty(value)) {30             return null;31         }32         return JSON.parseObject(value, Teacher.class);33     }34 35 }
最佳内存缓存框架Caffeine

用注解方便是方便,但是不好控制,还是自定义的好

7. 工程结构

最佳内存缓存框架Caffeine

完整的pom.xml

 1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4     <modelVersion>4.0.0</modelVersion> 5     <parent> 6         <groupId>org.springframework.boot</groupId> 7         <artifactId>spring-boot-starter-parent</artifactId> 8         <version>2.1.8.RELEASE</version> 9         <relativePath/> <!-- lookup parent from repository -->10     </parent>11     <groupId>com.cjs.example</groupId>12     <artifactId>cjs-caffeine-example</artifactId>13     <version>0.0.1-SNAPSHOT</version>14     <name>cjs-caffeine-example</name>15 16     <properties>17         <java.version>1.8</java.version>18     </properties>19 20     <dependencies>21         <dependency>22             <groupId>org.springframework.boot</groupId>23             <artifactId>spring-boot-starter-cache</artifactId>24         </dependency>25         <dependency>26             <groupId>org.springframework.boot</groupId>27             <artifactId>spring-boot-starter-data-redis</artifactId>28         </dependency>29         <dependency>30             <groupId>org.springframework.boot</groupId>31             <artifactId>spring-boot-starter-web</artifactId>32         </dependency>33 34         <dependency>35             <groupId>com.github.ben-manes.caffeine</groupId>36             <artifactId>caffeine</artifactId>37             <version>2.8.0</version>38         </dependency>39 40         <dependency>41             <groupId>org.projectlombok</groupId>42             <artifactId>lombok</artifactId>43             <optional>true</optional>44         </dependency>45         <dependency>46             <groupId>com.alibaba</groupId>47             <artifactId>fastjson</artifactId>48             <version>1.2.60</version>49         </dependency>50     </dependencies>51 52     <build>53         <plugins>54             <plugin>55                 <groupId>org.springframework.boot</groupId>56                 <artifactId>spring-boot-maven-plugin</artifactId>57             </plugin>58         </plugins>59     </build>60 61 </project>

免责声明:文章转载自《最佳内存缓存框架Caffeine》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇阿里开源的那个牛X的问题排查工具——Arthas,推出IDEA插件了!WPF学习————制作时钟下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

springboot2.0 redis EnableCaching的配置和使用

一、前言   关于EnableCaching最简单使用,个人感觉只需提供一个CacheManager的一个实例就好了。springboot为我们提供了cache相关的自动配置。引入cache模块,如下。 二、maven依赖 <dependency> <groupId>org.springframework.boot&...

教你七招提高.NET网站性能

一、减少往返行程(Reduce Round Trips) 使用下面的方法可以减少Web服务器和Browser之间的往返行程: 1、为Browser启用缓存 如果呈现的内容是静态的或变化周期较长,应启用Browser缓存,避免发出冗余的http请求。 2、缓冲页面输出 如果可能,则尽量缓冲页面输出,处理结束后再一次传送到客户端,这可以避免频繁传递小块...

java进程内存溢出案例

一. 上节回顾1. 内存 2. 场景一:磁盘和文件写案例 3. 命令:vmstat 二. 上节的两个问题问题一:buffer是磁盘读数据还是写数据的缓存? 问题二:cache是对文件读数据的缓存,是不是也会缓存写文件的数据? 问题一分析步骤: java进程内存溢出,问题定位以及分析(mat) 1. 运行下面的命令,清理缓存,从文件/tmp/file中,读取...

ThinkPHP模板(一)

如何关闭ThinkPHP的模板缓存 ThinkPHP的模板缓存是无奈关闭的,因为内置的模板引擎是一个编译型的模板引擎,必须经过编译后生成一个可执行的缓存文件才能被执行。但是可以设置缓存的有效期,例如设置 ‘TMPL_CACHE_TIME’ =>3, // 模板缓存有效期 -1 永久 单位为秒 这样,每隔3秒系统会自动重新编译模板文件。默认的配置是-1...

闲话缓存:ZFS 读缓存深入研究-ARC(二)

Solaris ZFS ARC的改动(相对于IBM ARC) 如我前面所说,ZFS实现的ARC和IBM提出的ARC淘汰算法并不是完全一致的。在某些方面,它做了一些扩展: ·         ZFS ARC是一个缓存容量可变的缓存算法,它的容量可以根据系统可用内存的状态进行调整。当系统内存比较充裕的时候,它的容量可以自动增加。当系统内存比较紧张(其它事情需要...

浏览器的缓存机制

浏览器的缓存机制,其实主要就是HTTP协议定义的缓存机制(expires,Cache-control等),但是也有非HTTP协议定义的缓存机制,如使用HTML的meta标签,虽然使用上很简单,但是只有部分浏览器支持,所以并不建议使用。本文主要讲的是HTTP协议定义的缓存机制,一般将缓存定为两大类,分别是强缓存和协商缓存。 强缓存:用户发送的请求直接从客户端...