首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring AOP + 註解實現方法級緩存:從硬編碼到切面複用的完整實踐

     編輯:Java 綜合教學

前言:重複的「先查緩存,再查 DB」邏輯該如何優雅消除?

業務代碼中充斥著「先取緩存,無則查 DB 回寫」的樣板邏輯。本文展示如何利用 Spring AOP + 自定義註解 @ServiceCache,將橫切關注點抽離為可復用切面,實現聲明式緩存,並深度解析併發穿透防護(sync 鎖)、空值緩存防穿透、Key 生成策略等生產級細節。

一、 痛點代碼 vs 目標代碼

Before:硬編碼耦合業務邏輯

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;
}

After:聲明式一行搞定

@ServiceCache(expire = 600, includeKeys = { "name" })
public CarProvinceEntity selectProvinceByName(String name) {
    return commonDao.mapper(CarProvinceEntity.class).source(Source.SLAVE)
            .sql("selectByName").session().selectOne(name);
}

二、 核心註解設計:@ServiceCache

@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 }
}

三、 切面核心實現:ServiceCacheAnnotationAspect

3.1 切點表達式:精準匹配 Service/Repository 層帶註解方法

@Around("(@within(org.springframework.stereotype.Service) || " +
        "@within(org.springframework.stereotype.Repository)) && " +
        "@annotation(com.xxx.cache.annotation.ServiceCache)")
public Object cacheProcess(ProceedingJoinPoint jp) throws Throwable { ... }

3.2 核心邏輯流程圖解

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
     └── 返回結果

3.3 關鍵細節:併發穿透雙重防護


// 問題:高併發下緩存失效瞬間,大量請求同時穿透打爆 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 → 回寫 → 釋放鎖」完整流程。

四、 Key 生成策略:ServiceCacheUtils

  • JSON 模式:參數對象全量序列化為 JSON,Key 唯一性最強,但長度較長。
  • TO_STRING 模式:參數 toString() 拼接,簡短但需保證業務唯一性。
  • includeKeys:指定參數名參與 Key 計算(如 includeKeys = {"name"}),需編譯器開啟 -parameters 保留參數名。

五、 Blank 佔位符模式:優雅處理 null 值緩存


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」語義混淆,防止緩存穿透。

六、 擴展思路:AOP + 註解的更多應用場景

同樣模式可快速實現:

  • @LogExecutionTime:方法耗時統計、慢查詢告警
  • @Retry:失敗自動重試(指數退避、指定異常)
  • @RateLimit:令牌桶/漏桶限流
  • @Idempotent:冪等性校驗(Token + Redis SETNX)
  • @DataSource:動態數據源切換(主從分離)

結語

從硬編碼到切面抽離,是從「寫代碼」到「設計框架」的關鍵躍遷。@ServiceCache 只是起點,掌握「註解定義語義 + 切面實現邏輯 + 工具類輔助」的組合拳,即可構建屬於團隊的基礎設施庫,讓業務開發回歸純粹。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved