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 工程约束)。 * * * 切 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 caffeine(Duration baseTtl) { return Caffeine.newBuilder() .maximumSize(10_000) .expireAfter(new JitterExpiry(baseTtl)); } /** * 按 TTL 基准 + 随机抖动的过期策略;null 哨兵固定 30s。 */ static class JitterExpiry implements Expiry { 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()); } } }