SpringBoot整合Ehcache3
前言
公司部門(mén)老項(xiàng)目要遷移升級(jí)java版本,需要進(jìn)行緩存相關(guān)操作,原框架未支持這部分,經(jīng)過(guò)調(diào)研java相關(guān)緩存方案大致分為ehcache和redis兩種,redis的value最大值為500mb且超過(guò)1mb會(huì)對(duì)存取有性能影響,業(yè)務(wù)系統(tǒng)需要支持列表查詢(xún)緩存就不可避免的涉及到大量的數(shù)據(jù)存取過(guò)濾,ehcache支持內(nèi)存+磁盤(pán)緩存不用擔(dān)心緩存容量問(wèn)題,所以框架初步版本決定集成ehcache3,設(shè)計(jì)流程結(jié)構(gòu)如下圖所示

緩存配置
maven引用
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.ehcachegroupId>
<artifactId>ehcacheartifactId>
dependency>
復(fù)制代碼個(gè)性化配置
#緩存配置
cache:
ehcache:
heap: 1000
offheap: 100
disk: 500
diskDir: tempfiles/cache/
復(fù)制代碼@Component
@ConfigurationProperties("frmae.cache.ehcache")
public class EhcacheConfiguration {
/**
* ehcache heap大小
* jvm內(nèi)存中緩存的key數(shù)量
*/
private int heap;
/**
* ehcache offheap大小
* 堆外內(nèi)存大小, 單位: MB
*/
private int offheap;
/**
* 磁盤(pán)持久化目錄
*/
private String diskDir;
/**
* ehcache disk
* 持久化到磁盤(pán)的大小, 單位: MB
* diskDir有效時(shí)才生效
*/
private int disk;
public EhcacheConfiguration(){
heap = 1000;
offheap = 100;
disk = 500;
diskDir = "tempfiles/cache/";
}
}
復(fù)制代碼代碼注入配置
因?yàn)閟pringboot默認(rèn)緩存優(yōu)先注入redis配置,所以需要手動(dòng)聲明bean進(jìn)行注入,同時(shí)ehcache的value值必須支持序列化接口,不能使用Object代替,這里聲明一個(gè)緩存基類(lèi),所有緩存value對(duì)象必須繼承該類(lèi)
public class BaseSystemObject implements Serializable {
}
復(fù)制代碼@Configuration
@EnableCaching
public class EhcacheConfig {
@Autowired
private EhcacheConfiguration ehcacheConfiguration;
@Autowired
private ApplicationContext context;
@Bean(name = "ehCacheManager")
public CacheManager getCacheManager() {
//資源池生成器配置持久化
ResourcePoolsBuilder resourcePoolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder()
// 堆內(nèi)緩存大小
.heap(ehcacheConfiguration.getHeap(), EntryUnit.ENTRIES)
// 堆外緩存大小
.offheap(ehcacheConfiguration.getOffheap(), MemoryUnit.MB)
// 文件緩存大小
.disk(ehcacheConfiguration.getDisk(), MemoryUnit.MB);
//生成配置
ExpiryPolicy expiryPolicy = ExpiryPolicyBuilder.noExpiration();
CacheConfiguration config = CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, BaseSystemObject.class, resourcePoolsBuilder)
//設(shè)置永不過(guò)期
.withExpiry(expiryPolicy)
.build();
CacheManagerBuilder cacheManagerBuilder = CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(ehcacheConfiguration.getDiskDir()));
return cacheManagerBuilder.build(true);
}
}
復(fù)制代碼緩存操作
緩存預(yù)熱
針對(duì)緩存框架選擇的雙寫(xiě)策略,即數(shù)據(jù)庫(kù)和緩存同時(shí)寫(xiě)入,所以在系統(tǒng)啟動(dòng)時(shí)需要預(yù)先將數(shù)據(jù)庫(kù)數(shù)據(jù)加載到緩存中
針對(duì)單表聲明自定義注解,個(gè)性化緩存定義自定義接口
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HPCache {
}
復(fù)制代碼public interface IHPCacheInitService {
String getCacheName();
void initCache();
}
復(fù)制代碼系統(tǒng)初始化時(shí)同步進(jìn)行緩存初始化,掃描注解實(shí)體類(lèi)與接口實(shí)現(xiàn)Bean
@Async
public void initCache(Class runtimeClass, List extraPackageNameList) {
List> cacheEntityList = new ArrayList<>();
if (!runtimeClass.getPackage().getName().equals(Application.class.getPackage().getName())) {
cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(runtimeClass.getPackage(), HPCache.class));
}
for (String packageName : extraPackageNameList) {
cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(packageName, HPCache.class));
}
for (Class clazz : cacheEntityList) {
TableName tableName = (TableName) clazz.getAnnotation(TableName.class);
List> resultList = commonDTO.selectList(tableName.value(), "*", "1=1", "", new HashMap<>(), false);
for (LinkedHashMap map : resultList) {
Cache cache = cacheManager.getCache(clazz.getName(), String.class, BaseSystemObject.class);
String unitguid = ConvertOp.convert2String(map.get("UnitGuid"));
try {
Object obj = clazz.newInstance();
obj = ConvertOp.convertLinkHashMapToBean(map, obj);
cache.put(unitguid, obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//自定義緩存
Map res = context.getBeansOfType(IHPCacheInitService.class);
for (Map.Entry en : res.entrySet()) {
IHPCacheInitService service = (IHPCacheInitService) en.getValue();
service.initCache();
}
System.out.println("緩存初始化完畢");
}
復(fù)制代碼 需要注意,在EhcacheConfig配置類(lèi)中需要進(jìn)行緩存名稱(chēng)的提前注冊(cè),否則會(huì)導(dǎo)致操作緩存時(shí)空指針異常
Map annotatedBeans = context.getBeansWithAnnotation(SpringBootApplication.class);
Class runtimeClass = annotatedBeans.values().toArray()[0].getClass();
//do,dao掃描
List extraPackageNameList = new ArrayList();
extraPackageNameList.add(Application.class.getPackage().getName());
List> cacheEntityList = new ArrayList<>();
if (!runtimeClass.getPackage().getName().equals(Application.class.getPackage().getName())) {
cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(runtimeClass.getPackage(), HPCache.class));
}
for (String packageName : extraPackageNameList) {
cacheEntityList.addAll(ScanUtil.getAllClassByPackageName_Annotation(packageName, HPCache.class));
}
for (Class clazz : cacheEntityList) {
cacheManagerBuilder = cacheManagerBuilder.withCache(clazz.getName(), config);
}
//自定義緩存
Map res = context.getBeansOfType(IHPCacheInitService.class);
for (Map.Entry en :res.entrySet()) {
IHPCacheInitService service = (IHPCacheInitService)en.getValue();
cacheManagerBuilder = cacheManagerBuilder.withCache(service.getCacheName(), config);
}
復(fù)制代碼 更新操作
手動(dòng)獲取ehcache的bean對(duì)象,調(diào)用put,repalce,delete方法進(jìn)行操作
private CacheManager cacheManager = (CacheManager) SpringBootBeanUtil.getBean("ehCacheManager");
public void executeUpdateOperation(String cacheName, String key, BaseSystemObject value) {
Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
if (cache.containsKey(key)) {
cache.replace(key, value);
} else {
cache.put(key, value);
}
}
public void executeDeleteOperation(String cacheName, String key) {
Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
cache.remove(key);
}
復(fù)制代碼查詢(xún)操作
緩存存儲(chǔ)單表以主鍵—object形式存儲(chǔ),個(gè)性化緩存為key-object形式存儲(chǔ),單條記錄可以通過(guò)getCache方法查詢(xún),列表查詢(xún)需要取出整個(gè)緩存按條件進(jìn)行過(guò)濾
public Object getCache(String cacheName, String key){
Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
return cache.get(key);
}
public List{
List result = new ArrayList<>();
Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
Iterator iter = cache.iterator();
while (iter.hasNext()) {
Cache.Entry entry = (Cache.Entry) iter.next();
result.add(entry.getValue());
}
return result;
}
復(fù)制代碼緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)一致性
數(shù)據(jù)庫(kù)數(shù)據(jù)操作與緩存操作順序?yàn)橄炔僮鲾?shù)據(jù)后操作緩存,在開(kāi)啟數(shù)據(jù)庫(kù)事務(wù)的情況下針對(duì)單條數(shù)據(jù)單次操作是沒(méi)有問(wèn)題的,如果是組合操作一旦數(shù)據(jù)庫(kù)操作發(fā)生異常回滾,緩存并沒(méi)有回滾就會(huì)導(dǎo)致數(shù)據(jù)的不一致,比如執(zhí)行順序?yàn)閐bop1=》cacheop1=》dbop2=》cacheop2,dbop2異常,cacheop1的操作已經(jīng)更改了緩存
這里選擇的方案是在數(shù)據(jù)庫(kù)全部執(zhí)行完畢后統(tǒng)一操作緩存,這個(gè)方案有一個(gè)缺點(diǎn)是如果緩存操作發(fā)生異常還是會(huì)出現(xiàn)上述問(wèn)題,實(shí)際過(guò)程中緩存只是對(duì)內(nèi)存的操作異常概率較小,對(duì)緩存操作持樂(lè)觀狀態(tài),同時(shí)我們提供手動(dòng)重置緩存的功能,算是一個(gè)折中方案,下面概述該方案的一個(gè)實(shí)現(xiàn)
聲明自定義緩存事務(wù)注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheTransactional {
}
復(fù)制代碼聲明切面監(jiān)聽(tīng),在標(biāo)記了CacheTransactional注解的方法執(zhí)行前進(jìn)行Redis標(biāo)識(shí),統(tǒng)一執(zhí)行完方法體后執(zhí)行緩存操作
@Aspect
@Component
@Order(value = 101)
public class CacheExecuteAspect {
@Autowired
private CacheExecuteUtil cacheExecuteUtil;
/**
* 切面點(diǎn) 指定注解
*/
@Pointcut("@annotation(com.haopan.frame.common.annotation.CacheTransactional) " +
"|| @within(com.haopan.frame.common.annotation.CacheTransactional)")
public void cacheExecuteAspect() {
}
/**
* 攔截方法指定為 repeatSubmitAspect
*/
@Around("cacheExecuteAspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CacheTransactional cacheTransactional = method.getAnnotation(CacheTransactional.class);
if (cacheTransactional != null) {
cacheExecuteUtil.putCacheIntoTransition();
try{
Object obj = point.proceed();
cacheExecuteUtil.executeOperation();
return obj;
}catch (Exception e){
e.printStackTrace();
throw e;
}
} else {
return point.proceed();
}
}
}
復(fù)制代碼將緩存操作以線程id區(qū)分放入待執(zhí)行隊(duì)列中序列化到redis,提供方法統(tǒng)一操作
public class CacheExecuteModel implements Serializable {
private String obejctClazzName;
private String cacheName;
private String key;
private BaseSystemObject value;
private String executeType;
}
復(fù)制代碼private CacheManager cacheManager = (CacheManager) SpringBootBeanUtil.getBean("ehCacheManager");
@Autowired
private RedisUtil redisUtil;
public void putCacheIntoTransition(){
String threadID = Thread.currentThread().getName();
System.out.println("init threadid:"+threadID);
CacheExecuteModel cacheExecuteModel = new CacheExecuteModel();
cacheExecuteModel.setExecuteType("option");
redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
}
public void putCache(String cacheName, String key, BaseSystemObject value) {
if(checkCacheOptinionInTransition()){
String threadID = Thread.currentThread().getName();
CacheExecuteModel cacheExecuteModel = new CacheExecuteModel("update", cacheName, key, value.getClass().getName(),value);
redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
}else{
executeUpdateOperation(cacheName,key,value);
}
}
public void deleteCache(String cacheName, String key) {
if(checkCacheOptinionInTransition()){
String threadID = Thread.currentThread().getName();
CacheExecuteModel cacheExecuteModel = new CacheExecuteModel("delete", cacheName, key);
redisUtil.redisTemplateSetForCollection(threadID,cacheExecuteModel, GlobalEnum.RedisDBNum.Cache.get_value());
redisUtil.setExpire(threadID,5, TimeUnit.MINUTES, GlobalEnum.RedisDBNum.Cache.get_value());
}else{
executeDeleteOperation(cacheName,key);
}
}
public void executeOperation(){
String threadID = Thread.currentThread().getName();
if(checkCacheOptinionInTransition()){
List executeList = redisUtil.redisTemplateGetForCollectionAll(threadID, GlobalEnum.RedisDBNum.Cache.get_value());
for (LinkedHashMap obj:executeList) {
String executeType = ConvertOp.convert2String(obj.get("executeType"));
if(executeType.contains("option")){
continue;
}
String obejctClazzName = ConvertOp.convert2String(obj.get("obejctClazzName"));
String cacheName = ConvertOp.convert2String(obj.get("cacheName"));
String key = ConvertOp.convert2String(obj.get("key"));
LinkedHashMap valueMap = (LinkedHashMap)obj.get("value");
String valueMapJson = JSON.toJSONString(valueMap);
try{
Object valueInstance = JSON.parseObject(valueMapJson,Class.forName(obejctClazzName));
if(executeType.equals("update")){
executeUpdateOperation(cacheName,key,(BaseSystemObject)valueInstance);
}else if(executeType.equals("delete")){
executeDeleteOperation(cacheName,key);
}
}catch (Exception e){
e.printStackTrace();
}
}
redisUtil.redisTemplateRemove(threadID,GlobalEnum.RedisDBNum.Cache.get_value());
}
}
public boolean checkCacheOptinionInTransition(){
String threadID = Thread.currentThread().getName();
System.out.println("check threadid:"+threadID);
return redisUtil.isValid(threadID, GlobalEnum.RedisDBNum.Cache.get_value());
} 作者:code2roc
鏈接:https://juejin.cn/post/7048876487318962206
來(lái)源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
