1. 程式人生 > >SpringCache自定義過期時間及自動重新整理

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原始碼解析,看看流程,沒準就有新發現

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

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