SpringCache自定義過期時間及自動重新整理
背景前提
閱讀說明(十分重要)
對於Cache和SpringCache原理不太清楚的朋友,可以看我之前寫的文章:Springboot中的快取Cache和CacheManager原理介紹
能關注SpringCache,想了解過期實現和自動重新整理的朋友,肯定有一定Java基礎的,所以先了解我的思想,達成共識比直接看程式碼重要許多
你可能只需要我裡面其中一個點而不是原搬照抄
我在實現過程遇到三大坑,先跟大家說下,興許對你有幫助
坑一:自己造輪子
對SpringCache不怎麼了解,直接百度快取看到Redis後,就直接使用RedisTemple開始工具類的搭建(說白了就是自己擼一個增刪查改功能的類,然後到處使用)
自己造輪子不僅重複了前人的工作,還做的沒別人好... ,不讓Spring幫忙管理就享受不到@Cacheable這些註解等一系列福利
對於管理,擴充套件,使用方便程度都不友好
結論:不能放棄別人寫好的工具類(我用Redis做快取,那麼對應的就是RedisCache、RedisManager和SpringCache註解等一套要用上)
坑二:Cache的設計思想不對(最重要)
在瞭解了SpringCache後,我十分愉快的用上了RedisCache和RedisCacheManager,真的十分簡單方便
但跟看這邊文章的你們一樣,不滿足於此,想著如果一個頻繁訪問快取,到時候過期一個或多個過期了,是不是就快取雪崩了
可當時我糾結的粗粒度太細了:
我希望每個快取裡的每個資料都能控制過期時間
比如:CacheName為systemCache的Cache裡有a,b兩個資料,我希望a資料5分鐘過期,b資料10分鐘過期
結論:這是完全沒必要的,我們控制過期時間,應該以Cache為最小單位,而不是以裡面單個數據
實際中快取資料是不需要精細到單獨處理的,都是一組一組的,如這幾個資料在30分鐘內失效,那一組資料是在
1小時內失效等等
例如:systemCache的ttl(詳見1.2的CacheConfig)設定為半小時,那麼它裡面所有的資料都為離存入時間間隔30分鐘後過期
我希望資料能純自動重新整理(不需要外在的觸發條件)
比如:跑個執行緒,隔斷時間自動掃描資料,進行純自動更新
結論:目前沒辦法實現快取純自動更新,必須要使用到該快取拿資料才能觸發更新檢查
純自動更新沒有意義,假設一個數據放了半小時沒人訪問要過期了,那就過期吧
因為快取前提是一段時間頻繁訪問的資料,如果都沒人訪問了,就不能稱之為快取
不然就是一個系統長期存在的動態變數,不適用於快取
坑三:對@Cacheble的理解太淺
於是想快取資料能在過期前的幾分鐘裡自動重新整理一下,那就很不錯
著手實現就想攔截@Cacheble,因為我們把@Cacheble放在訪問資料庫的方法上,那麼做個切面針對@Cacheble,在呼叫目標方法前判斷一下儲存的時間,快過期就重新取資料,不過期就不執行方法不就行了(不得不吐槽SpringCache對於過期設計有點考慮不足,封裝的死死的,沒向外暴露任何介面)
結果@Cacheble的代理類的邏輯是這樣的:
發現系統需要此快取資料 -> 自動嘗試get方法獲得快取 -> 存在則返回
發現系統需要此快取資料 -> 自動嘗試get方法獲得快取 -> 不存在才呼叫目標方法
所以切面切@Cacheble壓根沒用,別人是在快取失效的情況下才進入目標方法,這個過程才會被你寫的切面切!!
我的設計
網上有個比較好的自動重新整理的實現(參考):https://www.jianshu.com/p/275cb42080d9 但是不太喜歡
原因主要是不喜歡在@Cacheable裡面的變數做文章(會對原來已有的註解有影響),關鍵還會覆蓋,以第一個
@Cacheble寫的時間為準,程式碼開發一段時間,天知道這個Cache哪個地方第一次指定
在這闡述下設計邏輯,大家看看下面內容不懂的時候可以回來這裡看看
[中括號為涉及到的類]
涉及到如下8個類:
系統更新快取的註解:
@UpdataCache:是快取自動更新的標誌,在Cache的get方法上表明,然後每次get資料時就會在切面判斷是否快要過期
系統快取管理器的介面:
I_SystemCacheMgr:此介面繼承CacheManager,自定義快取管理器需要實現此介面,需要實現裡面一些更新快取相關的方法
Spring中的Cache介面和CacheManager的實現:
RedisCacheEnhance:繼承RedisCache,對其增強
RedisCacheMgr:繼承RedisManager,對其增強(說白了就是增加些自己的方法,改寫方法)
系統快取管理器的註冊類(向Spring註冊):
CacheConfig:Spring初始化時,向其註冊管理類,裡面寫自己實現的註冊邏輯
目標方法記載類:
CacheInvocation:為了能自動更新,那目標獲得資料的方法要記錄下來,才能要呼叫的時候主動呼叫
系統更新快取的執行緒:
UpdateDataTask:實現Callable介面的執行緒類,負責資料更新時執行目標方法,寫入快取
系統快取管理:
SystemCacheMgr:快取資料儲存資訊在此儲存,也負責管理I_SystemCacheMgr的實現類,進行更新操作的呼叫
系統快取AOP切面:
CacheAspect:對@Cacheable攔截,進行獲取資料的方法註冊。對@UpdateCache註解進行攔截,進行自動更新判
斷
接下來將依次展示程式碼,說下關鍵點
程式碼展示
@UpdataCache
該註解主要是對Cache的get方法進行標記,然後用AOP切面進行更新檢查
1 /** 2 * @author NiceBin 3 * @description: 快取更新介面,在Cache實現類的get方法上註解即可 4 * @date 2019/11/18 8:56 5 */ 6 @Target({ElementType.METHOD, ElementType.TYPE}) 7 @Retention(RetentionPolicy.RUNTIME) 8 @Inherited 9 public @interface UpdateCache { 10 }
I_SystemCacheMgr
主要是規定了系統快取管理器應該有的行為
1 /** 2 * 本系統的快取介面,SystemCacheMgr統一儲存資料記錄的時間和控制快取自動重新整理流程 3 * 4 * 為了實現資料快過期前的自動重新整理,需要以下操作: 5 * 1.實現此介面 6 * 如果用如RedisCacheManager這種寫好的類,需要子類繼承再實現此介面 7 * 如果Cache是CacheManager內部生成的,還需要重寫createCache方法 8 * 使生成的Cache走一遍Spring初始化Bean的過程,交給Spring管理 9 * 這裡主要為了Spring幫忙生成代理類,讓註解生效 10 * 2.實現了 {@link Cache} 介面的類在get方法上加上註解 {@link UpdateCache} 才有更新效果,所以如果要用如RedisCache 11 * 這種寫好的類,需要子類繼承,並重寫get方法 12 * 然後在get方法上加@UpdateCache 13 */ 14 public interface I_SystemCacheMgr extends CacheManager{ 15 /** 16 * 該資料是否過期 17 * true為已經過期 18 * @param cacheName 快取名字 19 * @param id 資料id 20 * @param saveTime 該快取內該資料的儲存時間 21 * @return 22 * @throws Exception 23 */ 24 boolean isApproachExpire(String cacheName, Object id, Timestamp saveTime) throws Exception; 25 26 /** 27 * 刪除指定Cache裡的指定資料 28 * @param cacheName 29 * @param id 30 * @throws Exception 31 */ 32 void remove(String cacheName, Object id) throws Exception; 33 34 /** 35 * 清除所有快取內容 36 * @throws Exception 37 */ 38 void clearAll() throws Exception; 39 40 /** 41 * 獲得所有的Cache 42 * @return 43 */ 44 ConcurrentMap<String, Cache> getAllCaches(); 45 }
RedisCacheEnhance
寫上@UpdateCache後,才能被AOP切入
1 /** 2 * @author NiceBin 3 * @description: 增強RedisCache 4 * 為了能在get方法寫上@Update註解,實現自動重新整理 5 * @date 2019/7/4 13:24 6 */ 7 public class RedisCacheEnhance extends RedisCache { 8 9 /** 10 * Create new {@link RedisCacheEnhance}. 11 * 12 * @param name must not be {@literal null}. 13 * @param cacheWriter must not be {@literal null}. 14 * @param cacheConfig must not be {@literal null}. 15 */ 16 protected RedisCacheEnhance(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) { 17 super(name, cacheWriter, cacheConfig); 18 } 19 20 @UpdateCache 21 public ValueWrapper get(Object key){ 22 System.out.println("進入get方法"); 23 return super.get(key); 24 } 25 26 @UpdateCache 27 public <T> T get(Object key, @Nullable Class<T> type){ 28 return super.get(key,type); 29 } 30 31 @UpdateCache 32 public <T> T get(Object key, Callable<T> valueLoader){ 33 return super.get(key,valueLoader); 34 }
RedisCacheMgr
RedisManager的增強類,這裡涉及的知識點比較多,跟大家簡單聊聊
1 /** 2 * @author NiceBin 3 * @description: RedisCacheManager增強類,為了實現本系統快取自動更新功能 4 * @date 2019/11/25 9:07 5 */ 6 public class RedisCacheMgr extends RedisCacheManager implements I_SystemCacheMgr { 7 8 private final RedisCacheWriter cacheWriter; 9 private ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>(); 10 11 private DefaultListableBeanFactory defaultListableBeanFactory; 12 13 public RedisCacheMgr(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) { 14 super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation); 15 this.cacheWriter = cacheWriter; 16 17 } 18 19 /** 20 * 重寫createRedisCache的方法,生成自己定義的Cache 21 * 這裡主要要讓Spring來生成代理Cache,不然在Cache上的註解是無效的 22 * @param name 23 * @param cacheConfig 24 * @return 25 */ 26 @Override 27 protected RedisCacheEnhance createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { 28 //利用Spring生成代理Cache 29 BeanDefinition beanDefinition = new RootBeanDefinition(RedisCacheEnhance.class); 30 //因為只有有參構造方法,所以要新增引數 31 ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues(); 32 constructorArgumentValues.addIndexedArgumentValue(0,name); 33 constructorArgumentValues.addIndexedArgumentValue(1,cacheWriter); 34 constructorArgumentValues.addIndexedArgumentValue(2,cacheConfig); 35 36 //如果有屬性需要設定,還能這樣做,不過需要有對應屬性名的set方法 37 //definition.getPropertyValues().add("propertyName", beanDefinition.getBeanClassName()); 38 39 ApplicationContext applicationContext = SystemContext.getSystemContext() 40 .getApplicationContext(); 41 //需要這樣獲取的DefaultListableBeanFactory類才能走一遍完整的Bean初始化流程!! 42 //像applicationContext.getBean(DefaultListableBeanFactory.class)都不好使!! 43 DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)applicationContext.getAutowireCapableBeanFactory(); 44 defaultListableBeanFactory.registerBeanDefinition(name,beanDefinition); 45 46 RedisCacheEnhance redisCacheEnhance = (RedisCacheEnhance)applicationContext.getBean(name); 47 caches.put(name, redisCacheEnhance); 48 return redisCacheEnhance; 49 } 50 51 /** 52 * 過期規則為:快取有效時間-(目前時間-記錄時間)<= 隨機時間 53 * 隨機時間是防止同一時刻過期時間太多,造成快取雪崩,在SystemStaticValue中快取項裡配置 54 * true為將要過期(可以重新整理了) 55 * 56 * @param cacheName 快取名稱 57 * @param id 資料id 58 * @param saveTime 儲存時間 59 * @return 60 */ 61 @Override 62 public boolean isApproachExpire(String cacheName, Object id, Timestamp saveTime) throws NoSuchAlgorithmException { 63 long ttl = -1; 64 65 RedisCacheConfiguration configuration = this.getCacheConfigurations().get(cacheName); 66 ttl = configuration.getTtl().getSeconds(); 67 68 if (ttl != -1 && saveTime!=null) { 69 int random = Tool.getSecureRandom(SystemStaticValue.CACHE_MIN_EXPIRE, SystemStaticValue.CACHE_MAX_EXPIRE); 70 Date date = new Date(); 71 long theNowTime = date.getTime() / 1000; 72 long theSaveTime = saveTime.getTime() / 1000; 73 if (ttl - (theNowTime - theSaveTime) <= random) { 74 return true; 75 } 76 } 77 return false; 78 } 79 80 @Override 81 public void remove(String cacheName, Object id) throws Exception { 82 Cache cache = this.getCache(cacheName); 83 cache.evict(id); 84 } 85 86 87 /** 88 * 清除所有快取內容 89 * 90 * @throws Exception 91 */ 92 @Override 93 public void clearAll() throws Exception { 94 Collection<String> cacheNames = this.getCacheNames(); 95 Iterator<String> iterator = cacheNames.iterator(); 96 while (iterator.hasNext()) { 97 String cacheName = iterator.next(); 98 Cache redisCache = this.getCache(cacheName); 99 redisCache.clear(); 100 } 101 } 102 103 @Override 104 public ConcurrentMap<String, Cache> getAllCaches() { 105 return caches; 106 } 107 }
知識點:如何閱讀原始碼來幫助自己註冊目標類
這是個很關鍵的點,我們想繼承RedisManager,那建構函式肯定要super父類的建構函式(而且RedisManager看設計並不太推薦讓我們繼承它的)
所以父類建構函式的引數,我們怎麼獲取,怎麼模擬就是關鍵性問題
第一步:百度,繼承RedisManager怎麼寫
不過這類不熱門的問題,大多數沒完美答案(就是能針對你的問題),可是有很多擦邊答案可以給你借鑑,我獲取到這樣的資訊
1 RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) 2 .cacheDefaults(defaultCacheConfig) // 預設配置(強烈建議配置上)。 比如動態創建出來的都會走此預設配置 3 .withInitialCacheConfigurations(initialCacheConfiguration) // 不同cache的個性化配置 4 .build();
如果我們想配個性化的RedisCacheManager,可以這樣建立
可以發現,這個build()方法就是我們的入手點,我們跟進去看看它的引數有什麼
1 /** 2 * Create new instance of {@link RedisCacheManager} with configuration options applied. 3 * 4 * @return new instance of {@link RedisCacheManager}. 5 */ 6 public RedisCacheManager build() { 7 8 RedisCacheManager cm = new RedisCacheManager(cacheWriter, defaultCacheConfiguration, initialCaches, 9 allowInFlightCacheCreation); 10 11 cm.setTransactionAware(enableTransactions); 12 13 return cm; 14 }
繼續跟蹤看RedisCacheManager的方法
1 public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, 2 Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) { 3 4 this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation); 5 6 Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null!"); 7 8 this.initialCacheConfiguration.putAll(initialCacheConfigurations); 9 }
這裡可發現defaultCacheConfiguration和initialCacheConfigurations是我們傳入的引數,allowInFlightCacheCreation就是簡單的布林值,能不能動態建立Cache而已
所以我們想辦法得到RedisCacheWriter這就大功告成了呀,怎麼找,Ctrl+F,搜尋變數,如圖:
一個個查詢,看cacheWriter是哪裡賦值進來的,最後發現
1 private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) { 2 this.cacheWriter = cacheWriter; 3 }
然後繼續搜尋RedisCacheManagerBuilder哪裡被呼叫:
重複以上步驟,有變數就搜尋變數,有方法就搜尋呼叫的地方,最後發現
1 public static RedisCacheManagerBuilder fromConnectionFactory(RedisConnectionFactory connectionFactory) { 2 3 Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); 4 5 return builder(new DefaultRedisCacheWriter(connectionFactory)); 6 }
看第5行,我只要有了RedisConnectionFactory,直接new一個就行(事實真的如此嗎),進去後發現
這個類不是public,外部是不允許new的,兄弟,還得繼續跟程式碼呀,這個類不是public,所以再看這個類已經無意義了,我們發現它實現了RedisCacheWriter介面,應該從這入手看看
1 public interface RedisCacheWriter { 2 3 /** 4 * Create new {@link RedisCacheWriter} without locking behavior. 5 * 6 * @param connectionFactory must not be {@literal null}. 7 * @return new instance of {@link DefaultRedisCacheWriter}. 8 */ 9 static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { 10 11 Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); 12 13 return new DefaultRedisCacheWriter(connectionFactory); 14 } 15 16 /** 17 * Create new {@link RedisCacheWriter} with locking behavior. 18 * 19 * @param connectionFactory must not be {@literal null}. 20 * @return new instance of {@link DefaultRedisCacheWriter}. 21 */ 22 static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { 23 24 Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); 25 26 return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50)); 27 }
到這問題就徹底解決了,介面定義了兩個獲取RedisCacheWriter的方法,只需要傳引數RedisConnectionFactory即可,而這個類Spring會自動配置(具體Spring中如何配置Redis自行百度,十分簡單)
至此super父類所需要的引數,我們都能自己構造了
這個知識點主要是想讓大家遇到問題有這個最基本的解決的思路,迎難而上~
知識點:用程式碼動態向Spring註冊Bean
RedisCacheMgr的createRedisCache方法中看到,我們生成的Cache需要像Spring註冊,這是為什麼呢
因為我們要想@UpdateCache註解,那必須得生成代理類,交給Spring管理,否則註解無效的
具體註冊我也沒深入研究(今後會寫一篇此博文),不過要按照這種方式註冊才有效
1 ApplicationContext applicationContext = SystemContext.getSystemContext() 2 .getApplicationContext(); 3 //需要這樣獲取的DefaultListableBeanFactory類才能走一遍完整的Bean初始化流程!! 4 //像applicationContext.getBean(DefaultListableBeanFactory.class)都不好使!! 5 DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)applicationContext.getAutowireCapableBeanFactory(); 6 defaultListableBeanFactory.registerBeanDefinition(name,beanDefinition);
CacheConfig
這個類是為了載入自定義的CacheManager
1 /** 2 * @author NiceBin 3 * @description: CacheManager初始化 4 * 目前系統只用一個Manager,使用RedisCacheManager 5 * 根據SystemStaticValue中的SystemCache列舉內容進行Cache的註冊 6 * 配置啟動前需要DefaultListableBeanFactory.class先載入完成 7 * 不然CacheManager或者Cache想用的時候會報錯 8 * @date 2019/11/13 17:02 9 */ 10 @Configuration 11 @Import(DefaultListableBeanFactory.class) 12 public class CacheConfig { 13 14 @Autowired 15 RedisConnectionFactory redisConnectionFactory; 16 17 @Bean 18 public RedisCacheMgr cacheManager() { 19 20 //建立Json自定義序列化器 21 FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); 22 //包裝成SerializationPair型別 23 RedisSerializationContext.SerializationPair serializationPair = RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer); 24 25 RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() 26 .entryTtl(Duration.ofDays(1)) 27 .computePrefixWith(cacheName -> "Cache"+cacheName); 28 // 針對不同cacheName,設定不同的過期時間,用了雙括號初始化方法~ 29 Map<String, RedisCacheConfiguration> initialCacheConfiguration = new HashMap<String, RedisCacheConfiguration>() {{ 30 SystemStaticValue.SystemCache[] systemCaches = SystemStaticValue.SystemCache.values(); 31 Arrays.asList(systemCaches).forEach((systemCache)-> 32 put(systemCache.getCacheName(),RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(systemCache.getSurviveTime())) 33 .serializeValuesWith(serializationPair))); 34 }}; 35 RedisCacheMgr redisCacheMgr = new RedisCacheMgr(RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory),defaultCacheConfig,initialCacheConfiguration,true); 36 37 //設定白名單---非常重要******** 38 /* 39 使用fastjson的時候:序列化時將class資訊寫入,反解析的時候, 40 fastjson預設情況下會開啟autoType的檢查,相當於一個白名單檢查, 41 如果序列化資訊中的類路徑不在autoType中,autoType會預設開啟 42 反解析就會報com.alibaba.fastjson.JSONException: autoType is not support的異常 43 */ 44 ParserConfig.getGlobalInstance().addAccept("com.tophousekeeper"); 45 return redisCacheMgr; 46 } 47 }
自定義JSON序列化類
1 /* 2 要實現物件的快取,定義自己的序列化和反序列化器。使用阿里的fastjson來實現的方便多。 3 */ 4 public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { 5 private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 6 private Class<T> clazz; 7 8 public FastJsonRedisSerializer(Class<T> clazz) { 9 super(); 10 this.clazz = clazz; 11 } 12 13 @Override 14 public byte[] serialize(T t) throws SerializationException { 15 if (null == t) { 16 return new byte[0]; 17 } 18 return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); 19 } 20 21 @Override 22 public T deserialize(byte[] bytes) throws SerializationException { 23 if (null == bytes || bytes.length <= 0) { 24 return null; 25 } 26 String str = new String(bytes, DEFAULT_CHARSET); 27 return (T) JSON.parseObject(str, clazz); 28 } 29 }
29-34行就是根據配置載入系統預設的幾個快取(涉及到Lambda表示式的迴圈知識)
1 public class SystemStaticValue { 2 ..... 3 //以下為快取資訊的配置(CACHE開頭)-------------------------------------------------------- 4 //系統快取名稱及過期時間(秒) 5 public enum SystemCache{ 6 //每日快取,有效時間24小時 7 DAY("dailyCache",60), 8 //半日快取,有效時間12小時 9 HALF_DAY("halfDayCache",12*60*60), 10 //1小時快取 11 ONE_HOUR("oneHour",1*60*60), 12 //半小時快取 13 HALF_HOUR("halfHour",30*60); 14 private String cacheName; 15 private long surviveTime; 16 17 SystemCache(String cacheName,long surviveTime){ 18 this.cacheName = cacheName; 19 this.surviveTime = surviveTime; 20 } 21 22 public String getCacheName() { 23 return cacheName; 24 } 25 26 public void setCacheName(String cacheName) { 27 this.cacheName = cacheName; 28 } 29 30 public long getSurviveTime() { 31 return surviveTime; 32 } 33 34 public void setSurviveTime(long surviveTime) { 35 this.surviveTime = surviveTime; 36 } 37 } 38 }
知識點:@Import的重要性
在35行,建立RedisCacheMgr 的時候,就會呼叫裡面的CreateCache的方法,裡面會把Cache向Spring註冊,需要用到DefaultListableBeanFactory類
所以在這裡必須要@import,保證其已經載入,不然到時候建立會報類不存在
知識點:自定義序列化
因為不自定義成JSON格式序列化,那麼存在Redis的內容不可直觀的看出來(都是亂七八糟的東西,不知道存的對不對),所以在21-23行要換成JSON序列化格式
大家有興趣可以看下這篇博文:https://blog.csdn.net/u010928589/article/details/84313987 這是一篇說Redis序列化如何自定義的思考過程,跟我上面的RedisCacheMgr實現思想類似
知識點:Lambda表示式
之前我也覺得不好用,不便於理解(其實就是我不會),然後這次下定決心弄懂,發現還是很不錯的(真香),給大家推薦這篇博文理解:https://blog.csdn.net/qq_25955145/article/details/82670160
CacheInvocation:
這個類主要是為了記錄呼叫的獲得快取資料的方法資訊,以便於自動更新時主動呼叫(下面這個Task就用到了)
1 /** 2 * @author NiceBin 3 * @description: 記錄被 {@link Cacheable} 註解過的方法資訊,為了主動更新快取去呼叫對應方法 4 * @date 2019/11/26 16:28 5 */ 6 public class CacheInvocation { 7 private Object key; 8 private final Object targetBean; 9 private final Method targetMethod; 10 private Object[] arguments; 11 12 public CacheInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) { 13 this.key = key; 14 this.targetBean = targetBean; 15 this.targetMethod = targetMethod; 16 //反射時不用檢查修飾符,略微提高效能 17 this.targetMethod.setAccessible(true); 18 if (arguments != null && arguments.length != 0) { 19 this.arguments = Arrays.copyOf(arguments, arguments.length); 20 } 21 } 22 23 public Object[] getArguments() { 24 return arguments; 25 } 26 27 public Object getTargetBean() { 28 return targetBean; 29 } 30 31 public Method getTargetMethod() { 32 return targetMethod; 33 } 34 35 public Object getKey() { 36 return key; 37 } 38 }
UpdateDataTask:
這裡就是主動更新資料的地方啦
1 /** 2 * @author NiceBin 3 * @description: 重新整理快取某個資料的任務 4 * @date 2019/11/29 15:29 5 */ 6 public class UpdateDataTask implements Callable { 7 //將要執行的方法資訊 8 private CacheInvocation cacheInvocation; 9 //對應要操作的快取 10 private Cache cache; 11 //對應要更新的資料id 12 private Object id; 13 14 /** 15 * 初始化任務 16 * @param cacheInvocation 17 * @param cache 18 * @param id 19 */ 20 public UpdateDataTask(CacheInvocation cacheInvocation,Cache cache,Object id){ 21 this.cacheInvocation = cacheInvocation; 22 this.cache = cache; 23 this.id = id; 24 } 25 26 @Override 27 public Object call() throws Exception { 28 if(cacheInvocation == null){ 29 throw new SystemException(SystemStaticValue.CACHE_EXCEPTION_CODE,"更新資料執行緒方法資訊不能為null"); 30 } 31 cache.put(id,methodInvoke()); 32 return true; 33 } 34 35 /** 36 * 代理方法的呼叫 37 * @return 38 */ 39 private Object methodInvoke() throws Exception{ 40 MethodInvoker methodInvoker = new MethodInvoker(); 41 methodInvoker.setArguments(cacheInvocation.getArguments()); 42 methodInvoker.setTargetMethod(cacheInvocation.getTargetMethod().getName()); 43 methodInvoker.setTargetObject(cacheInvocation.getTargetBean()); 44 methodInvoker.prepare(); 45 return methodInvoker.invoke(); 46 } 47 }
SystemCacheMgr:
系統快取管理的核心類,統籌全域性
1 /** 2 * @author NiceBin 3 * @description: 本系統的快取管理器 4 * 資料自動重新整理功能,要配合 {@link UpdateCache}才能實現 5 * 6 * 目前沒辦法實現快取純自動更新,必須要使用到該快取拿資料進行觸發 7 * 純自動更新沒有意義,假設一個數據放了半小時沒人訪問要過期了,那就過期吧 8 * 因為快取前提是一段時間頻繁訪問的資料,如果都沒人訪問了,就不能稱之為快取 9 * 不然就是一個系統長期存在的動態變數,不適用於快取 10 * @date 2019/11/14 16:18 11 */ 12 @Component 13 public class SystemCacheMgr { 14 //目前系統只考慮一個CacheManager 15 //必須有一個I_SystemCache的實現類,多個實現類用@Primary註解,類似於Spring的快取管理器 16 @Autowired 17 private I_SystemCacheMgr defaultCacheMgr; 18 //系統的執行緒池類 19 @Autowired 20 private SystemThreadPool systemThreadPool; 21 //所有快取的所有資料記錄Map 22 //外部Map中,key為快取名稱,value為該快取內的資料儲存資訊Map 23 //內部Map中,key為資料的id,value為記錄該資料的儲存資訊 24 private ConcurrentHashMap<String, ConcurrentHashMap<Object, DataInfo>> dataInfoMaps = new ConcurrentHashMap<>(); 25 26 /** 27 * 儲存資訊內部類,用於記錄 28 * 獲取要呼叫獲取方法,因為加鎖了執行緒才安全 29 */ 30 class DataInfo { 31 //記錄該資料的時間 32 private Timestamp saveTime; 33 //獲得此資料的方法資訊 34 private CacheInvocation cacheInvocation; 35 //保證只有一個執行緒提前更新此資料 36 private ReentrantLock lock; 37 38 public synchronized void setSaveTime(Timestamp saveTime) { 39 this.saveTime = saveTime; 40 } 41 42 43 public synchronized void setCacheInvocation(CacheInvocation cacheInvocation) { 44 this.cacheInvocation = cacheInvocation; 45 } 46 47 public synchronized void setLock(ReentrantLock lock) { 48 this.lock = lock; 49 } 50 } 51 52 /** 53 * 獲得DataInfo類,如果為空則建立一個 54 * @param cacheName 55 * @param id 56 * @return 57 */ 58 private DataInfo getDataInfo(String cacheName, Object id) { 59 ConcurrentHashMap<Object, DataInfo> dateInfoMap = dataInfoMaps.get((cacheName)); 60 DataInfo dataInfo; 61 if (dateInfoMap == null) { 62 //簡單的鎖住了,因為建立這個物件挺快的 63 synchronized (this) { 64 //重新獲取一次進行判斷,因為dateInfoMap是區域性變數,不能保證同步 65 dateInfoMap = dataInfoMaps.get((cacheName)); 66 if (dateInfoMap == null) { 67 dateInfoMap = new ConcurrentHashMap<>(); 68 dataInfo = new DataInfo(); 69 dataInfo.setLock(new ReentrantLock(true)); 70 dateInfoMap.put(id, dataInfo); 71 dataInfoMaps.put(cacheName, dateInfoMap); 72 } 73 } 74 } 75 //這裡不能用else,因為多執行緒同時進入if,後面進的dataInfo會是null 76 dataInfo = dateInfoMap.get(id); 77 78 return dataInfo; 79 } 80 81 /** 82 * 為該資料放入快取的時間記錄 83 * 84 * @param id 資料id 85 */ 86 public void recordDataSaveTime(String cacheName, Object id) { 87 Date date = new Date(); 88 Timestamp nowtime = new Timestamp(date.getTime()); 89 DataInfo dataInfo = getDataInfo(cacheName, id); 90 dataInfo.setSaveTime(nowtime); 91 } 92 93 /** 94 * 記錄獲得此資料的方法資訊,為了主動更新快取時的呼叫 95 * 96 * @param cacheName 快取名稱 97 * @param id 資料id 98 * @param targetBean 目標類 99 * @param targetMethod 目標方法 100 * @param arguments 目標方法的引數 101 */ 102 public void recordCacheInvocation(String cacheName, String id, Object targetBean, Method targetMethod, Object[] arguments) { 103 DataInfo dataInfo = getDataInfo(cacheName, id); 104 CacheInvocation cacheInvocation = new CacheInvocation(id, targetBean, targetMethod, arguments); 105 //鎖在這方法裡面有 106 dataInfo.setCacheInvocation(cacheInvocation); 107 } 108 109 /** 110 * 資料自動重新整理功能,要配合 {@link UpdateCache}才能實現 111 * 原理:先判斷資料是否過期,如果資料過期則從快取刪除。 112 * 113 * @param cacheName 快取名稱 114 * @param id 資料id 115 * @return 116 */ 117 public void autoUpdate(String cacheName, Object id) throws Exception { 118 DataInfo dataInfo = getDataInfo(cacheName, id); 119 Cache cache = defaultCacheMgr.getCache(cacheName); 120 121 122 //如果沒有儲存的時間,說明該資料還從未載入過 123 if (dataInfo.saveTime == null) { 124 return; 125 } 126 if (defaultCacheMgr.isApproachExpire(cacheName, id, dataInfo.saveTime)) { 127 if (dataInfo.lock.tryLock()) { 128 //獲取鎖後再次判斷資料是否過期 129 if (defaultCacheMgr.isApproachExpire(cacheName, id, dataInfo.saveTime)) { 130 ThreadPoolExecutor threadPoolExecutor = systemThreadPool.getThreadPoolExecutor(); 131 UpdateDataTask updateDataTask = new UpdateDataTask(dataInfo.cacheInvocation, cache, id); 132 FutureTask futureTask = new FutureTask(updateDataTask); 133 134 try { 135 threadPoolExecutor.submit(futureTask); 136 futureTask.get(1, TimeUnit.MINUTES); 137 //如果上一步執行完成沒報錯,那麼重新記錄儲存時間 138 recordDataSaveTime(cacheName,id); 139 } catch (TimeoutException ex) { 140 //如果訪問資料庫超時 141 throw new SystemException(SystemStaticValue.CACHE_EXCEPTION_CODE, "系統繁忙,稍後再試"); 142 } catch (RejectedExecutionException ex) { 143 //如果被執行緒池拒絕了 144 throw new SystemException(SystemStaticValue.CACHE_EXCEPTION_CODE, "系統繁忙,稍後再試"); 145 } finally { 146 dataInfo.lock.unlock(); 147 } 148 } 149 } 150 } 151 } 152 153 /** 154 * 清除所有快取內容 155 */ 156 public void clearAll() throws Exception { 157 defaultCacheMgr.clearAll(); 158 } 159 160 //以下為Set和Get 161 public I_SystemCacheMgr getDefaultCacheMgr() { 162 return defaultCacheMgr; 163 } 164 165 public void setDefaultCacheMgr(I_SystemCacheMgr defaultCacheMgr) { 166 this.defaultCacheMgr = defaultCacheMgr; 167 } 168 169 public ConcurrentHashMap<String, ConcurrentHashMap<Object, DataInfo>> getDataInfoMaps() { 170 return dataInfoMaps; 171 } 172 173 public void setDataInfoMaps(ConcurrentHashMap<String, ConcurrentHashMap<Object, DataInfo>> dataInfoMaps) { 174 this.dataInfoMaps = dataInfoMaps; 175 } 176 }
知識點:再次說下介面的重要性
17行直接讓Spring注入實現了I_SystemCacheMgr的類,直接使用實現的方法而不用關心具體的實現細節(對於SystemCacheMgr類來說,你換了它的實現邏輯也絲毫不影響它原來的程式碼呼叫)
CacheAspect
CacheAspect是註冊和觸發更新的核心類,Tool是用到的工具類的方法
1 /** 2 * @author NiceBin 3 * @description: 處理快取註解的地方:包括@UpdateCache,@Cacheable 4 * 5 * @date 2019/11/18 14:57 6 */ 7 @Aspect 8 @Component 9 public class CacheAspect { 10 @Autowired 11 SystemCacheMgr systemCacheMgr; 12 13 /** 14 * 資料註冊到SystemCacheMgr 15 * 為資料自動更新做準備 16 */ 17 @Before("@annotation(org.springframework.cache.annotation.Cacheable)") 18 public void registerCache(JoinPoint joinPoint){ 19 System.out.println("攔截了@Cacheable"); 20 //獲取到該方法前的@Cacheable註解,來獲取CacheName和key的資訊 21 Method method = Tool.getSpecificMethod(joinPoint); 22 Cacheable cacleable = method.getAnnotation(Cacheable.class); 23 String[] cacheNames = cacleable.value()!=null?cacleable.value():cacleable.cacheNames(); 24 String theKey = cacleable.key(); 25 //取出來的字串是'key',需要去掉'' 26 String key = theKey.substring(1,theKey.length()-1); 27 Arrays.stream(cacheNames).forEach(cacheName ->{ 28 //記錄資料儲存時間 29 systemCacheMgr.recordDataSaveTime(cacheName,key); 30 //記錄資料對應的方法資訊 31 systemCacheMgr.recordCacheInvocation(cacheName,key,joinPoint.getTarget(),method,joinPoint.getArgs()); 32 }); 33 } 34 35 /** 36 * 檢測該鍵是否快過期了 37 * 如果快過期則進行自動更新 38 * @param joinPoint 39 */ 40 @Before(value = "@annotation(com.tophousekeeper.system.annotation.UpdateCache)&&args(id)") 41 public void checkExpire(JoinPoint joinPoint,String id) throws Exception { 42 System.out.println("攔截了@UpdateCache"); 43 RedisCacheEnhance redisCacheEnhance = (RedisCacheEnhance) joinPoint.getTarget(); 44 systemCacheMgr.autoUpdate(redisCacheEnhance.getName(),id); 45 } 46 }
1 public class Tool { 2 3 /** 4 * 獲得代理類方法中真實的方法 5 * 小知識: 6 * ClassUtils.getMostSpecificMethod(Method method, Class<?> targetClass) 7 * 該方法是一個有趣的方法,他能從代理物件上的一個方法,找到真實物件上對應的方法。 8 * 舉個例子,MyComponent代理之後的物件上的someLogic方法,肯定是屬於cglib代理之後的類上的method, 9 * 使用這個method是沒法去執行目標MyComponent的someLogic方法, 10 * 這種情況下,就可以使用getMostSpecificMethod, 11 * 找到真實物件上的someLogic方法,並執行真實方法 12 * 13 * BridgeMethodResolver.findBridgedMethod(Method bridgeMethod) 14 * 如果當前方法是一個泛型方法,則會找Class檔案中實際實現的方法 15 * @param poxyMethod 代理的方法 16 * @param targetclass 真實的目標類 17 * @return 18 */ 19 public static Method getSpecificMethod(Method poxyMethod,Class targetclass){ 20 Method specificMethod = ClassUtils.getMostSpecificMethod(poxyMethod,targetclass); 21 specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); 22 return specificMethod; 23 } 24 25 /** 26 * 獲得代理類方法中真實的方法 27 * 小知識: 28 * AopProxyUtils.ultimateTargetClass() 29 * 獲取一個代理物件的最終物件型別 30 * @param joinPoint 切面的切點類 31 * @return 32 */ 33 public static Method getSpecificMethod(JoinPoint joinPoint){ 34 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); 35 Method poxyMethod = methodSignature.getMethod(); 36 Class targetClass = AopProxyUtils.ultimateTargetClass(joinPoint.getTarget()); 37 return Tool.getSpecificMethod(poxyMethod,targetClass); 38 } 39 }
總結
整套流程在磕磕碰碰中弄出來了,最後簡單的用了jmeter測試了一下,感覺還不錯
收穫最多的不只是新知識的學習,更多是解決問題的能力,在碰到這種並不是很熱門的問題,網上的答案沒有完全針對你問的,只能從中獲取你需要的小部分知識(就像現在你看這篇博文一樣)
然後自己再嘗試拼湊起來,有些問題百度無果之後,不妨自己跟跟原始碼,或者搜尋XXX原始碼解析,看看流程,沒準就有新發現
有什麼想法或問題歡迎評論區留言,一起探討~
盡我最大努力分享給大家,歡迎大家轉載(寫作不易,請標明出處)或者給我點個贊呀(右下角),謝啦!<