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類似。
文檔目錄
-
- 使用 jedis用戶端連接配接redis
- lettuce用戶端連接配接redis
- 記憶體緩存
和LinkedHashMapCache
CaffeineCache
- 統計
- Builder :未使用Spring4(或未使用Spring)的時候,或通過Builder手工構造
Cache
- 開發者文檔
- 更新和相容性指南
依賴哪個jar?
- jetcache-anno-api:定義jetcache的注解和常量,不傳遞依賴。如果你想把Cached注解加到接口上,又不希望你的接口jar傳遞太多依賴,可以讓接口jar依賴jetcache-anno-api。
- jetcache-core:核心api,完全通過程式設計來配置操作
,不依賴Spring。兩個記憶體中的緩存實作Cache
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的屬性非常類似,但是多幾個:
@CacheInvalidate注解說明:
如果在配置中配置了多個緩存area,在這裡指定使用哪個area,指向對應的@Cached定義。 | ||
指定緩存的唯一名稱,指向對應的@Cached定義。 | ||
指定key | ||
指定條件,如果表達式傳回true才執行删除 |
@CacheUpdate注解說明:
使用@CacheUpdate和@CacheInvalidate的時候,相關的緩存操作可能會失敗(比如網絡IO錯誤),是以指定緩存的逾時時間是非常重要的。
@CacheRefresh注解說明:
refresh | 重新整理間隔 | |
時間機關 | ||
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表示不統計 | |
jetcache.areaInCacheName | 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 | ||
jetcache.[local|remote].${area}.limit | 100 | 每個緩存執行個體的最大元素的全局配置,僅local類型的緩存需要指定。注意是每個緩存執行個體的限制,而不是全部,比如這裡指定100,然後用@CreateCache建立了兩個緩存執行個體(并且注解上沒有設定localLimit屬性),那麼每個緩存執行個體的限制都是100 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis | 無窮大 | 以毫秒為機關指定逾時時間的全局配置(以前為defaultExpireInMillis) |
jetcache.local.${area}.expireAfterAccessInMillis | 需要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);
}
使用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
如果不通過@CreateCache和@Cached注解,可以通過下面的方式建立RedisCache。通過注解建立的緩存會自動設定keyPrefix,這裡是手工建立緩存,對于遠端緩存需要設定keyPrefix屬性,以免不同Cache執行個體的key發生沖突。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
請確定jedis的版本在2.9.0以上,spring boot 1.5以下版本的spring-boot-dependencies會引入較低版本的jedis,可以在自己的pom中強制直接依賴jedis版本2.9.0: <dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
使用lettuce用戶端連接配接redis
java版本的用戶端,JetCache2.2以前使用jedis用戶端通路redis。從JetCache2.2版本開始,增加了對lettuce用戶端的支援,JetCache的lettuce支援提供了異步操作和redis叢集支援。
使用lettuce通路redis,對應的maven artifact是jetcache-redis-lettuce和jetcache-starter-redis-lettuce。lettuce使用Netty建立單個連接配接連redis,是以不需要配置連接配接池。 注意:新釋出的lettuce5更換了groupId和包名,2.3版本的JetCache同時支援lettuce4和5,jetcache-redis-lettuce,jetcache-starter-redis-lettuce提供lettuce5支援,jetcache-redis-lettuce4和jetcache-starter-redis-lettuce4提供lettuce4支援。 注意:JetCache2.2版本中,lettuce單詞存在錯誤的拼寫,錯寫為“luttece”,該錯誤存在于包名、類名和配置中,2.3已經改正。spring boot環境下的lettuce支援
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;
Cache
<T> Tunwrap(Class<T> clazz)
方法來擷取 RedisClient
RedisCommands
等。參考RedisLettuceCache.unwrap源代碼。 @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
如果不通過@CreateCache和@Cached注解,可以通過下面的方式建立Cache。通過注解建立的緩存會自動設定keyPrefix,這裡是手工建立緩存,對于遠端緩存需要設定keyPrefix屬性,以免不同Cache執行個體的key發生沖突。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
本地緩存目前有兩個實作。如果自己用jetcache-core的Cache API,可以不指定keyConvertor,此時本地緩存使用equals方法來比較key。如果使用jetcache-anno中的@Cached、@CreateCache等注解,必須指定keyConvertor。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。
- 子類的注解會覆寫接口和父類。