refactor(cache): 缓存操作接口化,支持 Redis/Caffeine 双实现切换

将系统所有缓存操作从 `RedisUtil` 迁移至 `CacheOperations` 接口,
通过 `gym.cache.type` 配置选择 Redis 或 Caffeine 实现。

变更内容:
- 新增 CacheOperations 接口,定义 get/set/delete/hasKey/expire/deleteByPattern 等标准方法
- 新增 RedisCacheOperations 实现(基于 ReactiveRedisTemplate,默认实现)
- 新增 CaffeineCacheOperations 实现(基于 Caffeine 本地缓存)
- 新增 CacheProperties 配置类,支持 Caffeine 容量/过期等参数配置
- 新增 CacheAutoConfiguration 自动装配,通过 AutoConfiguration.imports 注册
- 全系统 17 个业务服务 + 18 个测试文件从 RedisUtil 迁移至 CacheOperations
- manage-gateway 配置 caffeine 实现(无需 Redis),manage-app 配置 redis 实现
- 分布式锁等 Redis 独有特性保持直接使用 ReactiveRedisTemplate

测试:202 通过,0 失败,0 错误
This commit was merged in pull request #56.
This commit is contained in:
2026-08-02 18:26:15 +08:00
parent eb33755f23
commit 4ba4af2f53
45 changed files with 798 additions and 476 deletions
+4
View File
@@ -60,6 +60,10 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
<build>
@@ -0,0 +1,21 @@
package cn.novalon.gym.manage.common.cache;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 缓存自动配置
* <p>
* 启用 {@link CacheProperties} 配置绑定,并扫描 {@code cn.novalon.gym.manage.common.cache}
* 包下的缓存实现组件({@link RedisCacheOperations} / {@link CaffeineCacheOperations})。
* </p>
*
* @author 张翔
* @date 2026-08-02
*/
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@ComponentScan(basePackageClasses = CacheAutoConfiguration.class)
public class CacheAutoConfiguration {
}
@@ -0,0 +1,88 @@
package cn.novalon.gym.manage.common.cache;
import reactor.core.publisher.Mono;
/**
* 缓存操作接口
* <p>
* 定义统一的缓存操作抽象,支持通过配置切换不同的缓存实现(Caffeine / Redis)。
* 所有业务模块应依赖此接口而非具体实现,实现类由 {@link CacheAutoConfiguration} 按配置自动装配。
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see RedisCacheOperations
* @see CaffeineCacheOperations
*/
public interface CacheOperations {
/**
* 获取缓存值(带类型转换)
*
* @param key 缓存键
* @param clazz 目标类型
* @param <T> 返回值类型
* @return 缓存值,不存在时返回 {@link Mono#empty()}
*/
<T> Mono<T> get(String key, Class<T> clazz);
/**
* 获取缓存值(返回 Object)
*
* @param key 缓存键
* @return 缓存值,不存在时返回 {@link Mono#empty()}
*/
Mono<Object> get(String key);
/**
* 设置缓存值(无过期时间)
*
* @param key 缓存键
* @param value 缓存值
* @return true 设置成功
*/
Mono<Boolean> set(String key, Object value);
/**
* 设置缓存值并指定过期时间
*
* @param key 缓存键
* @param value 缓存值
* @param timeoutSeconds 过期时间(秒)
* @return true 设置成功
*/
Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds);
/**
* 删除缓存
*
* @param key 缓存键
* @return 被删除的键数量
*/
Mono<Long> delete(String key);
/**
* 判断缓存键是否存在
*
* @param key 缓存键
* @return true 存在
*/
Mono<Boolean> hasKey(String key);
/**
* 设置过期时间
*
* @param key 缓存键
* @param timeoutSeconds 过期时间(秒)
* @return true 设置成功
*/
Mono<Boolean> expire(String key, long timeoutSeconds);
/**
* 根据通配符模式删除缓存键
*
* @param pattern 通配符模式,如 {@code member:info:*}
* @return 被删除的键数量
*/
Mono<Long> deleteByPattern(String pattern);
}
@@ -0,0 +1,88 @@
package cn.novalon.gym.manage.common.cache;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 缓存配置属性
* <p>
* 通过 {@code gym.cache.type} 配置选择缓存实现:
* <ul>
* <li>{@code redis} — 使用 Redis 实现(默认)</li>
* <li>{@code caffeine} — 使用 Caffeine 本地缓存实现</li>
* </ul>
* </p>
*
* <pre>{@code
* gym:
* cache:
* type: redis # redis | caffeine
* caffeine:
* initial-capacity: 100
* max-size: 10000
* expire-after-write-seconds: 300
* }</pre>
*
* @author 张翔
* @date 2026-08-02
*/
@ConfigurationProperties(prefix = "gym.cache")
public class CacheProperties {
/** 缓存类型:redis / caffeine,默认 redis */
private String type = "redis";
/** Caffeine 本地缓存专属配置 */
private Caffeine caffeine = new Caffeine();
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
public static class Caffeine {
/** 初始容量 */
private int initialCapacity = 100;
/** 最大条数 */
private long maxSize = 10000;
/** 写入后过期时间(秒),默认 5 分钟 */
private long expireAfterWriteSeconds = 300;
public int getInitialCapacity() {
return initialCapacity;
}
public void setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
}
public long getMaxSize() {
return maxSize;
}
public void setMaxSize(long maxSize) {
this.maxSize = maxSize;
}
public long getExpireAfterWriteSeconds() {
return expireAfterWriteSeconds;
}
public void setExpireAfterWriteSeconds(long expireAfterWriteSeconds) {
this.expireAfterWriteSeconds = expireAfterWriteSeconds;
}
}
}
@@ -0,0 +1,111 @@
package cn.novalon.gym.manage.common.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* Caffeine 本地缓存实现
* <p>
* 基于 Caffeine 的进程内缓存,适合单机部署或开发测试环境。
* 当 {@code gym.cache.type=caffeine} 时生效。
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see CacheOperations
* @see RedisCacheOperations
*/
@Component
@ConditionalOnProperty(name = "gym.cache.type", havingValue = "caffeine")
public class CaffeineCacheOperations implements CacheOperations {
private static final Logger log = LoggerFactory.getLogger(CaffeineCacheOperations.class);
private final Cache<String, Object> cache;
public CaffeineCacheOperations(CacheProperties cacheProperties) {
CacheProperties.Caffeine config = cacheProperties.getCaffeine();
this.cache = Caffeine.newBuilder()
.initialCapacity(config.getInitialCapacity())
.maximumSize(config.getMaxSize())
.expireAfterWrite(config.getExpireAfterWriteSeconds(), TimeUnit.SECONDS)
.recordStats()
.build();
log.info("Caffeine 缓存已初始化: initialCapacity={}, maxSize={}, expireAfterWrite={}s",
config.getInitialCapacity(), config.getMaxSize(), config.getExpireAfterWriteSeconds());
}
@Override
@SuppressWarnings("unchecked")
public <T> Mono<T> get(String key, Class<T> clazz) {
Object value = cache.getIfPresent(key);
if (value == null) {
return Mono.empty();
}
if (clazz.isInstance(value)) {
return Mono.just((T) value);
}
log.warn("Caffeine 缓存类型不匹配: key={}, expected={}, actual={}",
key, clazz.getName(), value.getClass().getName());
return Mono.empty();
}
@Override
public Mono<Object> get(String key) {
Object value = cache.getIfPresent(key);
if (value == null) {
return Mono.empty();
}
return Mono.just(value);
}
@Override
public Mono<Boolean> set(String key, Object value) {
cache.put(key, value);
return Mono.just(true);
}
@Override
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
// Caffeine 在构建时已指定 expireAfterWrite,此处仅做 put 操作
// 如需 per-entry TTL,可考虑 Caffeine 的 Expiry 接口,但当前场景统一 TTL 已足够
cache.put(key, value);
return Mono.just(true);
}
@Override
public Mono<Long> delete(String key) {
cache.invalidate(key);
return Mono.just(1L);
}
@Override
public Mono<Boolean> hasKey(String key) {
return Mono.just(cache.getIfPresent(key) != null);
}
@Override
public Mono<Boolean> expire(String key, long timeoutSeconds) {
// Caffeine 不支持为单个 key 动态设置过期时间,直接返回 true(过期由构建时策略统一管理)
log.debug("Caffeine 不支持动态设置过期时间,忽略: key={}, timeoutSeconds={}", key, timeoutSeconds);
return Mono.just(true);
}
@Override
public Mono<Long> deleteByPattern(String pattern) {
Pattern regex = Pattern.compile(pattern.replace("*", ".*"));
long count = cache.asMap().keySet().stream()
.filter(k -> regex.matcher(k).matches())
.peek(cache::invalidate)
.count();
return Mono.just(count);
}
}
@@ -0,0 +1,83 @@
package cn.novalon.gym.manage.common.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* Redis 缓存实现
* <p>
* 基于 {@link ReactiveRedisTemplate} 的响应式 Redis 缓存操作。
* 当 {@code gym.cache.type=redis} 时生效(默认值)。
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see CacheOperations
* @see CaffeineCacheOperations
*/
@Component
@ConditionalOnProperty(name = "gym.cache.type", havingValue = "redis", matchIfMissing = true)
public class RedisCacheOperations implements CacheOperations {
private static final Logger log = LoggerFactory.getLogger(RedisCacheOperations.class);
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
public RedisCacheOperations(ReactiveRedisTemplate<String, Object> reactiveRedisTemplate) {
this.reactiveRedisTemplate = reactiveRedisTemplate;
}
@Override
public <T> Mono<T> get(String key, Class<T> clazz) {
return reactiveRedisTemplate.opsForValue().get(key)
.filter(clazz::isInstance)
.map(clazz::cast)
.onErrorResume(e -> {
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
return Mono.empty();
});
}
@Override
public Mono<Object> get(String key) {
return reactiveRedisTemplate.opsForValue().get(key);
}
@Override
public Mono<Boolean> set(String key, Object value) {
return reactiveRedisTemplate.opsForValue().set(key, value);
}
@Override
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds));
}
@Override
public Mono<Long> delete(String key) {
return reactiveRedisTemplate.delete(key);
}
@Override
public Mono<Boolean> hasKey(String key) {
return reactiveRedisTemplate.hasKey(key);
}
@Override
public Mono<Boolean> expire(String key, long timeoutSeconds) {
return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds));
}
@Override
public Mono<Long> deleteByPattern(String pattern) {
return reactiveRedisTemplate.keys(pattern)
.flatMap(keys -> reactiveRedisTemplate.delete(keys))
.count();
}
}
@@ -1 +1 @@
cn.novalon.gym.manage.common.config.JwtProperties
cn.novalon.gym.manage.common.cache.CacheAutoConfiguration