切換語言為:簡體

資深程式設計師對於快取操作的優雅封裝方法

  • 爱糖宝
  • 2024-07-07
  • 2064
  • 0
  • 0

快取大家再熟悉不過了,幾乎是現在任何系統的標配,並引申出來很多的問題:快取穿透、快取擊穿、快取雪崩.......哎,作為天天敲業務程式碼的人,哪有時間天天考慮這麼多的破事。直接封裝一個東西,我們直接拿來就用豈不是美哉。看了專案組的程式碼,我也忍不住 diy 了,對於增刪就算了,就是 get set 的 API 呼叫,修改?直接刪了重新新增吧,哪有先查快取再去修改儲存的。難點就在於快取的查詢,要不快取的穿透、擊穿、雪崩會誕生對吧。 我們先看下快取的邏輯:是的,其實就是這麼簡單,剩下的就是考慮一下快取穿透問題,最常見的處理方式就是加鎖。這裏我採用的是訊號量 Semaphore。 好的,現在展示我的程式碼,程式碼結構如下:

 .
 ├── CacheEnum.java                  -- 快取列舉
 ├── CacheLoader.java                -- 快取載入介面
 ├── CacheService.java               -- 快取服務
 └── CacheServiceImpl.java           -- 快取服務實現類
 
 1 directory, 4 files

ok,現在我們一一講解下:

CacheEnum

主要的程式碼:

 public enum CacheEnum {
                        /** 使用者token快取 */
                        USER_TOKEN("USER_TOKEN", 60, "使用者token"),
                        /** 使用者資訊快取 */
                        USER_INFO("USER_INFO", 60, "使用者資訊"),;
 
     /** 快取字首 */
     private final String  cacheName;
     /** 快取過期時間 */
     private final Integer expire;
     /** 快取描述 */
     private final String  desc;

其他的就是 get/set 方法,這裏不做展示。主要解決的痛點就是快取過期時間的統一管理、快取名稱的統一管理。

CacheService

這裏邊就是定義了快取操作的介面:

 public interface CacheService {
     
     /**
      * 獲取快取
      * @param cacheName 快取名稱
      * @param key 快取key
      * @param type 快取型別
      * @return 快取值
      * @param <T> 快取型別
      */
     <T> T get(String cacheName, String key, Class<T> type);
 
     /**
      * 獲取快取
      * @param cacheName 快取名稱
      * @param key 快取key
      * @param type 快取型別
      * @param loader 快取載入器
      * @return 快取值
      * @param <T> 快取型別
      */
     <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> loader);
 
     /**
      * 刪除快取
      * @param cacheName 快取名稱
      * @param key 快取key
      */
     void delete(String cacheName, String key);
 
     /**
      * 設定快取
      * @param cacheName 快取名稱
      * @param key 快取key
      * @param value 快取值
      */
     void set(String cacheName, String key, Object value);
 }

其實就是一些增刪查的方法。只不過這裏我們更加關注的是:快取的名稱,快取的 key,快取物件,快取物件的型別。 在 22 行這裏用到了CacheLoader 介面,其實就是處理快取物件在快取中拿不到的問題,它的定義也很簡單:

 @FunctionalInterface
 public interface CacheLoader<V> {
     /**
      * 載入快取
      * @param key 快取key
      * @return 快取值
      */
     V load(String key);
 }

就一個方法,直接使用上 lambda 表示式,下邊的測試類會講到。

CacheServiceImpl

 @Slf4j
 @Service
 public class CacheServiceImpl implements CacheService {
 
     /** Semaphore */
     private static final Semaphore        CACHE_LOCK = new Semaphore(100);
 
     /** 快取操作 */
     @Autowired
     private RedisTemplate<String, Object> redisTemplate;
 
     /**
      * 獲取快取key
      * @param cacheName 快取名稱
      * @param key 快取key
      * @return 快取key
      */
     private String getCacheKey(String cacheName, String key) {
         Assert.isTrue(StrUtil.isNotBlank(cacheName), "cacheName不能為空");
         Assert.isTrue(StrUtil.isNotBlank(key), "key不能為空");
         Assert.isTrue(CacheEnum.getByCacheName(cacheName) != null, "需要使用CacheEnum列舉建立快取");
         return cacheName + ":" + key;
     }
 
     @Override
     public <T> T get(String cacheName, String key, Class<T> type) {
         Object value = redisTemplate.opsForValue().get(getCacheKey(cacheName, key));
         if (value != null) {
             return type.cast(value);
         }
         return null;
     }
 
     @Override
     public <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> cacheLoader) {
         try {
             // 獲取鎖, 防止快取擊穿
             CACHE_LOCK.acquire();
             String cacheKey = getCacheKey(cacheName, key);
             Object value = redisTemplate.opsForValue().get(cacheKey);
             if (value != null) {
                 return type.cast(value);
             }
             value = cacheLoader.load(cacheKey);
             if (value != null) {
                 this.set(cacheName, key, value);
                 return type.cast(value);
             }
         } catch (InterruptedException e) {
             log.warn("獲取鎖失敗");
         } finally {
             CACHE_LOCK.release();
         }
         return null;
     }
 
     @Override
     public void delete(String cacheName, String key) {
         redisTemplate.opsForValue().getOperations().delete(getCacheKey(cacheName, key));
     }
 
     @Override
     public void set(String cacheName, String key, Object value) {
         String cacheKey = getCacheKey(cacheName, key);
         CacheEnum cacheEnum = CacheEnum.getByCacheName(cacheName);
         Assert.isTrue(cacheEnum != null, "需要使用CacheEnum列舉建立快取");
         redisTemplate.opsForValue().set(cacheKey, value, cacheEnum.getExpire(), TimeUnit.SECONDS);
     }
 }

這裏就是介面的具體實現。需要注意:

  1. 在獲得完整的快取 key 的時候,我們其實對於快取的 cacheName 做了驗證,參見上程式碼塊 21 行,不允許自己定義快取的 cacheName,統一在列舉類中定義。

  2. 因為 tair 的資源有點不好申請,這裏使用的 redis 作為快取的工具,結合 spring-boot-starter-data-redis 作為操作的 API。

  3. 應對快取穿透問題,這裏使用的是Semaphore 訊號量。

別的就沒什麼好說的,現在我們來測試一下我們的封裝是否管用。

測試程式碼

設定快取

測試用定義的列舉類建立快取:

     @Test
     void set() {
         cacheService.set(CacheEnum.USER_INFO.getCacheName(), "10001", getFakeUser("10001"));
     }

是沒問題的,不用列舉類建立:

     @Test
     void testSetSelfDefinedCacheName() {
         cacheService.set("user", "10001", getFakeUser("10001"));
     }

直接異常出來了:

 java.lang.IllegalArgumentException: 需要使用CacheEnum列舉建立快取

讀取快取

讀取快取,我的 API 中分為兩種情況:直接讀取,沒有就算了;讀取快取,沒有的話再從 DB 中拿。對應的單測如下:

     @Test
     void testGet() {
         UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
             UserEntity.class);
         log.info("user: {}", user);
     }
 
     @Test
     void testGetWithCacheLoader() {
         UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
             UserEntity.class, new CacheLoader<UserEntity>() {
                 @Override
                 public UserEntity load(String key) {
                     return getFakeUser("10001");
                 }
             });
         log.info("user: {}", user);
     }
 
     @Test
     void testGetWithSimpledCacheLoader() {
         UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
             UserEntity.class, key -> getFakeUser(key));
         log.info("user: {}", user);
     }

第三種就是對於 lambda 介面的簡化寫法。 基於以上的方式,我們操作快取就變得更加容易了。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.