業務代碼中充斥著「先取緩存,無則查 DB 回寫」的樣板邏輯。本文展示如何利用 Spring AOP + 自定義註解 @ServiceCache,將橫切關注點抽離為可復用切面,實現聲明式緩存,並深度解析併發穿透防護(sync 鎖)、空值緩存防穿透、Key 生成策略等生產級細節。
public Merchant loadCachedMerchant(String merchantId) {
String key = createCacheKey(merchantId);
Merchant merchant = (Merchant) memCachedClient.get(key); // 查緩存
if (merchant == null) {
merchant = merchantDao.searchMerchantByMerchantId1(merchantId); // 查 DB
if (merchant != null) {
merchant = extraCacheMerchant(merchant);
memCachedClient.put(key, merchant, 5 * 60); // 回寫緩存
}
}
return merchant;
}
@ServiceCache(expire = 600, includeKeys = { "name" })
public CarProvinceEntity selectProvinceByName(String name) {
return commonDao.mapper(CarProvinceEntity.class).source(Source.SLAVE)
.sql("selectByName").session().selectOne(name);
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceCache {
int value() default -1; // 過期秒數(別名)
int expire() default 60; // 過期秒數
Key key() default Key.JSON; // Key 生成策略:JSON 序列化 / toString
String[] includeKeys() default {}; // 指定參與 Key 的參數名(需編譯時保留參數名 -parameters)
boolean sync() default false; // 是否啟用同步鎖防併發穿透
boolean nullPattern() default true; // 是否緩存空值(防緩存穿透)
enum Key { JSON, TO_STRING }
}
@Around("(@within(org.springframework.stereotype.Service) || " +
"@within(org.springframework.stereotype.Repository)) && " +
"@annotation(com.xxx.cache.annotation.ServiceCache)")
public Object cacheProcess(ProceedingJoinPoint jp) throws Throwable { ... }
cacheProcess(ProceedingJoinPoint)
├── 1. 解析 MethodSignature,獲取 @ServiceCache 屬性
├── 2. expire > 0 才啟用緩存,否則直接 proceed()
├── 3. 生成 cacheKey (ServiceCacheUtils.buildCacheKey)
├── 4. sync=true ? 加鎖同步執行 cacheInvoke : 直接執行
└── 5. cacheInvoke 核心邏輯:
├── 從 ICache get(cacheKey)
│ ├── 命中 → 返回 (Blank 佔位符處理 null)
│ └── 未命中 → proceed() 執行目標方法
│ ├── 結果為 null 且 nullPattern=true → 存入 Blank 佔位符
│ └── 結果非 null → 存入緩存,設置 expire
└── 返回結果
// 問題:高併發下緩存失效瞬間,大量請求同時穿透打爆 DB
// 方案 1:sync=true 粗粒度同步鎖(同一 JVM 內有效)
if (sc.sync()) {
synchronized (this.syncLock) { // 注意:單機鎖,分布式需改用 Redis 分布式鎖
rval = this.cacheInvoke(sc, jp, cacheKey, expire);
}
} else {
rval = this.cacheInvoke(sc, jp, cacheKey, expire);
}
// 方案 2:nullPattern=true 緩存空值(防緩存穿透攻擊)
rval = rval == null && sc.nullPattern() ? Blank.INST : rval;
if (rval != null) serviceCache.put(cacheKey, rval, expire);
生產建議:sync=true 僅適用單機/低併發;高併發分布式場景**必須**結合 Redis Lua 腳本或 Redisson 分布式鎖實現「查緩存 → 獲取分布式鎖 → 雙重檢查 → 查 DB → 回寫 → 釋放鎖」完整流程。
toString() 拼接,簡短但需保證業務唯一性。includeKeys = {"name"}),需編譯器開啟 -parameters 保留參數名。
private static class Blank implements Serializable {
private static final Blank INST = new Blank();
private Blank() {}
}
// 存入時:null → Blank.INST
// 取出時:Blank.INST → null 返回給調用者
return rval instanceof Blank ? null : rval;
避免「緩存無值」與「緩存有值但值為 null」語義混淆,防止緩存穿透。
同樣模式可快速實現:
從硬編碼到切面抽離,是從「寫代碼」到「設計框架」的關鍵躍遷。@ServiceCache 只是起點,掌握「註解定義語義 + 切面實現邏輯 + 工具類輔助」的組合拳,即可構建屬於團隊的基礎設施庫,讓業務開發回歸純粹。