實現簡單的JAVA多級快取(Caffeine + redis)
阿新 • • 發佈:2018-11-09
需求
好久沒寫文章啦,之前寫的文章到現在也沒有收尾,沒辦法,時間不多啊,舊坑沒有填完就開始開新坑,最近專案組長說實現一個多級快取,通常我們喜歡把cache放到redis裡,可以把訪問速度提升,但是redis也算是遠端伺服器,會有IO時間的開銷,如果我們把快取放在本地記憶體,效能能進一步提升,這也就帶出了二級快取概念。有人說為什麼不把cache直接放到本地,如果是單機沒問題,但是叢集環境下還是需要兩級快取的配合。
快取的獲取與更新
隨便畫的,簡單來說,工具先從 一級快取取起,也就是本地快取,如果快取命中,就可以直接返回;如果一級快取沒有,就會去redis找,再不行就走傳統業務邏輯。這種快取比單一的快取工具比起來具有以下特點:
- 適應叢集環境
- 比單一Redis快取效能更高
- 設計了三級資料層(包括業務直接取資料)分攤了請求量,降低資料庫壓力
但跟所有快取框架一樣,快取只適合非關鍵資料,因為快取更新多少具有延遲性。
快取的更新比獲取更復雜一點,它存在多種情況:
- 當一級快取失效時(獲取不到),得益於Caffeine本身提供的功能,你能指定方法去redis獲取並更新到一級快取中。
- 當業務資料發生改變,呼叫delete方法直接清除一/二級快取。(這種方法現在比較暴力,後期可以完善)
- 當新建快取時,先Redis 存入快取,再通過Redis 的訊息訂閱機制 讓本地每臺機器接收最新的cache。
程式碼實現
環境的話比較通用的 spring + redission + Caffeine
- redission 要先在spring配置一下,裡面的配置檔案根據實際情況自己生成填入:
@Bean RedissonClient redissonClient() { Config config = new Config(); SingleServerConfig serverConfig = config.useSingleServer() .setAddress("redis://" + redissonProperties.getHost() + ":" + redissonProperties.getPort()) .setTimeout(redissonProperties.getTimeout()) .setConnectionPoolSize(redissonProperties.getConnectionPoolSize()) .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize()); if (StringUtils.isNotBlank(redissonProperties.getPassword())) { serverConfig.setPassword(redissonProperties.getPassword()); } return Redisson.create(config); }
- 快取工具類
@Component
@Slf4j
public class SecondLevelCacheUtil {
//為避免key衝突,key的設定應該規範
public static final String BRAINTRAIN = "braintrain:";
public static final String REDIS_TOPIC_PUT = BRAINTRAIN + "putTopic:";
public static final String REDIS_TOPIC_DELETE = BRAINTRAIN + "deleteTopic:";
@Autowired
CustomsProperties customsProperties;
@Autowired
RedissonClient redissonClient;
private Cache<String, String> cache;
@PostConstruct
void init() {
log.info("SecondLevelCacheUtil init");
cache = Caffeine.newBuilder()
.expireAfterWrite(customsProperties.getRedisFirstCacheTime(), TimeUnit.MILLISECONDS)
.build();
// 監聽刪除快取事件,及時清除本地一級快取
RTopic<String> deleteTopic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
deleteTopic.addListener((channel, message) -> {
log.info("first cache delete {}", message);
cache.invalidate(message);
});
// 監聽新增快取事件,及時新增本地一級快取
RTopic<String> putTopic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
putTopic.addListener((channel, message) -> {
if (StringUtils.isNotBlank(message)) {
log.info("first cache put {}", message);
String[] split = message.split("\\|\\|");
cache.put(split[0], split[1]);
}
});
log.info("SecondLevelCacheUtil done");
}
public <T> T get(String key, Class<T> clazz) {
try {
if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
return null;
}
//一級快取取不到時,呼叫getByRedis()取二級快取,由Caffeine原生提供機制
String json = cache.get(key, k -> getByRedis(k));
if (StringUtils.isNotBlank(json)) {
return JSON.parseObject(json, clazz);
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil get e={}", e);
}
return null;
}
public void delete(String key) {
try {
if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
return;
}
RBucket<Object> bucket = redissonClient.getBucket(key);
bucket.deleteAsync();
// 分發"刪除"主題,讓本地一級快取接收通知
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
long clientsReceivedMessage = topic.publish(key);
log.info("delete first/second cache ,key{}, {}個例項接收到資訊", key, clientsReceivedMessage);
} catch (Exception e) {
log.warn("SecondLevelCacheUtil delete e={}", e);
}
}
public void set(String key, Object value) {
try {
if (StringUtils.isNotBlank(key) && !Objects.isNull(value)) {
if (!key.startsWith(BRAINTRAIN)) {
return;
}
RBucket<String> bucket = redissonClient.getBucket(key);
String valueStr = JSONObject.toJSONString(value);
bucket.setAsync(valueStr, customsProperties.getRedisSecondCacheTime(), TimeUnit.MILLISECONDS);
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
// 分發"新增"主題,讓本地一級快取接收通知
long clientsReceivedMessage = topic.publish(key + "||" + valueStr);
log.info("after set , key: {} value: {}, {}個例項接收到資訊", key, valueStr, clientsReceivedMessage);
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil set e={}", e);
}
}
public void setIfAbsent(String key, Object value, Class clazz) {
if (null == get(key, clazz)) {
set(key, value);
}
}
//取二級快取的方法
private String getByRedis(String key) {
try {
log.info("快取不存在或過期,呼叫了redis獲取快取key的值");
if (StringUtils.isNotBlank(key)) {
RBucket<String> bucket = redissonClient.getBucket(key);
String result = bucket.get();
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
long clientsReceivedMessage = topic.publish(key + "||" + result);
log.info("first cache null, key: {} value: {}, {}個例項接收到資訊", key, result, clientsReceivedMessage);
return result;
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil getByRedis e={}", e);
}
return null;
}
}
3.部分設定項
@Component
@Data
public class CustomsProperties {
//一級快取失效時間
@Value("${braintrain.redisFirstCacheTime: 180000}")
Long redisFirstCacheTime;
//二級快取失效時間
@Value("${braintrain.redisSecondCacheTime: 2592000000}")
Long redisSecondCacheTime;
}
不足與改進
- 因為剛開始寫,這個工具類能滿足基本的二級快取需求,但其實改進的地方還有很多,比如根據實際情況利用Caffeine本身的淘汰策略進行cache更新與刪除,而不是直接設定失效時間,但這種改進要考慮一/二級快取的一致性,以免快取出現問題
- 應用重啟後一級快取處於全部失效狀態,如果全部從redis取會有讀取壓力;現有一級快取也是被動接收新cache,一級快取的命中率較低,這裡可以考慮redis的空間訊息通知。