快取大家再熟悉不過了,幾乎是現在任何系統的標配,並引申出來很多的問題:快取穿透、快取擊穿、快取雪崩.......哎,作為天天敲業務程式碼的人,哪有時間天天考慮這麼多的破事。直接封裝一個東西,我們直接拿來就用豈不是美哉。看了專案組的程式碼,我也忍不住 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); } }
這裏就是介面的具體實現。需要注意:
在獲得完整的快取 key 的時候,我們其實對於快取的 cacheName 做了驗證,參見上程式碼塊 21 行,不允許自己定義快取的 cacheName,統一在列舉類中定義。
因為 tair 的資源有點不好申請,這裏使用的 redis 作為快取的工具,結合 spring-boot-starter-data-redis 作為操作的 API。
應對快取穿透問題,這裏使用的是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 介面的簡化寫法。 基於以上的方式,我們操作快取就變得更加容易了。