Stories

Detail Return Return

藍易雲:Spring緩存是如何實現的?如何擴展使其支持過期刪除功能? - Stories Detail

下面把 <span style="color:red">Spring 緩存</span>的實現機制與“<span style="color:red">過期刪除(TTL/Expire)</span>”擴展路徑一次説清,並給出可直接落地的代碼與驗證方法 🔧⚡


1)結論先行(架構視角)

  • Spring 的緩存採用 <span style="color:red">AOP 攔截器</span> + <span style="color:red">Cache 抽象</span>:@Cacheable/@CachePut/@CacheEvictCacheInterceptorCacheManagerCache
  • <span style="color:red">TTL 不屬於抽象層通用能力</span>,而是由具體實現(如 Redis、Caffeine、JCache)提供;因此最佳實踐是在 CacheManager 層配置/注入 TTL,或用裝飾器增強本地緩存。
  • 企業級落地:對外使用 <span style="color:red">RedisCache(強一致 TTL)</span>,本地熱點用 <span style="color:red">Caffeine(近端極速回源)</span>,並在 <span style="color:red">CacheManager</span> 層按緩存名或註解元數據做 <span style="color:red">精細化 TTL</span> 管控。🙂

2)原理速覽(工作流程)

flowchart LR
A[方法調用] --> B{命中 @Cacheable?}
B -- 否 --> C[執行業務邏輯]
C --> D[返回結果]
B -- 是 --> E[CacheInterceptor 解析SpEL/Key]
E --> F[CacheManager 定位 Cache]
F --> G{Cache.get 命中?}
G -- 是 --> H[返回緩存值]
G -- 否 --> C
C --> I[Cache.put 寫入, 可能帶 TTL]
I --> H

解釋:攔截器按註解與 SpEL 計算 Key;CacheManager 決定具體 Cache;是否帶 <span style="color:red">TTL</span> 取決於底層實現或我們自定義的增強邏輯。


3)常見實現能力對比(便於選型)

維度 <span style="color:red">Caffeine</span> <span style="color:red">RedisCache</span> JCache(Ehcache等)
TTL/過期 支持 expireAfterWrite/Access 支持 entryTTL/逐鍵 TTL 通過 ExpiryPolicy
精細化 TTL 支持按 Cache 實例配置 支持全局/每 Cache 配置,亦可自定義解析 支持
一致性 進程內、極快但非共享 分佈式、強一致 TTL 視實現
適用場景 近端熱點、低延遲 共享會話、分佈式接口緩存 需要規範化標準接口時

4)三條擴展路徑(從易到難)

路線A:直接用支持 TTL 的底座(推薦)

A1. Caffeine(本地內存)

// 引入:Spring Boot 3.x(Spring 6.x)環境
@Bean
public CacheManager caffeineCacheManager() {
    CaffeineCache userCache = new CaffeineCache(
        "userCache",
        com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(java.time.Duration.ofMinutes(10)) // 設置寫入後過期
            .recordStats()
            .build()
    );
    SimpleCacheManager mgr = new SimpleCacheManager();
    mgr.setCaches(java.util.List.of(userCache));
    return mgr;
}

解釋:定義名為 userCache 的本地緩存,寫入後 <span style="color:red">10 分鐘過期</span>;expireAfterWrite 由 Caffeine 在訪問/維護時惰性清理,無需我們手刪。

A2. Redis(分佈式共享)

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
    // 全局默認 TTL 15 分鐘
    RedisCacheConfiguration defaultCfg = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(java.time.Duration.ofMinutes(15))
        .disableCachingNullValues();

    // 按緩存名提供差異化 TTL
    java.util.Map<String, RedisCacheConfiguration> cfgMap = new java.util.HashMap<>();
    cfgMap.put("userCache", defaultCfg.entryTtl(java.time.Duration.ofMinutes(5)));
    cfgMap.put("productCache", defaultCfg.entryTtl(java.time.Duration.ofHours(1)));

    return RedisCacheManager.builder(factory)
        .cacheDefaults(defaultCfg)
        .withInitialCacheConfigurations(cfgMap)
        .transactionAware()
        .build();
}

解釋RedisCacheManager 支持全局 TTL按緩存名 TTL;Redis 在服務器側執行強一致過期刪除


路線B:按“緩存名後綴”動態解析 TTL(零侵入註解)

// 支持 "cacheName#ttl=60s" 這種命名方式的 RedisCacheManager 定製
@Bean
public RedisCacheManager redisCacheManagerWithTtlParsing(RedisConnectionFactory f) {
    RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(java.time.Duration.ofMinutes(10));
    return new RedisCacheManager(
        new org.springframework.data.redis.cache.RedisCacheWriter.NonLockingRedisCacheWriter(f),
        base) {
        @Override
        protected org.springframework.data.redis.cache.RedisCache createRedisCache(
                String name, RedisCacheConfiguration cacheConfig) {
            // 解析自定義後綴
            java.time.Duration ttl = parseTtlFromName(name).orElse(cacheConfig.getTtl());
            return super.createRedisCache(stripTtlSuffix(name),
                    cacheConfig.entryTtl(ttl));
        }
        private java.util.Optional<java.time.Duration> parseTtlFromName(String name) {
            // 例:userCache#ttl=45s
            int i = name.indexOf("#ttl=");
            if (i > 0) {
                String v = name.substring(i + 5);
                return java.util.Optional.of(java.time.Duration.parse("PT" + v.toUpperCase()));
                // 允許 45S/10M/1H(S/M/H),藉助 Duration 解析
            }
            return java.util.Optional.empty();
        }
        private String stripTtlSuffix(String name) {
            int i = name.indexOf("#ttl=");
            return i > 0 ? name.substring(0, i) : name;
        }
    };
}

解釋:不改動 @Cacheable 註解,只需把 value="userCache#ttl=45s"(示例)作為緩存名;管理器在創建 Cache 時解析 TTL;適合多團隊協作、治理成本低。<span style="color:red">重點</span>:約定統一、測試覆蓋到位。


路線C:為 ConcurrentMapCache 增強 TTL(裝飾器,適合無中間件場景)

public class ExpiringConcurrentMapCache implements org.springframework.cache.Cache {
    private final String name;
    private final java.util.concurrent.ConcurrentMap<Object, Entry> store = new java.util.concurrent.ConcurrentHashMap<>();
    private final java.time.Duration ttl;

    public ExpiringConcurrentMapCache(String name, java.time.Duration ttl) {
        this.name = name; this.ttl = ttl;
        // 週期性清理,避免堆積
        java.util.concurrent.Executors.newSingleThreadScheduledExecutor()
            .scheduleAtFixedRate(this::purge, 1, 1, java.util.concurrent.TimeUnit.MINUTES);
    }
    private static class Entry {
        final Object val; final long expireAt;
        Entry(Object v, long e) { this.val = v; this.expireAt = e; }
        boolean expired() { return System.currentTimeMillis() >= expireAt; }
    }
    @Override public String getName(){ return name; }
    @Override public Object getNativeCache(){ return store; }
    @Override public ValueWrapper get(Object key){
        Entry e = store.get(key);
        if(e==null || e.expired()){ store.remove(key); return null; }
        return () -> e.val;
    }
    @Override public <T> T get(Object key, Class<T> type){
        ValueWrapper v = get(key);
        return v==null? null : type.cast(v.get());
    }
    @Override public void put(Object key, Object value){
        store.put(key, new Entry(value, System.currentTimeMillis()+ttl.toMillis()));
    }
    @Override public void evict(Object key){ store.remove(key); }
    @Override public void clear(){ store.clear(); }
    private void purge(){
        long now = System.currentTimeMillis();
        store.forEach((k,e)->{ if(e.expired()) store.remove(k); });
    }
}

解釋

  • 這是一個兼容 Spring Cache 接口的內存緩存,put 時寫入 expireAtget 時惰性刪除,後台每分鐘清一次理;
  • <span style="color:red">注意</span>:此實現適合單進程測試/輕量場景,不保證分佈式一致性;生產應優先 Redis/Caffeine。

配置注入

@Bean
public CacheManager localTtlCacheManager() {
    SimpleCacheManager mgr = new SimpleCacheManager();
    mgr.setCaches(java.util.List.of(
        new ExpiringConcurrentMapCache("demoCache", java.time.Duration.ofSeconds(30))
    ));
    return mgr;
}

解釋:將自定義的 ExpiringConcurrentMapCache 註冊為 Spring 管理的 CachedemoCache 的 TTL 為 <span style="color:red">30 秒</span>。


5)註解使用與驗證

@Cacheable(cacheNames = "userCache#ttl=45s", key = "#id") // 路線B:按名字解析TTL
public UserDTO getUser(long id) {
    // 第一次調用會走數據庫,後續45秒命中緩存
    ...
}

解釋:通過命名約定,把 <span style="color:red">TTL 策略</span>帶到 CacheManager;使用 Caffeine/Redis 的原生過期能力完成“過期刪除”。


6)運維與風控要點(務實清單)

  • <span style="color:red">分層策略</span>:讀多寫少 → TTL 長;強一致讀 → TTL 短或禁用緩存。
  • <span style="color:red">雪崩治理</span>:TTL 加隨機抖動(±10%);熱點 key 加互斥回源。
  • <span style="color:red">觀測可視化</span>:埋點命中率、回源時延、逐 cache 的 key 數與 TTL 分佈,作為 SLO 指標。
  • <span style="color:red">應急</span>:支持運維開關 <span style="color:red">全局禁用某 Cache</span> 與批量失效能力。

7)命中路徑與擴展方式對比(表格版)

場景 推薦實現 過期方式 優點 風險點
分佈式接口緩存 <span style="color:red">RedisCache</span> + 每 cache TTL 服務器端到期即刪 一致性強、治理清晰 需 Redis 可用性
近端熱點 <span style="color:red">Caffeine</span> expireAfterWrite/Access 惰性清理+維護 極低延遲 進程內不共享
純本地測試 自定義 <span style="color:red">ExpiringConcurrentMapCache</span> 定時 + 惰性 輕量零依賴 非分佈式

收官:<span style="color:red">Spring 緩存</span>的核心是抽象與攔截器,<span style="color:red">過期刪除</span>應交由底層實現或通過 <span style="color:red">CacheManager 裝飾</span>實現“策略外置、統一治理”。上線前做命中率/回源壓測雪崩演練,才能在高併發與抖動網絡下穩住體驗 🚀

user avatar san-mu Avatar wuliaodechaye Avatar zhuweitao Avatar gvison Avatar womaxuanhuang Avatar hsr2022 Avatar
Favorites 6 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.