init local code

This commit is contained in:
chenxing
2026-09-07 09:09:39 +08:00
commit 99cd26b3d2
259 changed files with 20554 additions and 0 deletions
@@ -0,0 +1,115 @@
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());
}
}
}