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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM