116 lines
4.3 KiB
Java
116 lines
4.3 KiB
Java
package com.yunhai.cms.config;
|
||
|
||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||
import com.github.benmanes.caffeine.cache.Expiry;
|
||
import org.springframework.cache.CacheManager;
|
||
import org.springframework.cache.annotation.CachingConfigurer;
|
||
import org.springframework.cache.annotation.EnableCaching;
|
||
import org.springframework.cache.caffeine.CaffeineCache;
|
||
import org.springframework.cache.interceptor.CacheErrorHandler;
|
||
import org.springframework.cache.support.SimpleCacheManager;
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Configuration;
|
||
import org.springframework.lang.NonNull;
|
||
|
||
import java.time.Duration;
|
||
import java.util.Arrays;
|
||
import java.util.concurrent.ThreadLocalRandom;
|
||
|
||
/**
|
||
* Caffeine 本地缓存装配(方案 §8.4 工程约束)。
|
||
*
|
||
* <ul>
|
||
* <li>TTL 随机抖动 ±60s 防雪崩:自定义 {@link JitterExpiry} 按条目随机;</li>
|
||
* <li>null 哨兵 30s 防穿透(爬虫扫随机 ID):写入值为 Spring NullValue 标记时短 TTL;</li>
|
||
* <li>CacheErrorHandler:缓存异常按 miss 降级 + error 日志(dirty data risk, TTL 自愈)。</li>
|
||
* </ul>
|
||
* 切 Redis 时仅替换本配置与 spring.cache.type(方案 §1.2/§12);
|
||
* yml 中 spring.cache.caffeine.spec 供默认管理器/演进期使用,本管理器按 CacheNames 显式装配。
|
||
*/
|
||
@Configuration
|
||
@EnableCaching
|
||
public class CacheConfig implements CachingConfigurer {
|
||
|
||
/** 抖动幅度:±60s */
|
||
private static final Duration JITTER = Duration.ofSeconds(60);
|
||
/** null 哨兵 TTL */
|
||
private static final Duration NULL_TTL = Duration.ofSeconds(30);
|
||
/** 抖动后 TTL 下限,避免抖出负数/过短 */
|
||
private static final Duration MIN_TTL = Duration.ofSeconds(30);
|
||
|
||
private final LoggingCacheErrorHandler errorHandler;
|
||
|
||
public CacheConfig(LoggingCacheErrorHandler errorHandler) {
|
||
this.errorHandler = errorHandler;
|
||
}
|
||
|
||
@Bean
|
||
@Override
|
||
public CacheManager cacheManager() {
|
||
SimpleCacheManager manager = new SimpleCacheManager();
|
||
manager.setCaches(Arrays.asList(
|
||
new CaffeineCache(CacheNames.SITE, caffeine(Duration.ofMinutes(30)).build()),
|
||
new CaffeineCache(CacheNames.ARTICLE, caffeine(Duration.ofMinutes(5)).build()),
|
||
new CaffeineCache(CacheNames.LIST, caffeine(Duration.ofMinutes(5)).build())));
|
||
return manager;
|
||
}
|
||
|
||
@Override
|
||
public CacheErrorHandler errorHandler() {
|
||
return errorHandler;
|
||
}
|
||
|
||
private Caffeine<Object, Object> caffeine(Duration baseTtl) {
|
||
return Caffeine.newBuilder()
|
||
.maximumSize(10_000)
|
||
.expireAfter(new JitterExpiry(baseTtl));
|
||
}
|
||
|
||
/**
|
||
* 按 TTL 基准 + 随机抖动的过期策略;null 哨兵固定 30s。
|
||
*/
|
||
static class JitterExpiry implements Expiry<Object, Object> {
|
||
|
||
private final Duration baseTtl;
|
||
|
||
JitterExpiry(Duration baseTtl) {
|
||
this.baseTtl = baseTtl;
|
||
}
|
||
|
||
@Override
|
||
public long expireAfterCreate(@NonNull Object key, Object value, long currentTime) {
|
||
if (isNullValue(value)) {
|
||
return NULL_TTL.toNanos();
|
||
}
|
||
long jitterMillis = ThreadLocalRandom.current()
|
||
.nextLong(-JITTER.toMillis(), JITTER.toMillis());
|
||
Duration jittered = baseTtl.plusMillis(jitterMillis);
|
||
if (jittered.compareTo(MIN_TTL) < 0) {
|
||
jittered = MIN_TTL;
|
||
}
|
||
return jittered.toNanos();
|
||
}
|
||
|
||
@Override
|
||
public long expireAfterUpdate(@NonNull Object key, Object value, long currentTime,
|
||
long currentDuration) {
|
||
return currentDuration;
|
||
}
|
||
|
||
@Override
|
||
public long expireAfterRead(@NonNull Object key, Object value, long currentTime,
|
||
long currentDuration) {
|
||
return currentDuration;
|
||
}
|
||
|
||
/**
|
||
* Spring 把 null 包成 NullValue 单例存入 Caffeine(Caffeine 本身不收 null)。
|
||
* 用类名判断避免对 Spring 内部类硬编译依赖。
|
||
*/
|
||
private boolean isNullValue(Object value) {
|
||
return value == null
|
||
|| "org.springframework.cache.support.NullValue".equals(value.getClass().getName());
|
||
}
|
||
}
|
||
}
|