更多學習資料,可以關注下方微信公衆號,回覆關鍵字:Java高級資料 。 即可免費獲取完整的Java高級開發工程師視頻課程。
jetcache
簡介
JetCache是一個基於Java的緩存系統封裝,提供統一的API和註解來簡化緩存的使用。 JetCache提供了比SpringCache更加強大的註解,可以原生的支持TTL、兩級緩存、分佈式自動刷新,還提供了Cache
接口用於手工緩存操作。當前有四個實現,RedisCache
、TairCache
(此部分未在github開源)、CaffeineCache
(in memory)和一個簡易的LinkedHashMapCache
(in memory),要添加新的實現也是非常簡單的。
全部特性:
- 通過統一的API訪問Cache系統
- 通過註解實現聲明式的方法緩存,支持TTL和兩級緩存
- 通過註解創建並配置
Cache
實例 - 針對所有
Cache
實例和方法緩存的自動統計 - Key的生成策略和Value的序列化策略是可以配置的
- 分佈式緩存自動刷新,分佈式鎖 (2.2+)
- 異步Cache API (2.2+,使用Redis的lettuce客戶端時)
- Spring Boot支持
要求
JetCache需要JDK1.8、Spring Framework4.0.8以上版本。Spring Boot爲可選,需要1.1.9以上版本。如果不使用註解(僅使用jetcache-core),Spring Framework也是可選的,此時使用方式與Guava/Caffeinecache類似。
文檔目錄
- 快速入門
- 基本Cache API
- 通過@CreateCache註解創建
Cache
實例 - 通過註解實現方法緩存
- 配置詳解
- 高級Cache API
- Redis支持(兩種redis客戶端二選一即可)
- 內存緩存
LinkedHashMapCache
和CaffeineCache
- 統計
- Builder:未使用Spring4(或未使用Spring)的時候,或通過Builder手工構造
Cache
- 開發者文檔
- 升級和兼容性指南
依賴哪個jar?
- jetcache-anno-api:定義jetcache的註解和常量,不傳遞依賴。如果你想把Cached註解加到接口上,又不希望你的接口jar傳遞太多依賴,可以讓接口jar依賴jetcache-anno-api。
- jetcache-core:核心api,完全通過編程來配置操作
Cache
,不依賴Spring。兩個內存中的緩存實現LinkedHashMapCache
和CaffeineCache
也由它提供。 - jetcache-anno:基於Spring提供@Cached和@CreateCache註解支持。
- jetcache-redis:使用jedis提供Redis支持。
- jetcache-redis-lettuce(需要JetCache2.3以上版本):使用lettuce提供Redis支持,實現了JetCache異步訪問緩存的的接口。
- jetcache-starter-redis:Spring Boot方式的Starter,基於Jedis。
- jetcache-starter-redis-lettuce(需要JetCache2.3以上版本):Spring Boot方式的Starter,基於Lettuce。
快速入門
創建緩存實例
通過@CreateCache註解創建一個緩存實例,默認超時時間是100秒
@CreateCache(expire = 100)
private Cache<Long, UserDO> userCache;
用起來就像map一樣
UserDO user = userCache.get(123L);
userCache.put(123L, user);
userCache.remove(123L);
創建一個兩級(內存+遠程)的緩存,內存中的元素個數限制在50個。
@CreateCache(name = "UserService.userCache", expire = 100, cacheType = CacheType.BOTH, localLimit = 50)
private Cache<Long, UserDO> userCache;
name屬性不是必須的,但是起個名字是個好習慣,展示統計數據的使用,會使用這個名字。如果同一個area兩個@CreateCache的name配置一樣,它們生成的Cache將指向同一個實例。
創建方法緩存
使用@Cached方法可以爲一個方法添加上緩存。JetCache通過Spring AOP生成代理,來支持緩存功能。註解可以加在接口方法上也可以加在類方法上,但需要保證是個Springbean。
public interface UserService {
@Cached(name="UserService.getUserById", expire = 3600)
User getUserById(long userId);
}
基本配置(使用Spring Boot)
如果使用SpringBoot,可以按如下的方式配置。
POM
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.4.4</version>
</dependency>
配置一個springboot風格的application.yml文件,把他放到資源目錄中
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: 127.0.0.1
port: 6379
然後創建一個App類放在業務包的根下,EnableMethodCache,EnableCreateCacheAnnotation這兩個註解分別激活Cached和CreateCache註解,其他和標準的SpringBoot程序是一樣的。這個類可以直接main方法運行。
package com.company.mypackage;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class);
}
}
未使用SpringBoot的配置方式
如果沒有使用springboot,可以按下面的方式配置(這裏使用jedis客戶端連接redis爲例)。
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-anno</artifactId>
<version>2.4.4</version>
</dependency>
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-redis</artifactId>
<version>2.4.4</version>
</dependency>
配置了這個JetCacheConfig類以後,可以使用@CreateCache和@Cached註解。
package com.company.mypackage;
import java.util.HashMap;
import java.util.Map;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.anno.support.SpringConfigProvider;
import com.alicp.jetcache.embedded.EmbeddedCacheBuilder;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import com.alicp.jetcache.redis.RedisCacheBuilder;
import com.alicp.jetcache.support.FastjsonKeyConvertor;
import com.alicp.jetcache.support.JavaValueDecoder;
import com.alicp.jetcache.support.JavaValueEncoder;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.util.Pool;
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public Pool<Jedis> pool(){
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
return new JedisPool(pc, "localhost", 6379);
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool<Jedis> pool){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisCacheBuilder remoteCacheBuilder = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
進一步閱讀
- CreateCache的詳細使用說明可以看這裏
- 使用@CacheCache創建的Cache接口實例,它的API使用可以看這裏
- 關於方法緩存(@Cached, @CacheUpdate, @CacheInvalidate)的詳細使用看這裏
- 詳細的配置說明看這裏。
基本Cache API
簡介
JetCache2.0的核心是com.alicp.jetcache.Cache
接口(以下簡寫爲Cache
),它提供了部分類似於javax.cache.Cache
(JSR107)的API操作。沒有完整實現JSR107的原因包括:
- 希望維持API的簡單易用。
- 對於特定的遠程緩存系統來說,
javax.cache.Cache
中定義的有些操作無法高效率的實現,比如一些原子操作方法和類似removeAll()
這樣的方法。 - JSR107比較複雜,完整實現要做的工作很多。
JSR107 style API
以下是Cache接口中和JSR107的javax.cache.Cache接口一致的方法,除了不會拋出異常,這些方法的簽名和行爲和JSR107都是一樣的。
V get(K key)
void put(K key, V value);
boolean putIfAbsent(K key, V value); //多級緩存MultiLevelCache不支持此方法
boolean remove(K key);
<T> T unwrap(Class<T> clazz);//2.2版本前,多級緩存MultiLevelCache不支持此方法
Map<K,V> getAll(Set<? extends K> keys);
void putAll(Map<? extends K,? extends V> map);
void removeAll(Set<? extends K> keys);
JetCache特有API
V computeIfAbsent(K key, Function<K, V> loader)
當key對應的緩存不存在時,使用loader加載。通過這種方式,loader的加載時間可以被統計到。
V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull)
當key對應的緩存不存在時,使用loader加載。cacheNullWhenLoaderReturnNull參數指定了當loader加載出來時null值的時候,是否要進行緩存(有時候即使是null值也是通過很繁重的查詢纔得到的,需要緩存)。
V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull, long expire, TimeUnit timeUnit)
當key對應的緩存不存在時,使用loader加載。cacheNullWhenLoaderReturnNull參數指定了當loader加載出來時null值的時候,是否要進行緩存(有時候即使是null值也是通過很繁重的查詢纔得到的,需要緩存)。expire和timeUnit指定了緩存的超時時間,會覆蓋緩存的默認超時時間。
void put(K key, V value, long expire, TimeUnit timeUnit)
put操作,expire和timeUnit指定了緩存的超時時間,會覆蓋緩存的默認超時時間。
AutoReleaseLock tryLock(K key, long expire, TimeUnit timeUnit)
boolean tryLockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action)
非堵塞的嘗試獲取一個鎖,如果對應的key還沒有鎖,返回一個AutoReleaseLock,否則立即返回空。如果Cache實例是本地的,它是一個本地鎖,在本JVM中有效;如果是redis等遠程緩存,它是一個不十分嚴格的分佈式鎖。鎖的超時時間由expire和timeUnit指定。多級緩存的情況會使用最後一級做tryLock操作。用法如下:
// 使用try-with-resource方式,可以自動釋放鎖
try(AutoReleaseLock lock = cache.tryLock("MyKey",100, TimeUnit.SECONDS)){
if(lock != null){
// do something
}
}
上面的代碼有個潛在的坑是忘記判斷if(lock!=null),所以一般可以直接用tryLockAndRun更加簡單
boolean hasRun = tryLockAndRun("MyKey",100, TimeUnit.SECONDS), () -> {
// do something
};
tryLock內部會在訪問遠程緩存失敗時重試,會自動釋放,而且不會釋放不屬於自己的鎖,比你自己做這些要簡單。當然,基於遠程緩存實現的任何分佈式鎖都不會是嚴格的分佈式鎖,不能和基於ZooKeeper或Consul做的鎖相比。
大寫API
Vget(K key)這樣的方法雖然用起來方便,但有功能上的缺陷,當get返回null的時候,無法斷定是對應的key不存在,還是訪問緩存發生了異常,所以JetCache針對部分操作提供了另外一套API,提供了完整的返回值,包括:
CacheGetResult<V> GET(K key);
MultiGetResult<K, V> GET_ALL(Set<? extends K> keys);
CacheResult PUT(K key, V value);
CacheResult PUT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);
CacheResult PUT_ALL(Map<? extends K, ? extends V> map);
CacheResult PUT_ALL(Map<? extends K, ? extends V> map, long expireAfterWrite, TimeUnit timeUnit);
CacheResult REMOVE(K key);
CacheResult REMOVE_ALL(Set<? extends K> keys);
CacheResult PUT_IF_ABSENT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);
這些方法的特徵是方法名爲大寫,與小寫的普通方法對應,提供了完整的返回值,用起來也稍微繁瑣一些。例如:
CacheGetResult<OrderDO> r = cache.GET(orderId);
if( r.isSuccess() ){
OrderDO order = r.getValue();
} else if (r.getResultCode() == CacheResultCode.NOT_EXISTS) {
System.out.println("cache miss:" + orderId);
} else if(r.getResultCode() == CacheResultCode.EXPIRED) {
System.out.println("cache expired:" + orderId));
} else {
System.out.println("cache get error:" + orderId);
}
通過@CreateCache註解創建Cache實例
簡介
在Springbean中使用@CreateCache註解創建一個Cache實例。例如
@CreateCache(expire = 100)
private Cache<Long, UserDO> userCache;
@CreateCache屬性表
屬性 |
默認值 |
說明 |
area |
“default” |
如果需要連接多個緩存系統,可在配置多個cache area,這個屬性指定要使用的那個area的name |
name |
未定義 |
指定緩存的名稱,不是必須的,如果沒有指定,會使用類名+方法名。name會被用於遠程緩存的key前綴。另外在統計中,一個簡短有意義的名字會提高可讀性。如果兩個 |
expire |
未定義 |
該Cache實例的默認超時時間定義,註解上沒有定義的時候會使用全局配置,如果此時全局配置也沒有定義,則取無窮大 |
timeUnit |
TimeUnit.SECONDS |
指定expire的單位 |
cacheType |
CacheType.REMOTE |
緩存的類型,包括CacheType.REMOTE、CacheType.LOCAL、CacheType.BOTH。如果定義爲BOTH,會使用LOCAL和REMOTE組合成兩級緩存 |
localLimit |
未定義 |
如果cacheType爲CacheType.LOCAL或CacheType.BOTH,這個參數指定本地緩存的最大元素數量,以控制內存佔用。註解上沒有定義的時候會使用全局配置,如果此時全局配置也沒有定義,則取100 |
serialPolicy |
未定義 |
如果cacheType爲CacheType.REMOTE或CacheType.BOTH,指定遠程緩存的序列化方式。JetCache內置的可選值爲SerialPolicy.JAVA和SerialPolicy.KRYO。註解上沒有定義的時候會使用全局配置,如果此時全局配置也沒有定義,則取SerialPolicy.JAVA |
keyConvertor |
未定義 |
指定KEY的轉換方式,用於將複雜的KEY類型轉換爲緩存實現可以接受的類型,JetCache內置的可選值爲KeyConvertor.FASTJSON和KeyConvertor.NONE。NONE表示不轉換,FASTJSON通過fastjson將複雜對象KEY轉換成String。如果註解上沒有定義,則使用全局配置。 |
默認值
對於以上未定義默認值的參數,如果沒有指定,將使用yml中指定的全局配置,請參考配置詳解。
通過註解實現方法緩存
JetCache方法緩存和SpringCache比較類似,它原生提供了TTL支持,以保證最終一致,並且支持二級緩存。JetCache2.4以後支持基於註解的緩存更新和刪除。
在spring環境下,使用@Cached註解可以爲一個方法添加緩存,@CacheUpdate用於更新緩存,@CacheInvalidate用於移除緩存元素。註解可以加在接口上也可以加在類上,加註解的類必須是一個spring bean,例如:
public interface UserService {
@Cached(name="userCache.", key="#userId", expire = 3600)
User getUserById(long userId);
@CacheUpdate(name="userCache.", key="#user.userId", value="#user")
void updateUser(User user);
@CacheInvalidate(name="userCache.", key="#userId")
void deleteUser(long userId);
}
key使用Spring的SpEL腳本來指定。如果要使用參數名(比如這裏的key="#userId"),項目編譯設置target必須爲1.8格式,並且指定javac的-parameters參數,否則就要使用key="args[0]"這樣按下標訪問的形式。
@CacheUpdate和@CacheInvalidate的name和area屬性必須和@Cached相同,name屬性還會用做cache的key前綴。
@Cached註解和@CreateCache的屬性非常類似,但是多幾個:
屬性 |
默認值 |
說明 |
area |
“default” |
如果在配置中配置了多個緩存area,在這裏指定使用哪個area |
name |
未定義 |
指定緩存的唯一名稱,不是必須的,如果沒有指定,會使用類名+方法名。name會被用於遠程緩存的key前綴。另外在統計中,一個簡短有意義的名字會提高可讀性。 |
key |
未定義 |
使用SpEL指定key,如果沒有指定會根據所有參數自動生成。 |
expire |
未定義 |
超時時間。如果註解上沒有定義,會使用全局配置,如果此時全局配置也沒有定義,則爲無窮大 |
timeUnit |
TimeUnit.SECONDS |
指定expire的單位 |
cacheType |
CacheType.REMOTE |
緩存的類型,包括CacheType.REMOTE、CacheType.LOCAL、CacheType.BOTH。如果定義爲BOTH,會使用LOCAL和REMOTE組合成兩級緩存 |
localLimit |
未定義 |
如果cacheType爲LOCAL或BOTH,這個參數指定本地緩存的最大元素數量,以控制內存佔用。如果註解上沒有定義,會使用全局配置,如果此時全局配置也沒有定義,則爲100 |
serialPolicy |
未定義 |
指定遠程緩存的序列化方式。可選值爲SerialPolicy.JAVA和SerialPolicy.KRYO。如果註解上沒有定義,會使用全局配置,如果此時全局配置也沒有定義,則爲SerialPolicy.JAVA |
keyConvertor |
未定義 |
指定KEY的轉換方式,用於將複雜的KEY類型轉換爲緩存實現可以接受的類型,當前支持KeyConvertor.FASTJSON和KeyConvertor.NONE。NONE表示不轉換,FASTJSON可以將複雜對象KEY轉換成String。如果註解上沒有定義,會使用全局配置。 |
enabled |
true |
是否激活緩存。例如某個dao方法上加緩存註解,由於某些調用場景下不能有緩存,所以可以設置enabled爲false,正常調用不會使用緩存,在需要的地方可使用CacheContext.enableCache在回調中激活緩存,緩存激活的標記在ThreadLocal上,該標記被設置後,所有enable=false的緩存都被激活 |
cacheNullValue |
false |
當方法返回值爲null的時候是否要緩存 |
condition |
未定義 |
使用SpEL指定條件,如果表達式返回true的時候才進行緩存 |
@CacheInvalidate註解說明:
屬性 |
默認值 |
說明 |
area |
“default” |
如果在配置中配置了多個緩存area,在這裏指定使用哪個area,指向對應的@Cached定義。 |
name |
未定義 |
指定緩存的唯一名稱,指向對應的@Cached定義。 |
key |
未定義 |
使用SpEL指定key |
condition |
未定義 |
使用SpEL指定條件,如果表達式返回true才執行刪除 |
@CacheUpdate註解說明:
屬性 |
默認值 |
說明 |
area |
“default” |
如果在配置中配置了多個緩存area,在這裏指定使用哪個area,指向對應的@Cached定義。 |
name |
未定義 |
指定緩存的唯一名稱,指向對應的@Cached定義。 |
key |
未定義 |
使用SpEL指定key |
value |
未定義 |
使用SpEL指定value |
condition |
未定義 |
使用SpEL指定條件,如果表達式返回true才執行更新 |
使用@CacheUpdate和@CacheInvalidate的時候,相關的緩存操作可能會失敗(比如網絡IO錯誤),所以指定緩存的超時時間是非常重要的。
@CacheRefresh註解說明:
屬性 |
默認值 |
說明 |
refresh |
未定義 |
刷新間隔 |
timeUnit |
TimeUnit.SECONDS |
時間單位 |
stopRefreshAfterLastAccess |
未定義 |
指定該key多長時間沒有訪問就停止刷新,如果不指定會一直刷新 |
refreshLockTimeout |
60秒 |
類型爲BOTH/REMOTE的緩存刷新時,同時只會有一臺服務器在刷新,這臺服務器會在遠程緩存放置一個分佈式鎖,此配置指定該鎖的超時時間 |
對於以上未定義默認值的參數,如果沒有指定,將使用yml中指定的全局配置,全局配置請參考配置說明。
配置詳解
yml配置文件案例(如果沒使用springboot,直接配置GlobalCacheConfig是類似的,參考快速入門教程):
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
hiddenPackages: com.alibaba
local:
default:
type: caffeine
limit: 100
keyConvertor: fastjson
expireAfterWriteInMillis: 100000
otherArea:
type: linkedhashmap
limit: 100
keyConvertor: none
expireAfterWriteInMillis: 100000
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
otherArea:
type: redis
keyConvertor: fastjson
valueEncoder: kryo
valueDecoder: kryo
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
配置通用說明如下
屬性 |
默認值 |
說明 |
jetcache.statIntervalMinutes |
0 |
統計間隔,0表示不統計 |
jetcache.areaInCacheName |
true |
jetcache-anno把cacheName作爲遠程緩存key前綴,2.4.3以前的版本總是把areaName加在cacheName中,因此areaName也出現在key前綴中。2.4.4以後可以配置,爲了保持遠程key兼容默認值爲true,但是新項目的話false更合理些。 |
jetcache.hiddenPackages |
無 |
@Cached和@CreateCache自動生成name的時候,爲了不讓name太長,hiddenPackages指定的包名前綴被截掉 |
jetcache.[local|remote].${area}.type |
無 |
緩存類型。tair、redis爲當前支持的遠程緩存;linkedhashmap、caffeine爲當前支持的本地緩存類型 |
jetcache.[local|remote].${area}.keyConvertor |
無 |
key轉換器的全局配置,當前只有一個已經實現的keyConvertor:fastjson。僅當使用@CreateCache且緩存類型爲LOCAL時可以指定爲none,此時通過equals方法來識別key。方法緩存必須指定keyConvertor |
jetcache.[local|remote].${area}.valueEncoder |
java |
序列化器的全局配置。僅remote類型的緩存需要指定,可選java和kryo |
jetcache.[local|remote].${area}.valueDecoder |
java |
序列化器的全局配置。僅remote類型的緩存需要指定,可選java和kryo |
jetcache.[local|remote].${area}.limit |
100 |
每個緩存實例的最大元素的全局配置,僅local類型的緩存需要指定。注意是每個緩存實例的限制,而不是全部,比如這裏指定100,然後用@CreateCache創建了兩個緩存實例(並且註解上沒有設置localLimit屬性),那麼每個緩存實例的限制都是100 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis |
無窮大 |
以毫秒爲單位指定超時時間的全局配置(以前爲defaultExpireInMillis) |
jetcache.local.${area}.expireAfterAccessInMillis |
0 |
需要jetcache2.2以上,以毫秒爲單位,指定多長時間沒有訪問,就讓緩存失效,當前只有本地緩存支持。0表示不使用這個功能。 |
上表中${area}對應@Cached和@CreateCache的area屬性。注意如果註解上沒有指定area,默認值是"default"。
關於緩存的超時時間,有多個地方指定,澄清說明一下:
- put等方法上指定了超時時間,則以此時間爲準
- put等方法上未指定超時時間,使用Cache實例的默認超時時間
- Cache實例的默認超時時間,通過在@CreateCache和@Cached上的expire屬性指定,如果沒有指定,使用yml中定義的全局配置,例如@Cached(cacheType=local)使用jetcache.local.default.expireAfterWriteInMillis,如果仍未指定則是無窮大
高級Cache API
CacheBuilder
CacheBuilder提供使用代碼直接構造Cache實例的方式,使用說明看這裏。如果沒有使用Spring,可以使用CacheBuilder,否則沒有必要直接使用CacheBuilder。
異步API
從JetCache2.2版本開始,所有的大寫API返回的CacheResult都支持異步。當底層的緩存實現支持異步的時候,大寫API返回的結果都是異步的。當前支持異步的實現只有jetcache的redis-luttece實現,其他的緩存實現(內存中的、Tair、Jedis等),所有的異步接口都會同步堵塞,這樣API仍然是兼容的。
以下的例子假設使用redis-luttece訪問cache,例如:
CacheGetResult<UserDO> r = cache.GET(userId);
這一行代碼執行完以後,緩存操作可能還沒有完成,如果此時調用r.isSuccess()或者r.getValue()或者r.getMessage()將會堵塞直到緩存操作完成。如果不想被堵塞,並且需要在緩存操作完成以後執行後續操作,可以這樣做:
CompletionStage<ResultData> future = r.future();
future.thenRun(() -> {
if(r.isSuccess()){
System.out.println(r.getValue());
}
});
以上代碼將會在緩存操作異步完成後,在完成異步操作的線程中調用thenRun中指定的回調。CompletionStage是Java8新增的功能,如果對此不太熟悉可以先查閱相關的文檔。需要注意的是,既然已經選擇了異步的開發方式,在回調中不能調用堵塞方法,以免堵塞其他的線程(回調方法很可能是在event loop線程中執行的)。
部分小寫的api不需要任何修改,就可以直接享受到異步開發的好處。比如put和removeAll方法,由於它們沒有返回值,所以此時就直接優化成異步調用,能夠減少RT;而get方法由於需要取返回值,所以仍然會堵塞。
自動load(read through)
LoadingCache類提供了自動load的功能,它是一個包裝,基於decorator模式,也實現了Cache接口。如果CacheBuilder指定了loader,那麼buildCache返回的Cache實例就是經過LoadingCache包裝過的。例如:
Cache<Long,UserDO> userCache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.loader(key -> loadUserFromDatabase(key))
.buildCache();
LoadingCache的get和getAll方法,在緩存未命中的情況下,會調用loader,如果loader拋出一場,get和getAll會拋出CacheInvokeException。
需要注意
- GET、GET_ALL這類大寫API只純粹訪問緩存,不會調用loader。
- 如果使用多級緩存,loader應該安裝在MultiLevelCache上,不要安裝在底下的緩存上。
註解的屬性只能是常量,所以沒有辦法在CreateCache註解中指定loader,不過我們可以這樣:
@CreateCache
private Cache<Long,UserDO> userCache;
@PostConstruct
public void init(){
userCache.config().setLoader(this::loadUserFromDatabase);
}
@CreateCache總是初始化一個經過LoadingCache包裝的Cache,直接在config中設置loader,可以實時生效。
自動刷新緩存
從JetCache2.2版本開始,RefreshCache基於decorator模式提供了自動刷新的緩存的能力,目的是爲了防止緩存失效時造成的雪崩效應打爆數據庫。同時設置了loader和refreshPolicy的時候,CacheBuilder的buildCache方法返回的Cache實例經過了RefreshCache的包裝。
RefreshPolicy policy = RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
.stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
Cache<String, Long> orderSumCache = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.loader(key -> loadOrderSumFromDatabase(key))
.refreshPolicy(policy)
.buildCache();
對一些key比較少,實時性要求不高,加載開銷非常大的緩存場景,適合使用自動刷新。上面的代碼指定每分鐘刷新一次,30分鐘如果沒有訪問就停止刷新。如果緩存是redis或者多級緩存最後一級是redis,緩存加載行爲是全局唯一的,也就是說不管有多少臺服務器,同時只有一個服務器在刷新,這是通過tryLock實現的,目的是爲了降低後端的加載負擔。
與LoadingCache一樣,使用@CreateCache時,我們需要這樣來添加自動刷新功能
@CreateCache
private Cache<String, Long> orderSumCache;
@PostConstruct
public void init(){
RefreshPolicy policy = RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
.stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
orderSumCache.config().setLoader(this::loadOrderSumFromDatabase);
orderSumCache.config().setRefreshPolicy(policy);
}
Redis支持(兩種redis客戶端二選一即可)
使用jedis客戶端連接redis
redis有多種java版本的客戶端,JetCache2.2以前使用jedis客戶端訪問redis。從JetCache2.2版本開始,增加了對luttece客戶端的支持,jetcache的luttece支持提供了異步操作和redis集羣支持。
如果選用jedis訪問redis,對應的maven artifact是jetcache-redis和jetcache-starter-redis(spring boot)。
spring boot環境下的jedis支持
application.yml文件如下(這裏省去了local相關的配置):
jetcache:
areaInCacheName: false
remote:
default:
type: redis
keyConvertor: fastjson
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
#sentinels: 127.0.0.1:26379 , 127.0.0.1:26380, 127.0.0.1:26381
#masterName: mymaster
@Bean(name = "defaultPool")
@DependsOn(RedisAutoConfiguration.AUTO_INIT_BEAN_NAME)//jetcache2.2+
//@DependsOn("redisAutoInit")//jetcache2.1
public JedisPoolFactory defaultPool() {
return new JedisPoolFactory("remote.default", JedisPool.class);
}
@Autowired
private Pool<Jedis> defaultPool;
也可以用Cache
接口上的<T> Tunwrap(Class<T> clazz)
方法來獲取JedisPool,參見RedisCache.unwrap源代碼。
不使用spring boot
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public Pool<Jedis> pool(){
// build jedis pool ...
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool<Jedis> pool){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisCacheBuilder remoteCacheBuilder = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
Builder API
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
JedisPool pool = new JedisPool(pc, "localhost", 6379);
Cache<Long,OrderDO> orderCache = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
常見問題
java.lang.NoSuchMethodError: redis.clients.jedis.JedisPool.<init>(Lorg/apache/commons/pool2/impl/GenericObjectPoolConfig;Ljava/lang/String;IILjava/lang/String;ILjava/lang/String;Z)V
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
使用lettuce客戶端連接redis
redis有多種java版本的客戶端,JetCache2.2以前使用jedis客戶端訪問redis。從JetCache2.2版本開始,增加了對lettuce客戶端的支持,JetCache的lettuce支持提供了異步操作和redis集羣支持。
注意:JetCache2.2版本中,lettuce單詞存在錯誤的拼寫,錯寫爲“luttece”,該錯誤存在於包名、類名和配置中,2.3已經改正。
spring boot環境下的lettuce支持
application.yml文件如下(這裏省去了local相關的配置):
jetcache:
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
uri: redis://127.0.0.1:6379/
jetcache:
areaInCacheName: false
remote:
default:
type: redis.lettuce
keyConvertor: fastjson
uri:
- redis://127.0.0.1:7000
- redis://127.0.0.1:7001
- redis://127.0.0.1:7002
@Bean(name = "defaultClient")
@DependsOn(RedisLettuceAutoConfiguration.AUTO_INIT_BEAN_NAME)
public LettuceFactory defaultClient() {
return new LettuceFactory("remote.default", RedisClient.class);
}
@Autowired
private RedisClient defaultClient;
不使用spring boot
@Configuration
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class JetCacheConfig {
@Bean
public RedisClient redisClient(){
RedisClient client = RedisClient.create("redis://127.0.0.1");
return client;
}
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider();
}
@Bean
public GlobalCacheConfig config(SpringConfigProvider configProvider,RedisClient redisClient){
Map localBuilders = new HashMap();
EmbeddedCacheBuilder localBuilder = LinkedHashMapCacheBuilder
.createLinkedHashMapCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE);
localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
Map remoteBuilders = new HashMap();
RedisLettuceCacheBuilder remoteCacheBuilder = RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.redisClient(redisClient);
remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
GlobalCacheConfig globalCacheConfig = new GlobalCacheConfig();
globalCacheConfig.setConfigProvider(configProvider);
globalCacheConfig.setLocalCacheBuilders(localBuilders);
globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
globalCacheConfig.setStatIntervalMinutes(15);
globalCacheConfig.setAreaInCacheName(false);
return globalCacheConfig;
}
}
builder API
RedisClient client = RedisClient.create("redis://127.0.0.1");
Cache<Long,OrderDO> orderCache = RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.redisClient(client)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
內存緩存LinkedHashMapCache和CaffeineCache
LinkedHashMapCache
LinkedHashMapCache是JetCache中實現的一個最簡單的Cache,使用LinkedHashMap做LRU方式淘汰。
Cache<Long, OrderDO> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
CaffeineCache
caffeine cache的介紹看這裏,它是guava cache的後續作品。
Cache<Long, OrderDO> cache = CaffeineCacheBuilder.createCaffeineCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
統計
當yml中的jetcache.statIntervalMinutes大於0時,通過@CreateCache和@Cached配置出來的Cache自帶監控。JetCache會按指定的時間定期通過logger輸出統計信息。默認輸出信息類似如下:
2017-01-12 19:00:00,001 INFO support.StatInfoLogger - jetcache stat from 2017-01-12 18:59:00,000 to 2017-01-12 19:00:00,000
cache | qps| rate| get| hit| fail| expire|avgLoadTime|maxLoadTime
-----------------------------------------------------+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------
default_AlicpAppChannelManager.getAlicpAppChannelById| 0.00| 0.00%| 0| 0| 0| 0| 0.0| 0
default_ChannelManager.getChannelByAccessToten | 30.02| 99.78%| 1,801| 1,797| 0| 4| 0.0| 0
default_ChannelManager.getChannelByAppChannelId | 8.30| 99.60%| 498| 496| 0| 1| 0.0| 0
default_ChannelManager.getChannelById | 6.65| 98.75%| 399| 394| 0| 4| 0.0| 0
default_ConfigManager.getChannelConfig | 1.97| 96.61%| 118| 114| 0| 4| 0.0| 0
default_ConfigManager.getGameConfig | 0.00| 0.00%| 0| 0| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfig | 43.98| 99.96%| 2,639| 2,638| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfigSettingMap | 2.45| 70.75%| 147| 104| 0| 43| 0.0| 0
default_GameManager.getGameById | 1.33|100.00%| 80| 80| 0| 0| 0.0| 0
default_GameManager.getGameUrlByUrlKey | 7.33|100.00%| 440| 440| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById | 30.98| 99.52%| 1,859| 1,850| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById_local | 30.98| 96.40%| 1,859| 1,792| 0| 67| 0.0| 0
default_InstanceManager.getInstanceById_remote | 1.12| 86.57%| 67| 58| 0| 6| 0.0| 0
default_IssueDao.getIssueById | 7.62| 81.40%| 457| 372| 0| 63| 0.0| 0
default_IssueDao.getRecentOnSaleIssues | 8.00| 85.21%| 480| 409| 0| 71| 0.0| 0
default_IssueDao.getRecentOpenAwardIssues | 2.52| 82.78%| 151| 125| 0| 26| 0.0| 0
default_PrizeManager.getPrizeMap | 0.82|100.00%| 49| 49| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics | 0.97|100.00%| 58| 58| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics_local | 0.97| 91.38%| 58| 53| 0| 5| 0.0| 0
default_TopicManager.getOnSaleTopics_remote | 0.08|100.00%| 5| 5| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId | 2.90| 98.85%| 174| 172| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId_local | 2.90| 96.55%| 174| 168| 0| 6| 0.0| 0
default_TopicManager.getTopicByTopicId_remote | 0.10| 66.67%| 6| 4| 0| 2| 0.0| 0
default_TopicManager.getTopicList | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
default_TopicManager.getTopicList_local | 0.02| 0.00%| 1| 0| 0| 1| 0.0| 0
default_TopicManager.getTopicList_remote | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
-----------------------------------------------------+----------+-------+--------------+--------------+--------------+--------------+-----------+-----------
只有使用computeIfAbsent方法或者@Cached註解纔會統計loadTime。用get方法取緩存,沒有命中的話自己去數據庫load,顯然是無法統計到的。
如果需要定製輸出,可以這樣做:
@Bean
public SpringConfigProvider springConfigProvider() {
return new SpringConfigProvider(){
public Consumer<StatInfo> statCallback() {
// return new StatInfoLogger(false);
... // 實現自己的logger
}
};
}
JetCache按statIntervalMinutes指定的週期,定期調用statCallback返回着這個Consumer,傳入的StatInfo是已經統計好的數據。這個方法默認的實現是:
returnnew StatInfoLogger(false);
StatInfoLogger的構造參數設置爲true會有更詳細的統計信息,包括put等操作的統計。StatInfoLogger輸出的是給人讀的信息,你也可以自定義logger將日誌輸出成特定格式,然後通過日誌系統統一收集和統計。
如果想要讓jetcache的日誌輸出到獨立的文件中,在使用logback的情況下可以這樣配置:
<appender name="JETCACHE_LOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>jetcache.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>jetcache.log.%d{yyyy-MM-dd}</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.alicp.jetcache" level="INFO" additivity="false">
<appender-ref ref="JETCACHE_LOGFILE" />
</logger>
Builder:未使用Spring4(或者spring)的時候,通過Builder手工構造Cache
JetCache2版本的@Cached和@CreateCache等註解都是基於Spring4.X版本實現的,在沒有Spring支持的情況下,註解將不能使用。但是可以直接使用JetCache的API來創建、管理、監控Cache,多級緩存也可以使用。
創建緩存
創建緩存的操作類似guava/caffeinecache,例如下面的代碼創建基於內存的LinkedHashMapCache:
Cache<String, Integer> cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(100)
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
創建RedisCache:
GenericObjectPoolConfig pc = new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
JedisPool pool = new JedisPool(pc, "localhost", 6379);
Cache<Long, OrderDO> orderCache = RedisCacheBuilder.createRedisCacheBuilder()
.keyConvertor(FastjsonKeyConvertor.INSTANCE)
.valueEncoder(JavaValueEncoder.INSTANCE)
.valueDecoder(JavaValueDecoder.INSTANCE)
.jedisPool(pool)
.keyPrefix("orderCache")
.expireAfterWrite(200, TimeUnit.SECONDS)
.buildCache();
多級緩存
在2.2以後通過下面的方式創建多級緩存:
Cache multiLevelCache = MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
.addCache(memoryCache, redisCache)
.expireAfterWrite(100, TimeUnit.SECONDS)
.buildCache();
實際上,使用MultiLevelCache可以創建多級緩存,它的構造函數接收的是一個Cache數組(可變參數)。
如果是2.2之前的版本:
Cache memoryCache = ...
Cache redisCache = ...
Cache multiLevelCache = new MultiLevelCache(memoryCache, redisCache);
監控統計
如果要對Cache進行監控統計:
Cache orderCache = ...
CacheMonitor orderCacheMonitor = new DefaultCacheMonitor("OrderCache");
orderCache.config().getMonitors().add(orderCacheMonitor); // jetcache 2.2+, or call builder.addMonitor() before buildCache()
// Cache<Long, Order> monitedOrderCache = new MonitoredCache(orderCache, orderCacheMonitor); //before jetcache 2.2
int resetTime = 1;
boolean verboseLog = false;
DefaultCacheMonitorManager cacheMonitorManager = new DefaultCacheMonitorManager(resetTime, TimeUnit.SECONDS, verboseLog);
cacheMonitorManager.add(orderCacheMonitor);
cacheMonitorManager.start();
首先創建一個CacheMonitor,每個DefaultCacheMonitor只能用於一個Cache。當DefaultCacheMonitorManager啓動以後,會使用slf4j按指定的時間定期輸出統計信息到日誌中(簡版輸出格式參見統計),DefaultCacheMonitor構造時指定的名字會作爲輸出時cache的名字。
在組裝多級緩存的過程中,可以給每個緩存安裝一個Monitor,這樣可以監控每一級的命中情況。
也可以自己對統計信息進行處理,調用下面的構造方法創建DefaultCacheMonitorManager:
public DefaultCacheMonitorManager(int resetTime, TimeUnit resetTimeUnit, Consumer<StatInfo> stat
開發者文檔
clone下來以後,可以按maven項目導入idea或eclipse。
跑通單元測試,需要在本地運行redis,先安裝docker,然後用下面的命令運行redis-sentinel
docker run --rm -it -p 6379-6381:6379-6381 -p 26379-26381:26379-26381 areyouok/redis-sentinel
接下來mvn cleantest可以跑通所有測試,如果在IDE裏面,可能還需要給javac設置-parameters參數。需要注意的是機器繁忙時單元測試有可能會失敗,因爲很多單元測試使用了sleep,爲了不讓單元測試運行的時間過長,sleep的時間都設置的比較短,這樣機器卡頓時可能導致檢查失敗,不過對於正常機器這並不經常發生。
使用snapshot版本,在自己的pom裏面加上:
<repositories>
<repository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
升級和兼容性指南
2.5.0
- 從2.3.3及更低版本升級到2.5.0會發生ClassCastException(如果你使用了MultiLevelCache或者cacheType.CacheType.BOTH)。 解決辦法是先升級到2.4.4並且發佈到生產環境,然後再升級到2.5.0。
- 子類的註解會覆蓋接口和父類。