實作思路:
1) 采用caffeine作為一級緩存,caffeine是一個高性能的Java緩存庫,采用的是Window TinyLfu回收政策,
提供了一個近乎最佳的緩存命中率;優點是資料就在應用記憶體中是以速度塊;缺點是1)受應用記憶體限制,容量有限,
2)沒有持久化,重新開機服務後緩存資料會丢失 3)分布式式環境下,緩存資料無法同步
2) 采用redis作為二級緩存,redis是一個高可用高性能的key-value資料庫,支援多種資料類型,支援叢集,
和應用伺服器分開部署易于橫向擴充;優點是 1) 支援多種資料類型,易于擴容 2)有持久化,重新開機應用伺服器緩存資料
不會丢失 3) redis是一個集中式緩存,不存在和應用伺服器之間資料同步的問題; 缺點是每次都需要通路redis,存在
IO浪費情況
總結:從上面的描述可以看出Caffeine和redis的優缺點正好相反,是以他們可以有效的互補。
實作步驟:
1) 基本架構
1.1 lombok 靈活開發
1.2 log4j 日志
1.3) fastJson 和 jackson
2) 定義緩存的頂級接口
package org.github.roger.cache;
import java.util.concurrent.Callable;
/**
* 緩存的頂級接口
*/
public interface ICache {
/**
* @return 傳回緩存的名稱
*/
String getName();
/**
* @return 傳回真實的緩存對象
*/
Object getRealCache();
/**
* 根據key擷取其對應的緩存對象,如果沒有就傳回null
* @param key
* @return 傳回緩存key對應的緩存對象
*/
Object get(Object key);
/**
* 根據key擷取其對應的緩存對象,并将傳回的緩存對象轉換成對應的類型
* 如果沒有就傳回null
* @param key
* @param type 緩存對象的類型
* @param <T>
* @return
*/
<T> T get(Object key,Class<T> type);
/**
* 根據key擷取其對應的緩存對象,并将傳回的緩存對象轉換成對應的類型
* 如果對應key不存在則調用valueLoader加載資料
* @param key
* @param valueLoader 加載緩存的回調方法
* @param <T>
* @return
*/
<T> T get(Object key, Callable<T> valueLoader);
/**
* 将對應key-value放到緩存,如果key原來有值就直接覆寫
*
* @param key 緩存key
* @param value 緩存的值
*/
void put(Object key, Object value);
/**
* 如果緩存key沒有對應的值就将值put到緩存,如果有就直接傳回原有的值
* 就相當于:
* Object existingValue = cache.get(key);
* if (existingValue == null) {
* cache.put(key, value);
* return null;
* } else {
* return existingValue;
* }
* @param key 緩存key
* @param value 緩存key對應的值
* @return 因為值本身可能為NULL,或者緩存key本來就沒有對應值的時候也為NULL,
* 是以如果傳回NULL就表示已經将key-value鍵值對放到了緩存中
*/
Object putIfAbsent(Object key, Object value);
/**
* 在緩存中删除對應的key
*
* @param key 緩存key
*/
void evict(Object key);
/**
* 清楚緩存
*/
void clear();
}
3.定義一個緩存頂級接口的抽象實作類,用于實作不同級别的緩存的公共方法
package org.github.roger.cache;
import com.alibaba.fastjson.JSON;
import org.github.roger.support.NullValue;
import org.springframework.util.Assert;
import java.util.concurrent.Callable;
public abstract class AbstractValueAdaptingCache implements ICache{
//緩存名稱
private String name;
public AbstractValueAdaptingCache(String name) {
Assert.notNull(name,"緩存名稱不能為空");
this.name = name;
}
/**
* 緩存對象是否允許為空
* @return true 允許 false 不允許
*/
public abstract boolean isAllowNullValues();
public String getName() {
return this.name;
}
public <T> T get(Object key, Class<T> type) {
return (T) fromStoreValue(get(key));
}
protected Object fromStoreValue(Object storeValue) {
if(isAllowNullValues() && storeValue instanceof NullValue){
return null;
}
return storeValue;
}
protected Object toStoreValue(Object userValue){
if(isAllowNullValues() && userValue == null){
return NullValue.INSTANCE;
}
return userValue;
}
/**
* {@link #get(Object, Callable)} 方法加載緩存值的包裝異常
*/
public class LoaderCacheValueException extends RuntimeException {
private final Object key;
public LoaderCacheValueException(Object key, Throwable ex) {
super(String.format("加載key為 %s 的緩存資料,執行被緩存方法異常", JSON.toJSONString(key)), ex);
this.key = key;
}
public Object getKey() {
return this.key;
}
}
}
4.建立一個多級模式緩存的類,繼承上面的抽象類
4.1) 對每個級别的緩存建立配置項類,以及多級配置項類
package org.github.roger.settings;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.github.roger.enumeration.ExpireMode;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class FirstCacheSetting implements Serializable {
/**
* 緩存初始Size
*/
private int initialCapacity = 10;
/**
* 緩存最大Size
*/
private int maximumSize = 500;
/**
* 緩存有效時間
*/
private int expireTime = 0;
/**
* 緩存時間機關
*/
private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
/**
* 緩存失效模式{@link ExpireMode}
*/
private ExpireMode expireMode = ExpireMode.WRITE;
public boolean isAllowNullValues() {
return false;
}
}
package org.github.roger.settings;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.github.roger.enumeration.ExpireMode;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SecondaryCacheSetting implements Serializable {
/**
* 緩存有效時間
*/
private long expiration = 0;
/**
* 緩存主動在失效前強制重新整理緩存的時間
*/
private long preloadTime = 0;
/**
* 時間機關 {@link TimeUnit}
*/
private TimeUnit timeUnit = TimeUnit.MICROSECONDS;
/**
* 是否強制重新整理(走資料庫),預設是false
*/
private boolean forceRefresh = false;
/**
* 是否使用緩存名稱作為 redis key 字首
*/
private boolean usePrefix = true;
/**
* 是否允許存NULL值
*/
boolean allowNullValue = false;
/**
* 非空值和null值之間的時間倍率,預設是1。allowNullValue=true才有效
*
* 如配置緩存的有效時間是200秒,倍率這設定成10,
* 那麼當緩存value為null時,緩存的有效時間将是20秒,非空時為200秒
*/
int magnification = 1;
}
package org.github.roger.settings;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
public class MultiLayeringCacheSetting implements Serializable {
private static final String SPLIT = "-";
/**
* 内部緩存名,由[一級緩存有效時間-二級緩存有效時間-二級緩存自動重新整理時間]組成
*/
private String internalKey;
boolean useFirstCache = true;
private FirstCacheSetting firstCacheSetting;
private SecondaryCacheSetting secondaryCacheSetting;
public MultiLayeringCacheSetting(FirstCacheSetting firstCacheSetting, SecondaryCacheSetting secondaryCacheSetting) {
this.firstCacheSetting = firstCacheSetting;
this.secondaryCacheSetting = secondaryCacheSetting;
internalKey();
}
@JSONField(serialize = false, deserialize = false)
private void internalKey() {
// 一級緩存有效時間-二級緩存有效時間-二級緩存自動重新整理時間
StringBuilder sb = new StringBuilder();
if (firstCacheSetting != null) {
sb.append(firstCacheSetting.getTimeUnit().toMillis(firstCacheSetting.getExpireTime()));
}
sb.append(SPLIT);
if (secondaryCacheSetting != null) {
sb.append(secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getExpiration()));
sb.append(SPLIT);
sb.append(secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getPreloadTime()));
}
internalKey = sb.toString();
}
}
4.2) 利用配置項類和步驟3建立的緩存頂級接口的抽象實作類 建立的建立多級模式緩存的類
package org.github.roger;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.github.roger.cache.AbstractValueAdaptingCache;
import org.github.roger.cache.ICache;
import org.github.roger.settings.MultiLayeringCacheSetting;
import java.util.concurrent.Callable;
@Slf4j
public class MultiLayeringCache extends AbstractValueAdaptingCache {
private AbstractValueAdaptingCache firstCache;
private AbstractValueAdaptingCache secondCache;
private boolean useFirstCache = true;
private MultiLayeringCacheSetting multilayeringCacheSetting;
public MultiLayeringCache(String name, AbstractValueAdaptingCache firstCache, AbstractValueAdaptingCache secondCache, boolean useFirstCache,MultiLayeringCacheSetting multilayeringCacheSetting) {
super(name);
this.firstCache = firstCache;
this.secondCache = secondCache;
this.useFirstCache = useFirstCache;
this.multilayeringCacheSetting = multilayeringCacheSetting;
}
public Object getRealCache() {
return this;
}
public Object get(Object key) {
Object storeValue = null;
if(useFirstCache){
storeValue = firstCache.get(key);
log.info("查詢一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(storeValue));
}
if(storeValue == null){
storeValue = secondCache.get(key);
firstCache.putIfAbsent(key, storeValue);
log.info("查詢二級緩存,并将資料放到一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(storeValue));
}
return fromStoreValue(storeValue);
}
@Override
public <T> T get(Object key, Class<T> type) {
if (useFirstCache) {
Object result = firstCache.get(key, type);
log.info("查詢一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(result));
if (result != null) {
return (T) fromStoreValue(result);
}
}
T result = secondCache.get(key, type);
firstCache.putIfAbsent(key, result);
log.info("查詢二級緩存,并将資料放到一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(result));
return result;
}
public <T> T get(Object key, Callable<T> valueLoader) {
if (useFirstCache) {
Object result = firstCache.get(key);
log.info("查詢一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(result));
if (result != null) {
return (T) fromStoreValue(result);
}
}
T result = secondCache.get(key, valueLoader);
firstCache.putIfAbsent(key, result);
log.info("查詢二級緩存,并将資料放到一級緩存。 key={},傳回值是:{}", key, JSON.toJSONString(result));
return result;
}
public void put(Object key, Object value) {
secondCache.put(key, value);
// 删除一級緩存
if (useFirstCache) {
//TODO deleteFirstCache(key);
}
}
public Object putIfAbsent(Object key, Object value) {
Object result = secondCache.putIfAbsent(key, value);
// 删除一級緩存
if (useFirstCache) {
//TODO deleteFirstCache(key);
}
return result;
}
public void evict(Object key) {
// 删除的時候要先删除二級緩存再删除一級緩存,否則有并發問題
secondCache.evict(key);
// 删除一級緩存
if (useFirstCache) {
//TODO deleteFirstCache(key);
}
}
public void clear() {
// 删除的時候要先删除二級緩存再删除一級緩存,否則有并發問題
secondCache.clear();
if (useFirstCache) {
//TODO
/*// 清除一級緩存需要用到redis的訂閱/釋出模式,否則叢集中其他服伺服器節點的一級緩存資料無法删除
RedisPubSubMessage message = new RedisPubSubMessage();
message.setCacheName(getName());
message.setMessageType(RedisPubSubMessageType.CLEAR);
// 釋出消息
RedisPublisher.publisher(redisTemplate, new ChannelTopic(getName()), message);*/
}
}
public ICache getFirstCache() {
return firstCache;
}
public ICache getSecondCache() {
return secondCache;
}
public boolean isAllowNullValues() {
return secondCache.isAllowNullValues();
}
}