最佳內存緩存框架Caffeine

Caffeine是一種高性能的緩存庫,是基於Java 8的最佳(最優)緩存框架。html

Cache(緩存),基於Google Guava,Caffeine提供一個內存緩存,大大改善了設計Guava's cache 和 ConcurrentLinkedHashMap 的體驗。java

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可能頗有用,由於它是自動緩存加載的。git

Caffeine提供了靈活的結構來建立緩存,而且有如下特性:github

  • 自動加載條目到緩存中,可選異步方式
  • 能夠基於大小剔除
  • 能夠設置過時時間,時間能夠從上次訪問或上次寫入開始計算
  • 異步刷新
  • keys自動包裝在弱引用中
  • values自動包裝在弱引用或軟引用中
  • 條目剔除通知
  • 緩存訪問統計

 

1.  加載/填充web

Caffeine提供如下四種類型的加載策略:redis

1.1.  Manualspring

 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 entry
11 cache.put(key, graph);
12 // Remove an entry
13 cache.invalidate(key);

Cache接口能夠顯式地控制檢索、更新和刪除條目。 apache

1.2.  Loading json

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 computable
7 Graph graph = cache.get(key);
8 // Lookup and compute entries that are absent
9 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 absent
7 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 absent
10 CompletableFuture<Graph> graph = cache.get(key);
11 // Lookup and asynchronously compute entries that are absent
12 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 policy
10 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 resource
14         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 key
2 cache.invalidate(key)
3 // bulk keys
4 cache.invalidateAll(keys)
5 // all keys
6 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 ChengJianSheng
23  * @date 2019-09-15
24  */
25 @Slf4j
26 @Configuration
27 public class CacheConfig {
28 
29     @Autowired
30     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                       @Nullable
40                       @Override
41                       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 ChengJianSheng
15  * @date 2019-09-15
16  */
17 @Service
18 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 ChengJianSheng
16  * @date 2019-09-15
17  */
18 @RestController
19 @RequestMapping("/student")
20 public class StudentController {
21 
22     @Autowired
23     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 }

 

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 ChengJianSheng
15  * @date 2019-09-15
16  */
17 @Slf4j
18 @Service
19 public class TeacherService {
20 
21     @Autowired
22     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 }

用註解方即是方便,可是很差控制,仍是自定義的好

 

7.  工程結構

完整的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>

https://github.com/chengjiansheng/cjs-caffeine-example

 

8.  文檔

https://github.com/ben-manes/caffeine/wiki 

https://github.com/ben-manes/caffeine

https://www.itcodemonkey.com/article/9498.html 

相關文章
相關標籤/搜索