下面把 <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/@CacheEvict→CacheInterceptor→CacheManager→Cache。 - <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)原理速覽(工作流程)
解釋:攔截器按註解與 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時寫入expireAt,get時惰性刪除,後台每分鐘清一次理; - <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 管理的 Cache;demoCache 的 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>實現“策略外置、統一治理”。上線前做命中率/回源壓測與雪崩演練,才能在高併發與抖動網絡下穩住體驗 🚀