一個(gè)簡(jiǎn)單實(shí)用的函數(shù)式緩存工具類:封裝了基本的緩存增刪查操作,提供了熱點(diǎn)數(shù)據(jù)集中失效和緩存穿透的統(tǒng)一解決方案,以及在此基礎(chǔ)上的開(kāi)發(fā)模型。
背景介紹
日常開(kāi)發(fā)中緩存使用越來(lái)越普遍而問(wèn)題也接憧而至,首先是開(kāi)發(fā)方式的繁瑣:查詢緩存->有就直接返回,沒(méi)有就查詢數(shù)據(jù)庫(kù)或者調(diào)用其他服務(wù)獲取,再保存至緩存中...如:
/**
* 獲取渠道下,字段值
* <li></li>
* @param channelNo: 渠道
* @param field: 字段
* @return: java.lang.String 值
*/
private String getChannelConfigFieldValue(String channelNo,String field){
....
//從緩存獲取
String key = StringUtils.join(CHANNEL_CONFIG_KEY_PREFIX,channelNo);
String result = RedisUtils.get(key);
JSONObject jsonObject;
if(StringUtils.isNotBlank(result)){
jsonObject = JSON.parseObject(result);
}else{
//加載并緩存數(shù)據(jù)
jsonObject = loadAndCacheChannelConfigInfo(channelNo);
}
....
}
/**
* 加載并緩存渠道配置信息
* <li></li>
* @param channelNo: 渠道號(hào)
* @return: com.alibaba.fastjson.JSONObject
*/
private JSONObject loadAndCacheChannelConfigInfo(String channelNo){
.....
//發(fā)起遠(yuǎn)程調(diào)用/訪問(wèn)數(shù)據(jù)庫(kù) 加載數(shù)據(jù)
Map<String, Object> queryParam = new HashMap<>();
...
log.info("加載并緩存渠道配置信息 param => {}", JSON.toJSONString(queryParam));
String result = HttpClientUtils.sendHttpPostJson(getChannelInfoUrl, JSON.toJSONString(queryParam));
...
JSONObject jsonObject = JSON.parseObject(result);
...
//設(shè)置緩存
String key = StringUtils.join(CHANNEL_CONFIG_KEY_PREFIX,channelNo);
RedisUtils.set(key, result,CACHE_EXPIRE_TIME);
log.info("加載并緩存渠道配置信息成功 key=> {}", key);
return jsonObject;
...
}
此類模板式的代碼出現(xiàn)在使用緩存的任何地方,且大多是CV大法而來(lái),既不美觀也不優(yōu)雅,還極易出錯(cuò)。其次,在使用緩存的過(guò)程中由于使用方法或者對(duì)問(wèn)題的處理不當(dāng),可能給數(shù)據(jù)庫(kù)或者依賴的服務(wù)造成嚴(yán)重的影響,常見(jiàn)的問(wèn)題如:熱點(diǎn)數(shù)據(jù)集中失效,緩存穿透等。所以需要將緩存開(kāi)發(fā)使用過(guò)程中的共性抽離出來(lái),抽象并封裝使之模板化,規(guī)范化,并最終形成一個(gè)可擴(kuò)展,易維護(hù),使用方便的工具或者工具集。
設(shè)計(jì)思路
-
統(tǒng)一緩存開(kāi)發(fā)使用模式,提供/約定一套開(kāi)發(fā)方法/模型。
動(dòng)靜分離:抽象公共部分,函數(shù)化變化部分。
-
封裝常見(jiàn)問(wèn)題處理流程默認(rèn)實(shí)現(xiàn),并提供擴(kuò)展機(jī)制。
SPI機(jī)制:內(nèi)部默認(rèn)實(shí)現(xiàn),外部可擴(kuò)展。
-
資源使用細(xì)粒度保護(hù),減少資源競(jìng)爭(zhēng)。
分布式鎖:redisson分布式鎖
知識(shí)準(zhǔn)備
一,函數(shù)式編程:
-
什么是函數(shù)式編程
函數(shù)式編程并不是Java新提出的概念,屬于編程范式中的一種,它起源于一個(gè)數(shù)學(xué)問(wèn)題,我們并不需要過(guò)多的了解函數(shù)式編程的歷史,要追究它的歷史以及函數(shù)式編程,關(guān)于范疇論、柯里化早就讓人立馬放棄學(xué)習(xí)函數(shù)式編程了,對(duì)于函數(shù)式編程我們所要知道的是,它能將一個(gè)行為傳遞作為參數(shù)進(jìn)行傳遞。
-
函數(shù)式編程的目的
Java8出現(xiàn)之前,我們關(guān)注的往往是某一類對(duì)象應(yīng)該具有什么樣的屬性,當(dāng)然這也是面向?qū)ο蟮暮诵?-對(duì)數(shù)據(jù)進(jìn)行抽象。但是java8出現(xiàn)以后,這一點(diǎn)開(kāi)始出現(xiàn)變化,似乎在某種場(chǎng)景下,更加關(guān)注某一類共有的行為(這似乎與之前的接口有些類似),這也就是java8提出函數(shù)式編程的目的。

-
Lambda表達(dá)式
不得不提增加Lambda的目的,其實(shí)就是為了支持函數(shù)式編程,而為了支持Lambda表達(dá)式,才有了函數(shù)式接口。另外,為了在面對(duì)大型數(shù)據(jù)集合時(shí),為了能夠更加高效的開(kāi)發(fā),編寫(xiě)的代碼更加易于維護(hù),更加容易運(yùn)行在多核CPU上,java在語(yǔ)言層面增加了Lambda表達(dá)式。
-
函數(shù)式接口
在Java中有一個(gè)接口中只有一個(gè)方法表示某特定方法并反復(fù)使用,例如
Runnable接口中只有run方法就表示執(zhí)行的線程任務(wù)。Java8中對(duì)于這樣的接口有了一個(gè)特定的名稱——函數(shù)式接口。Java8中即使是支持函數(shù)式編程,也并沒(méi)有再標(biāo)新立異另外一種語(yǔ)法表達(dá)。所以只要是只有一個(gè)方法的接口,都可以改寫(xiě)成Lambda表達(dá)式。在Java8中新增了java.util.function用來(lái)支持Java的函數(shù)式編程,其中的接口均是只包含一個(gè)方法,與其他接口的區(qū)別:- 函數(shù)式接口中只能有一個(gè)抽象方法(我們?cè)谶@里不包括與Object的方法重名的方法);
- 可以有從Object繼承過(guò)來(lái)的抽象方法,因?yàn)樗蓄惖淖罱K父類都是Object;
- 接口中唯一抽象方法的命名并不重要,因?yàn)楹瘮?shù)式接口就是對(duì)某一行為進(jìn)行抽象,主要目的就是支持Lambda表達(dá)式。
Java8還提供了@FunctionalInterface注解來(lái)幫助我們標(biāo)識(shí)函數(shù)式接口。另外需要注意的是函數(shù)式接口的目的是對(duì)某一個(gè)行為進(jìn)行封裝,某些接口可能只是巧合符合函數(shù)式接口的定義。java8的Function包下的類是為了支持基本類型而添加的接口,部分類圖如下:

-
進(jìn)一步學(xué)習(xí)
推薦大家閱讀《java8 in action》 以及《Java函數(shù)式編程》,前者是對(duì)java8新特性全面的闡述,深入淺出,注重實(shí)戰(zhàn),非常適合入門(mén)。后者則注重函數(shù)式編程背后的故事,教你如何構(gòu)建函數(shù)式數(shù)據(jù)結(jié)構(gòu),以及函數(shù)式編程范式如何幫助你編寫(xiě)更好的程序。
二,SPI機(jī)制
-
java SPI 機(jī)制
SPI(Service Provider Interface),是JDK內(nèi)置的一種 服務(wù)提供發(fā)現(xiàn)機(jī)制,可以用來(lái)啟用框架擴(kuò)展和替換組件,主要是被框架的開(kāi)發(fā)人員使用,比如java.sql.Driver接口,其他不同廠商可以針對(duì)同一接口做出不同的實(shí)現(xiàn),MySQL和PostgreSQL都有不同的實(shí)現(xiàn)提供給用戶,而Java的SPI機(jī)制可以為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)。Java中SPI機(jī)制主要思想是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要,其核心思想就是 解耦。

-
SPI機(jī)制的約定:
- 在META-INF/services/目錄中創(chuàng)建以接口全限定名命名的文件該文件內(nèi)容為Api具體實(shí)現(xiàn)類的全限定名
- 使用ServiceLoader類動(dòng)態(tài)加載META-INF中的實(shí)現(xiàn)類
- 如SPI的實(shí)現(xiàn)類為Jar則需要放在主程序classPath中
- Api具體實(shí)現(xiàn)類必須有一個(gè)不帶參數(shù)的構(gòu)造方法
-
不足
- 不能按需加載,需要遍歷所有的實(shí)現(xiàn),并實(shí)例化,然后在循環(huán)中才能找到我們需要的實(shí)現(xiàn)。如果不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
- 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過(guò) Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
- 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
-
改造
基于 xkernel
三,分布式鎖
這里用到分布式鎖 starter,詳見(jiàn):分布式鎖 starter
實(shí)施步驟
一,緩存模塊設(shè)計(jì)
整個(gè)模塊圍繞2個(gè)接口和1個(gè)工具類展開(kāi):緩存管理接口,緩存函數(shù)接口,緩存整合工具類。
緩存管理接口主要定義常用緩存基本操作接口,如設(shè)置,獲取,刪除緩存等,默認(rèn)實(shí)現(xiàn)基于Redis。
緩存函數(shù)接口則是基于Suppliper和Function2個(gè)函數(shù)式接口,分別定義了獲取單個(gè)對(duì)象和集合對(duì)象的4個(gè)常用接口,后續(xù)可根據(jù)實(shí)際需要擴(kuò)展,默認(rèn)實(shí)現(xiàn)基于緩存管理接口+分布式鎖 starter 。
緩存整合工具類則組合了2個(gè)接口,對(duì)外統(tǒng)一提供緩存相關(guān)操作方法。
-
緩存管理接口和緩存函數(shù)接口是模塊的擴(kuò)展點(diǎn),基于xkernel 提供的SPI機(jī)制,結(jié)合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty實(shí)現(xiàn)。
總體類結(jié)構(gòu)圖如下所示:

包結(jié)構(gòu)如下:
xService
└── src
├── main
│ ├── java
│ │ └── com.javacoo
│ │ ├────── xservice
│ │ │ ├──────cache
│ │ │ │ ├── CacheFunction 緩存函數(shù)接口
│ │ │ │ ├── CacheManager 緩存管理接口
│ │ │ │ ├── CacheFactory 緩存工廠
│ │ │ │ ├── CacheHolder 緩存對(duì)象持有者
│ │ │ │ ├── config
│ │ │ │ │ └── CacheConfig 緩存配置
│ │ │ │ └── internal 接口內(nèi)部實(shí)現(xiàn)
│ │ │ │ ├── redis
│ │ │ │ └── ├── CombinatorialFunction 函數(shù)組合對(duì)象
│ │ │ │ ├── RedisCacheFunction 緩存函數(shù)接口實(shí)現(xiàn)類
│ │ │ │ └── RedisCacheManager 緩存管理接口實(shí)現(xiàn)類
│ │ │ └──────utils
│ │ │ └── CacheUtil 緩存工具類
│ └── resource
│ ├── META-INF
│ └── ext
│ └── internal
│ ├── com.javacoo.xservice.base.support.cache.CacheFunction
│ └── com.javacoo.xservice.base.support.cache.CacheManager
└── test 測(cè)試
二,核心邏輯概述
模塊核心是圍繞數(shù)據(jù)加載方案及熱點(diǎn)數(shù)據(jù)集中失效和緩存穿透問(wèn)題解決方案展開(kāi):數(shù)據(jù)加載方案主要是采取多重檢查機(jī)制,確保分布式,高并發(fā)條件下數(shù)據(jù)不多加載,不漏加載。熱點(diǎn)數(shù)據(jù)集中失效和緩存穿透問(wèn)題解決方案主要采用如下策略:
- 熱點(diǎn)數(shù)據(jù)集中失效解決方案:redisson分布式鎖+隨機(jī)過(guò)期時(shí)間
- 緩存穿透的解決方案:設(shè)置空數(shù)據(jù)特定值(根據(jù)業(yè)務(wù)場(chǎng)景特性:空數(shù)據(jù)的key數(shù)量有限、key重復(fù)請(qǐng)求概率較高),缺點(diǎn):需要存儲(chǔ)所有空數(shù)據(jù)的key,對(duì)于一些惡意攻擊,KEY不相同的情況,也起不了保護(hù)數(shù)據(jù)庫(kù)的作用。
- 緩存穿透的解決備選方案:空數(shù)據(jù)的key各不相同、key重復(fù)請(qǐng)求概率低的場(chǎng)景而言,可使用BloomFilter。
加載數(shù)據(jù)并緩存流程圖:

三,如何擴(kuò)展
基于xkernel 提供的SPI機(jī)制,擴(kuò)展非常方便,大致步驟如下:
- 實(shí)現(xiàn)緩存函數(shù)接口:如 com.xxxx.xxxx.MyCacheFunction
- 配置緩存函數(shù)接口:
- 在項(xiàng)目resource目錄新建包->META-INF->services
- 創(chuàng)建com.javacoo.xservice.base.support.cache.CacheFunction文件,文件內(nèi)容:實(shí)現(xiàn)類的全局限定名,如:
myCacheFunction=com.xxxx.xxxx.MyCacheFunction
- 修改配置文件,添加如下內(nèi)容:
#緩存函數(shù)接口實(shí)現(xiàn)
app.config.cache.functionImpl = myCacheFunction
四,如何使用
由于過(guò)于簡(jiǎn)單,直接上代碼,如下所示:
/**
* 查詢緩存KEY:id
*/
private static final String QUERY_CACHE_KEY = "query:%1$s";
/**
* 查詢緩存超時(shí)時(shí)間:1 分鐘
*/
private static final int QUERY_CACHE_TIMEOUT = 60 * 1;
/**
* 緩存工具類
*/
@Autowired
protected CacheUtil cacheUtil;
@Autowired
private ExampleDao exampleDao;
@Override
public Optional<ExampleDto> getExampleInfo(String id) {
AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
// 從緩存中獲取數(shù)據(jù)
String cacheKey = String.format(QUERY_CACHE_KEY,id);
return Optional.ofNullable(cacheUtil.getCacheValueFunction(cacheKey,QUERY_CACHE_TIMEOUT,ExampleDto.class, getExampleInfoFunction,id));
}
/**
* 獲取示例信息函數(shù)
* <li></li>
* @author duanyong@jccfc.com
* @param id: id
* @return: ExampleDto 示例信息
*/
private Function<String, ExampleDto> getExampleInfoFunction = (String id)-> exampleDao.getExampleInfo(id);
五,代碼實(shí)現(xiàn)
-
緩存管理接口
/** * 緩存管理接口 * <li></li> * * @author: duanyong@jccfc.com */ @Spi(CacheConfig.DEFAULT_IMPL) public interface CacheManager { /** * 設(shè)置單個(gè)值 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param value: 值 * @return: boolean true->成功 */ boolean set(String key, Object value); /** * 設(shè)置單個(gè)值并設(shè)置失效時(shí)間 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param value: 值 * @param expireTime:緩存時(shí)間,單位秒 * @return: boolean true->成功 */ boolean setAndExpire(String key, Object value, int expireTime); /** * 獲取單個(gè)值 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @return: T 返回對(duì)象 */ <T> T get(String key); /** * 批量設(shè)置hash * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param hash: Map對(duì)象 * @return: boolean true->成功 */ boolean hmSet(String key, Map<String, Object> hash); /** * 給hash字段設(shè)置值 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param field: 字段 * @param value: 值 * @return: boolean true->成功 */ boolean hSet(String key, String field, Object value); /** * 獲取hash值 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param field: 字段 * @return: T 返回對(duì)象 */ <T> T hGet(String key, String field); /** * 如果緩存key不存在則設(shè)置緩存并設(shè)置失效時(shí)間,否則不做操作 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param value: 值 * @param time: 緩存時(shí)間,單位秒 * @return: boolean true->成功 */ boolean setIfAbsent(final String key, Object value, int time); /** * 刪除緩存 * <li></li> * @author duanyong@jccfc.com * @date 2021/7/14 9:13 * @param key: 鍵 * @return: boolean true->成功 */ boolean del(String key); /** * 刪除hash緩存 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param fields: 字段 * @return: long 值 */ long hashDel(String key, String... fields); /** * 自增 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param liveTime: 天數(shù) 這個(gè)計(jì)數(shù)器的有效存留時(shí)間 * @param delta: 自增量 * @return: java.lang.Long */ Long incr(String key, long liveTime, long delta); } -
緩存管理接口實(shí)現(xiàn)類:
/** * 緩存管理接口實(shí)現(xiàn)類 * <li>基于redis實(shí)現(xiàn)</li> * * @author: duanyong@jccfc.com */ @Slf4j public class RedisCacheManager implements CacheManager { /** * RedisTemplate */ @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 設(shè)置單個(gè)值 * <li></li> * * @param key : 鍵 * @param value : 值 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean set(String key, Object value) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { log.error("[Redis緩存]設(shè)置緩存 key:{},value:{},失敗:{}", key, value, e); return false; } } /** * 設(shè)置單個(gè)值并設(shè)置失效時(shí)間 * <li></li> * * @param key : 鍵 * @param value : 值 * @param expireTime :緩存時(shí)間,單位秒 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean setAndExpire(String key, Object value, int expireTime) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { //設(shè)置默認(rèn)時(shí)間24H expireTime = expireTime <= 0 ? 1 : expireTime; redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS); return true; } catch (Exception e) { log.error("[Redis緩存]設(shè)置緩存 key:{},value:{},失敗:{}", key, value, e); return false; } } /** * 獲取單個(gè)值 * <li></li> * * @param key : 鍵 * @author duanyong@jccfc.com * @return: T 返回對(duì)象 */ @Override public <T> T get(String key) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { return (T) redisTemplate.opsForValue().get(key); } catch (Exception e) { log.error("[Redis緩存]獲取緩存 key:{},失敗:{}", key, e); return null; } } /** * 批量設(shè)置hash * <li></li> * * @param key : 鍵 * @param hash : Map對(duì)象 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean hmSet(String key, Map<String, Object> hash) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { redisTemplate.opsForHash().putAll(key, hash); return true; } catch (Exception e) { log.error("[Redis緩存]設(shè)置緩存hash key:{},value:{},失敗:{}", key, hash, e); return false; } } /** * 給hash字段設(shè)置值 * <li></li> * * @param key : 鍵 * @param field : 字段 * @param value : 值 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean hSet(String key, String field, Object value) { Objects.requireNonNull(key, "緩存Key不能為空!"); Objects.requireNonNull(field, "緩存哈希字段不能為空!"); try { redisTemplate.opsForHash().put(key, field, value); return true; } catch (Exception e) { log.error("[Redis緩存]設(shè)置hash值, key:{}, value:{} 失敗. {}", key, field, e); return false; } } /** * 獲取hash值 * <li></li> * * @param key : 鍵 * @param field : 字段 * @author duanyong@jccfc.com * @return: T 返回對(duì)象 */ @Override public <T> T hGet(String key, String field) { Objects.requireNonNull(key, "緩存Key不能為空!"); Objects.requireNonNull(field, "緩存哈希字段不能為空!"); try { return (T) redisTemplate.opsForHash().get(key, field); } catch (Exception e) { log.error("[Redis緩存]獲取緩存hash key:{},field:{} 失敗,{}", key, field, e); return null; } } /** * 如果緩存key不存在則設(shè)置緩存并設(shè)置失效時(shí)間,否則不做操作 * <li></li> * * @param key : 鍵 * @param value : 值 * @param time : 緩存時(shí)間,單位秒 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean setIfAbsent(String key, Object value, int time) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { time = time <= 0 ? 1 : time; return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS); } catch (Exception e) { log.error("[Redis緩存]設(shè)置緩存 key:{},value:{}失敗,{}", key, value, e); return false; } } /** * 刪除緩存 * <li></li> * * @param key : 鍵 * @author duanyong@jccfc.com * @return: boolean true->成功 */ @Override public boolean del(String key) { Objects.requireNonNull(key, "緩存Key不能為空!"); try { return redisTemplate.delete(key); } catch (Exception e) { log.error("[Redis緩存]刪除緩存 key:{} 失敗.{}", key, e); return false; } } /** * 刪除hash緩存 * <li></li> * * @param key : 鍵 * @param fields : 字段 * @author duanyong@jccfc.com * @return: long 值 */ @Override public long hashDel(String key, String... fields) { Objects.requireNonNull(key, "緩存Key不能為空!"); Objects.requireNonNull(fields, "緩存哈希字段不能為空!"); try { return redisTemplate.opsForHash().delete(key, fields); } catch (Exception e) { log.error("[Redis緩存]刪除緩存 key:{},fields:{} 失敗,{}", key, fields, e); } return -1; } /** * 自增 * <li></li> * * @param key : 鍵 * @param liveTime : 天數(shù) 這個(gè)計(jì)數(shù)器的有效存留時(shí)間 * @param delta : 自增量 * @author duanyong@jccfc.com * @return: java.lang.Long */ @Override public Long incr(String key, long liveTime, long delta) { RedisAtomicLong entityIdCounter = new RedisAtomicLong("INCSEQ_"+key, redisTemplate.getConnectionFactory()); Long increment = entityIdCounter.addAndGet(delta); //初始設(shè)置過(guò)期時(shí)間 if ((null == increment || increment.longValue() == 0) && liveTime > 0) { entityIdCounter.expire(liveTime, TimeUnit.DAYS); } return increment; } } -
緩存函數(shù)接口:
/** * 緩存函數(shù)接口 * @author: duanyong@jccfc.com * @since: 2021/7/14 11:08 */ @Spi(CacheConfig.DEFAULT_IMPL) public interface CacheFunction { /** * 根據(jù)參數(shù)獲取緩存 * <li>Function版</li> * @author duanyong@jccfc.com * @param key: 緩存KEY * @param expireTime: 超時(shí)時(shí)間,單位 秒 * @param clazz: 目標(biāo)對(duì)象類型 * @param function: 執(zhí)行函數(shù) * @param p: 附加給function的參數(shù) * @return: java.util.Optional<T> 對(duì)象數(shù)據(jù) */ <T,P> Optional<T> getCacheValueFunction(String key,int expireTime, Class<T> clazz, Function<P,T> function,P p); /** * 獲取緩存 * <li>Supplier版</li> * @author duanyong@jccfc.com * @param key:緩存KEY * @param expireTime:超時(shí)時(shí)間,單位 秒 * @param clazz:目標(biāo)對(duì)象類型 * @param function:執(zhí)行函數(shù) * @return: java.util.Optional<T> 對(duì)象數(shù)據(jù) */ <T> Optional<T> getCacheValueFunction(String key,int expireTime, Class<T> clazz, Supplier<T> function); /** * 根據(jù)參數(shù)獲取集合緩存 * <li>Function版</li> * @author duanyong@jccfc.com * @param key: 緩存KEY * @param expireTime: 超時(shí)時(shí)間,單位 秒 * @param clazz: 目標(biāo)對(duì)象類型 * @param function: 執(zhí)行函數(shù) * @param p: 附加給function的參數(shù) * @return: java.util.Optional<java.util.List<T>> 對(duì)象集合數(shù)據(jù) */ <P,T> Optional<List<T>> getCacheListValueFunction(String key,int expireTime, Class<T> clazz, Function<P,List<T>> function,P p); /** * 獲取集合緩存 * <li>Supplier版本</li> * @author duanyong@jccfc.com * @param key: 緩存KEY * @param expireTime: 超時(shí)時(shí)間,單位 秒 * @param clazz: 目標(biāo)對(duì)象類型 * @param function: 執(zhí)行函數(shù) * @return: java.util.Optional<java.util.List<T>> 對(duì)象集合數(shù)據(jù) */ <T> Optional<List<T>> getCacheListValueFunction(String key,int expireTime, Class<T> clazz,Supplier<List<T>> function); } -
緩存函數(shù)接口實(shí)現(xiàn)類:
/** * 緩存函數(shù)接口實(shí)現(xiàn)類 * <li></li> * <li>1,熱點(diǎn)數(shù)據(jù)集中失效解決方案:redisson分布式鎖+隨機(jī)過(guò)期時(shí)間</li> * <li>2,緩存穿透的解決方案:設(shè)置空數(shù)據(jù)特定值(根據(jù)業(yè)務(wù)場(chǎng)景特性:空數(shù)據(jù)的key數(shù)量有限、key重復(fù)請(qǐng)求概率較高),缺點(diǎn):需要存儲(chǔ)所有空數(shù)據(jù)的key,對(duì)于一些惡意攻擊,KEY不相同的情況,也起不了保護(hù)數(shù)據(jù)庫(kù)的作用</li> * <li>3,緩存穿透的解決備選方案:空數(shù)據(jù)的key各不相同、key重復(fù)請(qǐng)求概率低的場(chǎng)景而言,可使用BloomFilter</li> * @author: duanyong@jccfc.com */ @Slf4j public class RedisCacheFunction implements CacheFunction { /** * 根據(jù)參數(shù)獲取緩存 * <li>Function版</li> * * @param key : 緩存KEY * @param expireTime : 超時(shí)時(shí)間,單位 秒 * @param clazz : 目標(biāo)對(duì)象類型 * @param function : 執(zhí)行函數(shù) * @param p : 附加給function的參數(shù) * @author duanyong@jccfc.com * @return: java.util.Optional<T> 對(duì)象數(shù)據(jù) */ @Override public <T, P> Optional<T> getCacheValueFunction(String key, int expireTime, Class<T> clazz, Function<P, T> function, P p) { return getCacheValue(key,expireTime,clazz,CombinatorialFunction.<T,P>builder().function(function).p(p).build()); } /** * 獲取緩存 * <li>Supplier版</li> * * @param key :緩存KEY * @param expireTime :超時(shí)時(shí)間,單位 秒 * @param clazz :目標(biāo)對(duì)象類型 * @param function :執(zhí)行函數(shù) * @author duanyong@jccfc.com * @return: java.util.Optional<T> 對(duì)象數(shù)據(jù) */ @Override public <T> Optional<T> getCacheValueFunction(String key, int expireTime, Class<T> clazz, Supplier<T> function) { return getCacheValue(key,expireTime,clazz,CombinatorialFunction.<T,Object>builder().supplier(function).build()); } /** * 根據(jù)參數(shù)獲取集合緩存 * <li>Function版</li> * * @param key : 緩存KEY * @param expireTime : 超時(shí)時(shí)間,單位 秒 * @param clazz : 目標(biāo)對(duì)象類型 * @param function : 執(zhí)行函數(shù) * @param p : 附加給function的參數(shù) * @author duanyong@jccfc.com * @return: java.util.Optional<java.util.List<T>> 對(duì)象集合數(shù)據(jù) */ @Override public <P, T> Optional<List<T>> getCacheListValueFunction(String key, int expireTime, Class<T> clazz, Function<P, List<T>> function, P p) { return getCacheListValue(key,expireTime,clazz,CombinatorialFunction.<T,P>builder().listFunction(function).p(p).build()); } /** * 獲取集合緩存 * <li>Supplier版本</li> * * @param key : 緩存KEY * @param expireTime : 超時(shí)時(shí)間,單位 秒 * @param clazz : 目標(biāo)對(duì)象類型 * @param function : 執(zhí)行函數(shù) * @author duanyong@jccfc.com * @return: java.util.Optional<java.util.List<T>> 對(duì)象集合數(shù)據(jù) */ @Override public <T> Optional<List<T>> getCacheListValueFunction(String key, int expireTime, Class<T> clazz, Supplier<List<T>> function) { return getCacheListValue(key,expireTime,clazz,CombinatorialFunction.<T,Object>builder().listSupplier(function).build()); } /** * 獲取集合緩存 * <li></li> * @author duanyong@jccfc.com * @param key: 緩存KEY * @param expireTime: 超時(shí)時(shí)間,單位 秒 * @param clazz: 目標(biāo)對(duì)象類型 * @param combinatorialFunction:函數(shù)組合對(duì)象 * @return: java.util.Optional<java.util.List<T>> 對(duì)象集合數(shù)據(jù) */ public <P, T> Optional<List<T>> getCacheListValue(String key, int expireTime, Class<T> clazz,CombinatorialFunction<T,P> combinatorialFunction) { //獲取緩存 List<T> records = getList(key, clazz); if (records != null && !records.isEmpty()) { return Optional.of(records); } //檢查是否是特定值-empty Object o = get(key); if(Constants.CACHE_EMPTY_VALUE.equals(o)){ return Optional.empty(); } //獲取鎖失敗 if (!tryLock(key)) { log.error("獲取鎖失敗:key->{},直接返回.",key); return Optional.empty(); } //獲取鎖成功 try { //再檢查一次:當(dāng)其他等待線程獲取到鎖時(shí),緩存一般已經(jīng)有值,所以需要再次確認(rèn),以免重復(fù)查庫(kù) records = getList(key, clazz); if (records != null && !records.isEmpty()) { return Optional.of(records); } //執(zhí)行目標(biāo)函數(shù) if(combinatorialFunction.getListSupplier() != null){ records = combinatorialFunction.getListSupplier().get(); }else if(combinatorialFunction.getListFunction() != null && combinatorialFunction.getP() != null){ records = combinatorialFunction.getListFunction().apply(combinatorialFunction.getP()); } if (records != null && !records.isEmpty()) { //設(shè)置緩存 setObject(key, records,expireTime); //再次獲取緩存,確保緩存成功 records = getList(key, clazz); if (records != null && !records.isEmpty()) { log.info("線程{},執(zhí)行目標(biāo)函數(shù)獲取數(shù)據(jù)并緩存,key={}",Thread.currentThread().getName(), key); return Optional.of(records); } } //緩存空字符串?dāng)?shù)據(jù),防止重復(fù)請(qǐng)求 setAndExpire(key, Constants.CACHE_EMPTY_VALUE,expireTime); log.info("線程{},數(shù)據(jù)不存在,緩存空字符串?dāng)?shù)據(jù),防止重復(fù)請(qǐng)求,key={}",Thread.currentThread().getName(), key); } catch(Exception e){ log.error("獲取集合緩存失敗:key->{}.",key,e); throw e; }finally { unlock(key); } return Optional.empty(); } /** * 獲取單值緩存 * <li></li> * @author duanyong@jccfc.com * @param key: 緩存KEY * @param expireTime: 超時(shí)時(shí)間,單位 秒 * @param clazz: 目標(biāo)對(duì)象類型 * @param combinatorialFunction: 函數(shù)組合對(duì)象 * @return: java.util.Optional<T> 對(duì)象數(shù)據(jù) */ public <T, P> Optional<T> getCacheValue(String key, int expireTime, Class<T> clazz, CombinatorialFunction<T,P> combinatorialFunction) { //獲取緩存 T record = getObject(key, clazz); if (record != null) { return Optional.of(record); } //檢查是否是特定值-empty Object o = get(key); if(Constants.CACHE_EMPTY_VALUE.equals(o)){ return Optional.empty(); } //獲取鎖失敗 if (!tryLock(key)) { log.error("獲取鎖失敗:key->{},直接返回.",key); return Optional.empty(); } try { //再檢查一次:當(dāng)其他等待線程獲取到鎖時(shí),緩存一般已經(jīng)有值,所以需要再次確認(rèn),以免重復(fù)查庫(kù) record = getObject(key, clazz); if(record != null){ return Optional.of(record); } //執(zhí)行目標(biāo)函數(shù) if(combinatorialFunction.getSupplier() != null){ record = combinatorialFunction.getSupplier().get(); }else if(combinatorialFunction.getFunction() != null && combinatorialFunction.getP() != null){ record = combinatorialFunction.getFunction().apply(combinatorialFunction.getP()); } if(record != null){ //設(shè)置緩存 setObject(key, record,expireTime); //再次獲取緩存,確保緩存成功 record = getObject(key, clazz); if(record != null){ log.info("線程{},執(zhí)行目標(biāo)函數(shù)獲取數(shù)據(jù)并緩存,key={}",Thread.currentThread().getName(), key); return Optional.of(record); } } //緩存空字符串?dāng)?shù)據(jù),防止重復(fù)請(qǐng)求 setAndExpire(key, Constants.CACHE_EMPTY_VALUE,expireTime); log.info("線程{},數(shù)據(jù)不存在,緩存空字符串?dāng)?shù)據(jù),防止重復(fù)請(qǐng)求,key={}",Thread.currentThread().getName(), key); } catch(Exception e){ log.error("根據(jù)參數(shù)獲取緩存失敗:key->{}.",key,e); throw e; } finally { unlock(key); } return Optional.empty(); } /** * 加鎖 * <li></li> * @author duanyong@jccfc.com * @param cacheKey: 緩存key * @return: boolean 是否成功 */ private boolean tryLock(String cacheKey){ int timeout = Lock.TIMEOUT_SECOND; if(!LockHolder.getLock().isPresent()){ log.error("不支持加鎖"); return false; } boolean isLocked = LockHolder.getLock().get().tryLock(cacheKey, TimeUnit.SECONDS,0,timeout); if(isLocked){ SwapAreaUtils.getSwapAreaData().setCacheKey(cacheKey); log.info("加鎖成功,KEY:{},自動(dòng)失效時(shí)間:{}秒",cacheKey,timeout); } return isLocked; } /** * 解鎖 * <li></li> * @author duanyong@jccfc.com * @param key: 緩存key * @return: void */ private void unlock(String key){ if(!LockHolder.getLock().isPresent()){ return; } LockHolder.getLock().get().unlock(key); } /** * 獲取對(duì)象集合 * <li></li> * @author duanyong@jccfc.com * @param key:鍵 * @param clazz:對(duì)象類型 * @return: java.util.List<T>對(duì)象集合 */ private final <T> List<T> getList(final String key,final Class<T> clazz) { try{ return FastJsonUtil.toList(get(key), clazz); }catch(Exception ex){ log.error("獲取對(duì)象集合->JSON轉(zhuǎn)集合對(duì)象失敗",ex); //如果不是空值,說(shuō)明數(shù)據(jù)異常,需要?jiǎng)h除此數(shù)據(jù) if(!Constants.CACHE_EMPTY_VALUE.equals(get(key))){ del(key); } } return Collections.emptyList(); } /** * 獲取對(duì)象 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param clazz: 對(duì)象類型 * @return: T 目標(biāo)對(duì)象 */ private final <T> T getObject(final String key,final Class<T> clazz) { try{ return FastJsonUtil.toBean(get(key), clazz); }catch(Exception ex){ //如果不是空值 if(!Constants.CACHE_EMPTY_VALUE.equals(get(key))){ del(key); } } return null; } /** * 刪除緩存 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @return: boolean true->成功 */ private boolean del(String key) { if(getCacheManager() == null){ return false; } return getCacheManager().del(key); } /** * 設(shè)置對(duì)象 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param value: 對(duì)象 * @param expireTime 超時(shí)時(shí)間,單位 秒 */ private void setObject(final String key, final Object value,final int expireTime) { setAndExpire(key, FastJsonUtil.toJSONString(value),expireTime); } /** * 設(shè)置單個(gè)值并設(shè)置失效時(shí)間 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @param value: 值 * @param expireTime:緩存時(shí)間,單位秒 * @return: boolean true->成功 */ private boolean setAndExpire(String key, Object value, int expireTime) { if(getCacheManager() == null){ return false; } return getCacheManager().setAndExpire(key,value,expireTime); } /** * 獲取單個(gè)值 * <li></li> * @author duanyong@jccfc.com * @param key: 鍵 * @return: T 返回對(duì)象 */ private <T> T get(String key) { if(getCacheManager() == null){ return null; } return getCacheManager().get(key); } /** * 獲取緩存管理器 * <li></li> * @author duanyong@jccfc.com * @return: com.javacoo.xservice.base.support.cache.CacheManager */ private CacheManager getCacheManager(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持緩存"); return null; } return CacheHolder.getCacheManager().get(); } } -
函數(shù)組合對(duì)象:
/** * 函數(shù)組合對(duì)象 * <li></li> * * @author: duanyong@jccfc.com */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CombinatorialFunction<T,P> { /** * 單值,提供者函數(shù) */ private Supplier<T> supplier; /** * 單值,帶參數(shù)函數(shù) */ private Function<P, T> function; /** * 集合,提供者函數(shù) */ private Supplier<List<T>> listSupplier; /** * 集合,帶參數(shù)函數(shù) */ private Function<P, List<T>> listFunction; /** * 參數(shù) */ private P p; } -
緩存管理對(duì)象持有者:
/** * 緩存管理對(duì)象持有者 * <li></li> * @author duanyong@jccfc.com */ public class CacheHolder { /** 緩存管理接口對(duì)象*/ static CacheManager cacheManager; /** 緩存函數(shù)接口對(duì)象*/ static CacheFunction cacheFunction; public static Optional<CacheManager> getCacheManager() { return Optional.ofNullable(cacheManager); } public static Optional<CacheFunction> getCacheFunction() { return Optional.ofNullable(cacheFunction); } } -
緩存管理接口工廠:
/** * 緩存管理接口工廠 * <li></li> * @author duanyong@jccfc.com */ @Slf4j @Component @ConditionalOnBean(CacheConfig.class) public class CacheFactory { /** 緩存配置 */ @Autowired private CacheConfig cacheConfig; @Bean public CacheManager createCacheManager() { log.info("初始化緩存管理,實(shí)現(xiàn)類名稱:{}",cacheConfig.getImpl()); CacheHolder.cacheManager = ExtensionLoader.getExtensionLoader(CacheManager.class).getExtension(cacheConfig.getImpl()); log.info("初始化緩存管理,緩存管理接口實(shí)現(xiàn)類:{}", CacheHolder.cacheManager); return CacheHolder.cacheManager; } @Bean public CacheFunction createCacheFunction() { log.info("初始化緩存函數(shù)接口,實(shí)現(xiàn)類名稱:{}",cacheConfig.getFunctionImpl()); CacheHolder.cacheFunction = ExtensionLoader.getExtensionLoader(CacheFunction.class).getExtension(cacheConfig.getFunctionImpl()); log.info("初始化緩存函數(shù)接口實(shí)現(xiàn)類:{}", CacheHolder.cacheFunction); return CacheHolder.cacheFunction; } } -
工具類實(shí)現(xiàn):
/** * 緩存工具類 * <li>提供緩存基本操作</li> * <li>獲取單個(gè)對(duì)象緩存:Function版</li> * <li>獲取單個(gè)對(duì)象緩存:Supplier版</li> * <li>獲取集合類型緩存:Function版</li> * <li>獲取集合類型緩存:Supplier版</li> * @author duanyong@jccfc.com */ @Component public class CacheUtil { /** * 設(shè)置單個(gè)值 * @param key * @param value * @return */ public boolean set(String key, Object value) { return getCacheManager() == null ? false : getCacheManager().set(key,value); } /** * 設(shè)置單個(gè)值并設(shè)置失效時(shí)間 * @param key * @param value * @param expireTime 緩存時(shí)間,單位秒 * @return */ public boolean setAndExpire(String key, Object value, int expireTime) { return getCacheManager() == null ? false : getCacheManager().setAndExpire(key,value,expireTime); } /** * 獲取單個(gè)值 * @param key * @return */ public <T> T get(String key) { return getCacheManager() == null ? null : getCacheManager().get(key); } /** * 批量設(shè)置hash * @param key * @param hash * @return */ public boolean hmSet(String key, Map<String, Object> hash) { return getCacheManager() == null ? false : getCacheManager().hmSet(key,hash); } /** * 給hash字段設(shè)置值 * @param key * @param field * @param value * @return */ public boolean hSet(String key, String field, Object value) { return getCacheManager() == null ? false : getCacheManager().hSet(key,field,value); } /** * 獲取hash值 * 引用見(jiàn){@link RedisTemplate} * @param key * @param field * @return */ public <T> T hGet(String key, String field) { return getCacheManager() == null ? null : getCacheManager().hGet(key,field); } /** * 如果緩存key不存在則設(shè)置緩存并設(shè)置失效時(shí)間,否則不做操作 * @param key * @param value * @param time 緩存時(shí)間,單位秒 * @return */ public boolean setIfAbsent(final String key, Object value, int time) { return getCacheManager() == null ? false : getCacheManager().setIfAbsent(key,value,time); } /** * 刪除緩存 * @param key * @return */ public boolean del(String key) { return getCacheManager() == null ? false : getCacheManager().del(key); } /** * 刪除hash緩存 * @param key * @param fields * @return */ public long hashDel(String key, String... fields) { return getCacheManager() == null ? -1 : getCacheManager().hashDel(key,fields); } /** * redis 自增 * @param key * @param liveTime 天數(shù) 這個(gè)計(jì)數(shù)器的有效存留時(shí)間 * @param delta 自增量 * @return */ public long incr(String key, long liveTime, long delta) { return getCacheManager() == null ? -1 : getCacheManager().incr(key,liveTime,delta); } /** * 獲取緩存 * <p> * 說(shuō)明:Function版 * </p> * @author DuanYong * @param key 緩存KEY * @param expireTime 超時(shí)時(shí)間,單位 秒 * @param clazz 目標(biāo)對(duì)象類型 * @param function 執(zhí)行函數(shù) * @param p 附加給function的參數(shù) * @return 目標(biāo)對(duì)象 */ public <T,P> T getCacheValueFunction(String key,int expireTime, Class<T> clazz, Function<P,T> function,P p) { return getCacheFunction() == null ? null : getCacheFunction().getCacheValueFunction(key,expireTime,clazz,function,p).orElse(null); } /** * 獲取緩存 * <p> * 說(shuō)明:Supplier版 * </p> * @author DuanYong * @param key 緩存KEY * @param clazz 目標(biāo)對(duì)象類型 * @param function 執(zhí)行函數(shù) * @return 目標(biāo)對(duì)象 */ public <T> T getCacheValueFunction(String key,int expireTime, Class<T> clazz, Supplier<T> function) { return getCacheFunction() == null ? null : getCacheFunction().getCacheValueFunction(key,expireTime,clazz,function).orElse(null); } /** * 獲取集合緩存 * <p>說(shuō)明:Function版本</p> * @author DuanYong * @param key 緩存KEY * @param clazz 目標(biāo)對(duì)象類型 * @param function 執(zhí)行函數(shù) * @param p 附加給function的參數(shù) * @return 目標(biāo)對(duì)象 */ public <P,T> List<T> getCacheListValueFunction(String key,int expireTime, Class<T> clazz, Function<P,List<T>> function,P p) { return getCacheFunction() == null ? Collections.emptyList() : getCacheFunction().getCacheListValueFunction(key,expireTime,clazz,function,p).orElse(Collections.emptyList()); } /** * 獲取集合緩存 * <p>說(shuō)明:Supplier版本</p> * @author DuanYong * @param key 緩存KEY * @param clazz 目標(biāo)對(duì)象類型 * @param function 執(zhí)行函數(shù) * @return 目標(biāo)對(duì)象 */ public <T> List<T> getCacheListValueFunction(String key,int expireTime, Class<T> clazz,Supplier<List<T>> function) { return getCacheFunction() == null ? Collections.emptyList() : getCacheFunction().getCacheListValueFunction(key,expireTime,clazz,function).orElse(Collections.emptyList()); } /** * 獲取緩存管理器 * <li></li> * @author duanyong@jccfc.com * @return: com.javacoo.xservice.base.support.cache.CacheManager */ private CacheManager getCacheManager(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持緩存"); return null; } return CacheHolder.getCacheManager().get(); } /** * 獲取緩存函數(shù)接口 * <li></li> * @author duanyong@jccfc.com * @return: com.javacoo.xservice.base.support.cache.CacheFunction */ private CacheFunction getCacheFunction(){ if(!CacheHolder.getCacheManager().isPresent()){ log.error("不支持緩存"); return null; } if(!CacheHolder.getCacheFunction().isPresent()){ log.error("不支持緩存函數(shù)"); return null; } return CacheHolder.getCacheFunction().get(); }
應(yīng)用場(chǎng)景
- 適用于大多數(shù)并發(fā)較少,數(shù)據(jù)一致性要求不高的場(chǎng)景。
結(jié)果驗(yàn)證及局限性
- 統(tǒng)一了緩存使用模式,簡(jiǎn)化了開(kāi)發(fā)。
- 此方案在并發(fā)較少,數(shù)據(jù)一致性要求不高的場(chǎng)景效果較好。
- 高并發(fā),數(shù)據(jù)一致性要求高的場(chǎng)景其緩存設(shè)計(jì)方案可參考: OpenResty+Lua+Redis+Canal實(shí)現(xiàn)多級(jí)緩存架構(gòu)
后續(xù)規(guī)劃
- 完善緩存相關(guān)問(wèn)題解決方案。
一些信息
路漫漫其修遠(yuǎn)兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
郵箱:xihuady@126.com