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:
@@ -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>
|
||||
|
||||
+21
@@ -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 {
|
||||
}
|
||||
Vendored
+88
@@ -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);
|
||||
}
|
||||
Vendored
+88
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -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);
|
||||
}
|
||||
}
|
||||
+83
@@ -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
@@ -1 +1 @@
|
||||
cn.novalon.gym.manage.common.config.JwtProperties
|
||||
cn.novalon.gym.manage.common.cache.CacheAutoConfiguration
|
||||
Reference in New Issue
Block a user