feat(cache): 二级缓存支持开关,总开关全关 + L1 可单独关闭

- Cache.enabled 总开关:默认 true,关则 L1+L2 一并 NoOp(穿透到方法)
- Cache.L1.enabled 单独开关:默认 true,总开关开时可关 L1 留 L2(纯 Redis 模式)
- MultiLevelCache 按 cacheEnabled/l1Enabled 分发读写删;L1 关闭时广播仍发,兼容集群混合配置
- CachingConfiguration 启动日志输出当前缓存模式,便于排查
This commit is contained in:
daxpay
2026-08-11 08:49:08 +08:00
parent fcea2e204c
commit 0d747ee782
4 changed files with 126 additions and 34 deletions

View File

@@ -144,13 +144,23 @@ public class CachingConfiguration implements CachingConfigurer {
CacheInvalidationPublisher publisher,
SecureCacheNameMatcher secureCacheNameMatcher,
ObjectProvider<SecureAesGcmEncryptor> encryptorProvider) {
// 缓存总开关:关闭后 L1+L2 一并退化成 NoOp直接穿透到方法用于排查/诊断
boolean cacheEnabled = platformCommonProperties.getCache().isEnabled();
// L1 单独开关:总开关开启时,可单独关闭 L1 仅保留 L2 Redis纯 Redis 模式)
boolean l1Enabled = platformCommonProperties.getCache().getL1().isEnabled();
boolean secureL2Enabled = encryptorProvider.getIfAvailable() != null;
log.info("缓存模式: enabled={}, l1={} -> {}",
cacheEnabled, l1Enabled,
!cacheEnabled ? "DISABLED (NoOp, 直接穿透)"
: (l1Enabled ? "L1(Caffeine)+L2(Redis) 二级缓存" : "L2(Redis) only (L1 已关闭)"));
return new MultiLevelCacheManager(
redisCacheManager,
localCacheRegistry,
publisher,
secureCacheNameMatcher,
secureL2Enabled
secureL2Enabled,
cacheEnabled,
l1Enabled
);
}

View File

@@ -17,6 +17,7 @@ import java.util.concurrent.Callable;
/// - 缓存失效通过 Artemis 广播通知其他节点删除本地 L1
/// - 本地缓存 key 必须统一使用字符串形式,保证跨节点广播删除一致性
/// - 敏感缓存secure:)在数据加密未启用时仅使用 L1禁止明文写 Redis
/// - 两层开关:[#cacheEnabled] 总开关关 L1+L2 一并 NoOp[#l1Enabled] 在总开关开启时可单独关 L1 留 L2
@Slf4j
public class MultiLevelCache implements Cache {
@@ -34,19 +35,35 @@ public class MultiLevelCache implements Cache {
/// 敏感缓存是否允许写/读 L2依赖数据加密已启用
private final boolean secureL2Enabled;
/// 缓存总开关false 时 L1+L2 一并 NoOp直接穿透到方法
///
/// 由 [cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties.Cache#isEnabled] 控制。
private final boolean cacheEnabled;
/// L1 本地缓存单独开关false 时跳过 L1 读写删,仅保留 L2 Redis纯 Redis 模式)
///
/// 由 [cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties.Cache.L1#isEnabled] 控制。
/// 仅在 [#cacheEnabled] 为 true 时有意义;总开关关闭时 L1 随之关闭。
private final boolean l1Enabled;
public MultiLevelCache(String name,
LocalCacheRegistry localCacheRegistry,
Cache redisCache,
CacheInvalidationPublisher publisher,
boolean secureCache,
boolean secureL2Enabled) {
boolean secureL2Enabled,
boolean cacheEnabled,
boolean l1Enabled) {
this.name = name;
this.localCache = localCacheRegistry.getOrCreate(name);
this.redisCache = redisCache;
this.publisher = publisher;
this.secureCache = secureCache;
this.secureL2Enabled = secureL2Enabled;
if (secureCache && !secureL2Enabled) {
this.cacheEnabled = cacheEnabled;
this.l1Enabled = l1Enabled;
// 仅在缓存启用时才提示敏感缓存降级,关闭态不打扰日志
if (cacheEnabled && secureCache && !secureL2Enabled) {
log.warn("敏感缓存 [{}] 因未启用数据加密,仅使用 L1 本地缓存,不写 Redis", name);
}
}
@@ -69,29 +86,40 @@ public class MultiLevelCache implements Cache {
/// 读取缓存,优先从 L1 本地缓存读取,未命中则从 L2 Redis 读取并回填 L1
///
/// 读取流程:
/// - 查 L1 本地缓存
/// - L1 未命中则查 L2 Redis
/// - L2 命中则回填 L1
/// - 总开关关闭时直接返回 null穿透到方法
/// - L1 开启时查本地缓存,命中直接返回
/// - L1 关闭或未命中则查 L2 Redis
/// - L2 命中且 L1 开启则回填 L1
/// - 全未命中返回 null
///
/// 敏感缓存且未启用加密时跳过 L2仅查 L1
/// 敏感缓存且未启用加密时跳过 L2仅查 L1L1 也关则穿透)
@Override
public ValueWrapper get(Object key) {
// 缓存总开关关闭:直接穿透,不查任何一层
if (!this.cacheEnabled) {
return null;
}
String localKey = this.toLocalKey(key);
Object localValue = this.localCache.getIfPresent(localKey);
if (localValue != null) {
return () -> localValue;
// L1 单独开关:开启时先查本地缓存
if (this.l1Enabled) {
Object localValue = this.localCache.getIfPresent(localKey);
if (localValue != null) {
return () -> localValue;
}
}
// 敏感缓存未开加密:禁止读 Redis避免历史明文或无法解密的脏数据
if (this.isL1Only()) {
log.debug("敏感缓存 L1-only 未命中: cacheName={}, key={}", this.name, key);
log.debug("敏感缓存 L1-only 未命中: cacheName={}, key={}, l1Enabled={}", this.name, key, this.l1Enabled);
return null;
}
ValueWrapper redisValue = this.redisCache.get(key);
if (redisValue != null) {
this.localCache.put(localKey, Objects.requireNonNull(redisValue.get()));
// L1 开启才回填本地,关闭时仅返回 L2 值
if (this.l1Enabled) {
this.localCache.put(localKey, Objects.requireNonNull(redisValue.get()));
}
return redisValue;
}
@@ -127,51 +155,77 @@ public class MultiLevelCache implements Cache {
}
}
/// 写入缓存,同时写入 L2 Redis 和 L1 本地缓存
/// 写入缓存
///
/// 注意:写入操作不广播通知其他节点,其他节点在读取时会从 L2 加载最新值
///
/// 敏感缓存且未启用加密时仅写 L1,禁止明文写 Redis
/// - L1 开启:写 L2 Redis + L1 本地
/// - L1 关闭:仅写 L2 Redis纯 Redis 模式)
/// - 敏感缓存且未启用加密时仅写 L1L1 也关则不缓存)
@Override
public void put(Object key, Object value) {
// 缓存总开关关闭:直接穿透,不写任何一层
if (!this.cacheEnabled) {
return;
}
if (value == null) {
log.debug("缓存值为空,跳过写入: cacheName={}, key={}", this.name, key);
return;
}
String localKey = this.toLocalKey(key);
if (this.isL1Only()) {
this.localCache.put(localKey, value);
log.debug("写入 L1-only 敏感缓存: cacheName={}, key={}", this.name, key);
// 敏感缓存禁 Redis仅写 L1L1 也关则该缓存实质不缓存
if (this.l1Enabled) {
this.localCache.put(localKey, value);
log.debug("写入 L1-only 敏感缓存: cacheName={}, key={}", this.name, key);
} else {
log.debug("敏感缓存 L1 已关闭且禁写 Redis跳过写入: cacheName={}, key={}", this.name, key);
}
return;
}
this.redisCache.put(key, value);
this.localCache.put(localKey, value);
log.debug("写入二级缓存: cacheName={}, key={}", this.name, key);
if (this.l1Enabled) {
this.localCache.put(localKey, value);
}
log.debug("写入缓存: cacheName={}, key={}, l1Enabled={}", this.name, key, this.l1Enabled);
}
/// 删除缓存,同时删除 L2 Redis 和 L1 本地缓存,并广播通知其他节点删除 L1
/// 删除缓存
///
/// 删除流程:
/// - 删除 L2 Redis
/// - 删除本机 L1
/// - 发布 Artemis 广播消息
/// - 其他节点收到消息后删除各自 L1
/// - 删除 L2 Redis(共享层)
/// - L1 开启时删除本机 L1
/// - 发布 Artemis 广播消息(始终发,其他节点可能 L1 开启)
///
/// 注意:广播不因本机 L1 关闭而跳过——集群中其他节点可能 L1 开启,仍需通知其删除本地缓存
@Override
public void evict(Object key) {
// 缓存总开关关闭:无缓存可删,直接返回
if (!this.cacheEnabled) {
return;
}
String localKey = this.toLocalKey(key);
// 仍尝试删 Redis清理可能存在的历史数据
this.redisCache.evict(key);
this.localCache.invalidate(localKey);
log.debug("删除二级缓存: cacheName={}, key={}", this.name, key);
if (this.l1Enabled) {
this.localCache.invalidate(localKey);
}
log.debug("删除缓存: cacheName={}, key={}, l1Enabled={}", this.name, key, this.l1Enabled);
this.publisher.publishEvict(this.name, localKey);
}
/// 清空缓存,同时清空 L2 Redis 和 L1 本地缓存,并广播通知其他节点清空 L1
/// 清空缓存
///
/// 清空 L2 RedisL1 开启时清空本机 L1始终广播其他节点可能 L1 开启)
@Override
public void clear() {
// 缓存总开关关闭:无缓存可清,直接返回
if (!this.cacheEnabled) {
return;
}
this.redisCache.clear();
this.localCache.invalidateAll();
log.debug("清空二级缓存: cacheName={}", this.name);
if (this.l1Enabled) {
this.localCache.invalidateAll();
}
log.debug("清空缓存: cacheName={}, l1Enabled={}", this.name, this.l1Enabled);
this.publisher.publishClear(this.name);
}

View File

@@ -18,8 +18,9 @@ import java.util.concurrent.ConcurrentHashMap;
///
/// 设计要点:
/// - 作为 Spring 默认 CacheManager通过 @Primary 标识
/// - L1 本地缓存始终启用,不需要开关控制
/// - 缓存失效通知通过 Artemis 广播,始终启用
/// - 两层开关:[#cacheEnabled] 总开关(关=L1+L2 全关);[#l1Enabled] L1 单独开关(总开关开时可关 L1 留 L2
/// - L2 无单独开关:关 L2 等同于关总开关
/// - 缓存失效通知通过 Artemis 广播
/// - 敏感 cacheName 结合 secureL2Enabled 决定是否写 Redis
@Slf4j
@RequiredArgsConstructor
@@ -36,6 +37,17 @@ public class MultiLevelCacheManager implements CacheManager {
/// 敏感缓存是否允许 L2数据加密已启用时为 true
private final boolean secureL2Enabled;
/// 缓存总开关false 时 L1+L2 一并 NoOp直接穿透到方法
///
/// 由 [cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties.Cache#isEnabled] 透传。
private final boolean cacheEnabled;
/// L1 本地缓存单独开关false 时跳过 L1 仅保留 L2 Redis
///
/// 由 [cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties.Cache.L1#isEnabled] 透传。
/// 仅在 [#cacheEnabled] 为 true 时有意义。
private final boolean l1Enabled;
private final Map<String, MultiLevelCache> cacheMap = new ConcurrentHashMap<>();
@Override
@@ -58,14 +70,17 @@ public class MultiLevelCacheManager implements CacheManager {
}
boolean secureCache = this.secureMatcher != null && this.secureMatcher.matches(name);
log.debug("创建二级缓存: name={}, secure={}, secureL2Enabled={}", name, secureCache, this.secureL2Enabled);
log.debug("创建二级缓存: name={}, secure={}, secureL2Enabled={}, cacheEnabled={}, l1Enabled={}",
name, secureCache, this.secureL2Enabled, this.cacheEnabled, this.l1Enabled);
return new MultiLevelCache(
name,
this.localCacheRegistry,
redisCache,
this.publisher,
secureCache,
this.secureL2Enabled
this.secureL2Enabled,
this.cacheEnabled,
this.l1Enabled
);
}
}

View File

@@ -27,6 +27,12 @@ public class PlatformCommonProperties {
///
@Data
public static class Cache {
/// 缓存总开关,关闭后 L1 本地缓存 + L2 Redis 缓存全部退化成 NoOp直接穿透到方法
///
/// 设计要点:
/// - L1 与 L2 是强绑定的整体L1 加速 + L2 共享),不单独开关
/// - true默认= 启用整套二级缓存false = 彻底禁用,排查/诊断时使用
private boolean enabled = true;
/// L1 本地缓存配置
private L1 l1 = new L1();
/// L2 Redis 缓存配置
@@ -40,6 +46,13 @@ public class PlatformCommonProperties {
///
@Data
public static class L1 {
/// L1 本地缓存单独开关false 时关闭 L1 仅保留 L2 Redis纯 Redis 模式)
///
/// 设计要点:
/// - 仅在 [Cache#isEnabled] 总开关开启时生效;总开关关闭时 L1/L2 一并 NoOp
/// - L2 无单独开关:关 L2 等同于关 [Cache#isEnabled] 总开关(全关)
/// - 关闭 L1 适用场景:强一致性要求 / 排查本地脏读 / 单机无本地加速诉求
private boolean enabled = true;
/// 默认超时时间 60秒
private Long defaultTtl = 60L;
/// 默认最大容量