SpringCache自定義過時時間及自動刷新

背景前提

閱讀說明(十分重要)

對於Cache和SpringCache原理不太清楚的朋友,能夠看我以前寫的文章:Springboot中的緩存Cache和CacheManager原理介紹html

能關注SpringCache,想了解過時實現和自動刷新的朋友,確定有必定Java基礎的,因此先了解個人思想,達成共識比直接看代碼重要許多redis

你可能只須要我裏面其中一個點而不是原搬照抄spring

我在實現過程遇到三大坑,先跟你們說下,興許對你有幫助數據庫

坑一:本身造輪子

對SpringCache不怎麼了解,直接百度緩存看到Redis後,就直接使用RedisTemple開始工具類的搭建(說白了就是本身擼一個增刪查改功能的類,而後處處使用)json

本身造輪子不只重複了前人的工做,還作的沒別人好... ,不讓Spring幫忙管理就享受不到@Cacheable這些註解等一系列福利緩存

對於管理,擴展,使用方便程度都不友好安全

結論:不能放棄別人寫好的工具類(我用Redis作緩存,那麼對應的就是RedisCache、RedisManager和SpringCache註解等一套要用上)多線程

坑二:Cache的設計思想不對(最重要)

在瞭解了SpringCache後,我十分愉快的用上了RedisCache和RedisCacheManager,真的十分簡單方便app

但跟看這邊文章的大家同樣,不知足於此,想着若是一個頻繁訪問緩存,到時候過時一個或多個過時了,是否是就緩存雪崩了dom

可當時我糾結的粗粒度太細了:


我但願每一個緩存裏的每一個數據都能控制過時時間

好比: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源碼解析,看看流程,沒準就有新發現

有什麼想法或問題歡迎評論區留言,一塊兒探討~

盡我最大努力分享給你們,歡迎你們轉載(寫做不易,請標明出處)或者給我點個贊呀(右下角),謝啦!

原文出處:https://www.cnblogs.com/top-housekeeper/p/11980973.html

相關文章
相關標籤/搜索