Files
YschcmsService/src/main/java/com/yunhai/cms/config/CacheConfig.java
T
2026-09-07 09:09:39 +08:00

116 lines
4.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}
}
}