前言
公司部門老專案要遷移升級java版本,需要進行快取相關操作,原框架未支持這部分,經過調研java相關快取方案大致分為ehcache和redis兩種,redis的value最大值為500mb且超過1mb會對存取有性能影響,業務系統需要支持串列查詢快取就不可避免的涉及到大量的資料存取過濾,ehcache支持記憶體+磁盤快取不用擔心快取容量問題,所以框架初步版本決定集成ehcache3,設計流程結構如下圖所示

快取配置
maven參考
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
個性化配置
#快取配置
cache:
ehcache:
heap: 1000
offheap: 100
disk: 500
diskDir: tempfiles/cache/
@Component
@ConfigurationProperties("frmae.cache.ehcache")
public class EhcacheConfiguration {
/**
* ehcache heap大小
* jvm記憶體中快取的key數量
*/
private int heap;
/**
* ehcache offheap大小
* 堆外記憶體大小, 單位: MB
*/
private int offheap;
/**
* 磁盤持久化目錄
*/
private String diskDir;
/**
* ehcache disk
* 持久化到磁盤的大小, 單位: MB
* diskDir有效時才生效
*/
private int disk;
public EhcacheConfiguration(){
heap = 1000;
offheap = 100;
disk = 500;
diskDir = "tempfiles/cache/";
}
}
代碼注入配置
因為springboot默認快取優先注入redis配置,所以需要手動宣告bean進行注入,同時ehcache的value值必須支持序列化介面,不能使用Object代替,這里宣告一個快取基類,所有快取value物件必須繼承該類
public class BaseSystemObject implements Serializable {
}
@Configuration
@EnableCaching
public class EhcacheConfig {
@Autowired
private EhcacheConfiguration ehcacheConfiguration;
@Autowired
private ApplicationContext context;
@Bean(name = "ehCacheManager")
public CacheManager getCacheManager() {
//資源池生成器配置持久化
ResourcePoolsBuilder resourcePoolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder()
// 堆內快取大小
.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)
//設定永不過期
.withExpiry(expiryPolicy)
.build();
CacheManagerBuilder cacheManagerBuilder = CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(ehcacheConfiguration.getDiskDir()));
return cacheManagerBuilder.build(true);
}
}
快取操作
快取預熱
針對快取框架選擇的雙寫策略,即資料庫和快取同時寫入,所以在系統啟動時需要預先將資料庫資料加載到快取中
針對單表宣告自定義注解,個性化快取定義自定義介面
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HPCache {
}
public interface IHPCacheInitService {
String getCacheName();
void initCache();
}
系統初始化時同步進行快取初始化,掃描注解物體類與介面實作Bean
@Async
public void initCache(Class runtimeClass, List<String> extraPackageNameList) {
List<Class<?>> 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<LinkedHashMap<String, Object>> resultList = commonDTO.selectList(tableName.value(), "*", "1=1", "", new HashMap<>(), false);
for (LinkedHashMap<String, Object> 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<String, IHPCacheInitService> res = context.getBeansOfType(IHPCacheInitService.class);
for (Map.Entry en : res.entrySet()) {
IHPCacheInitService service = (IHPCacheInitService) en.getValue();
service.initCache();
}
System.out.println("快取初始化完畢");
}
需要注意,在EhcacheConfig配置類中需要進行快取名稱的提前注冊,否則會導致操作快取時空指標例外
Map<String, Object> annotatedBeans = context.getBeansWithAnnotation(SpringBootApplication.class);
Class runtimeClass = annotatedBeans.values().toArray()[0].getClass();
//do,dao掃描
List<String> extraPackageNameList = new ArrayList<String>();
extraPackageNameList.add(Application.class.getPackage().getName());
List<Class<?>> 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<String, IHPCacheInitService> res = context.getBeansOfType(IHPCacheInitService.class);
for (Map.Entry en :res.entrySet()) {
IHPCacheInitService service = (IHPCacheInitService)en.getValue();
cacheManagerBuilder = cacheManagerBuilder.withCache(service.getCacheName(), config);
}
更新操作
手動獲取ehcache的bean物件,呼叫put,repalce,delete方法進行操作
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);
}
查詢操作
快取存盤單表以主鍵—object形式存盤,個性化快取為key-object形式存盤,單條記錄可以通過getCache方法查詢,串列查詢需要取出整個快取按條件進行過濾
public Object getCache(String cacheName, String key){
Cache cache = cacheManager.getCache(cacheName, String.class, BaseSystemObject.class);
return cache.get(key);
}
public List<Object> getAllCache(String cacheName){
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;
}
快取與資料庫資料一致性
資料庫資料操作與快取操作順序為先操作資料后操作快取,在開啟資料庫事務的情況下針對單條資料單次操作是沒有問題的,如果是組合操作一旦資料庫操作發生例外回滾,快取并沒有回滾就會導致資料的不一致,比如執行順序為dbop1=》cacheop1=》dbop2=》cacheop2,dbop2例外,cacheop1的操作已經更改了快取
這里選擇的方案是在資料庫全部執行完畢后統一操作快取,這個方案有一個缺點是如果快取操作發生例外還是會出現上述問題,實際程序中快取只是對記憶體的操作例外概率較小,對快取操作持樂觀狀態,同時我們提供手動重置快取的功能,算是一個折中方案,下面概述該方案的一個實作
宣告自定義快取事務注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheTransactional {
}
宣告切面監聽,在標記了CacheTransactional注解的方法執行前進行Redis標識,統一執行完方法體后執行快取操作
@Aspect
@Component
@Order(value = https://www.cnblogs.com/yanpeng19940119/p/101)
public class CacheExecuteAspect {
@Autowired
private CacheExecuteUtil cacheExecuteUtil;
/**
* 切面點 指定注解
*/
@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();
}
}
}
將快取操作以執行緒id區分放入待執行佇列中序列化到redis,提供方法統一操作
public class CacheExecuteModel implements Serializable {
private String obejctClazzName;
private String cacheName;
private String key;
private BaseSystemObject value;
private String executeType;
}
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<LinkedHashMap> 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());
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/402404.html
標籤:Java
上一篇:Mybatis的聯合查詢
