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
@@ -1,5 +1,6 @@
package cn.novalon.gym.manage.gateway.cache;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -12,16 +13,11 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* 请求缓存服务
*
* 文件定义:实现网关请求的缓存机制
* 涉及业务:响应缓存、缓存失效、缓存统计
*
* 核心功能:
* 1. 请求响应缓存
* 2. 缓存键生成
* 3. 缓存失效管理
* 4. 缓存统计
*
* <p>
* 底层缓存存储委托给 {@link CacheOperations} 接口,支持通过配置切换 Redis / Caffeine 实现。
* 请求级别的缓存键生成、命中统计等逻辑保留在此服务中。
* </p>
*
* @author 张翔
* @date 2026-03-26
*/
@@ -30,35 +26,33 @@ public class RequestCacheService {
private static final Logger logger = LoggerFactory.getLogger(RequestCacheService.class);
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private final CacheOperations cacheOperations;
private final Map<String, CacheStats> stats = new ConcurrentHashMap<>();
private boolean cacheEnabled = true;
private Duration defaultTtl = Duration.ofMinutes(5);
private int maxCacheSize = 10000;
public RequestCacheService(CacheOperations cacheOperations) {
this.cacheOperations = cacheOperations;
}
public Mono<String> get(ServerHttpRequest request) {
if (!cacheEnabled) {
return Mono.empty();
}
String cacheKey = generateCacheKey(request);
CacheEntry entry = cache.get(cacheKey);
if (entry == null) {
recordMiss(cacheKey);
return Mono.empty();
}
if (isExpired(entry)) {
cache.remove(cacheKey);
recordMiss(cacheKey);
return Mono.empty();
}
recordHit(cacheKey);
logger.debug("Cache hit for key: {}", cacheKey);
return Mono.just(entry.getValue());
return cacheOperations.get(cacheKey, String.class)
.doOnNext(value -> {
recordHit(cacheKey);
logger.debug("Cache hit for key: {}", cacheKey);
})
.switchIfEmpty(Mono.defer(() -> {
recordMiss(cacheKey);
return Mono.empty();
}));
}
public void put(ServerHttpRequest request, String response) {
@@ -67,37 +61,31 @@ public class RequestCacheService {
}
String cacheKey = generateCacheKey(request);
if (cache.size() >= maxCacheSize) {
evictOldestEntries();
}
CacheEntry entry = new CacheEntry(
response,
System.currentTimeMillis(),
defaultTtl.toMillis()
);
cache.put(cacheKey, entry);
logger.debug("Cached response for key: {}", cacheKey);
cacheOperations.setWithExpire(cacheKey, response, defaultTtl.toSeconds())
.doOnSuccess(result -> logger.debug("Cached response for key: {}", cacheKey))
.subscribe();
}
public void evict(ServerHttpRequest request) {
String cacheKey = generateCacheKey(request);
cache.remove(cacheKey);
logger.debug("Evicted cache for key: {}", cacheKey);
cacheOperations.delete(cacheKey)
.doOnSuccess(result -> logger.debug("Evicted cache for key: {}", cacheKey))
.subscribe();
}
public void evictByPattern(String pattern) {
cache.keySet().removeIf(key -> key.matches(pattern));
logger.info("Evicted cache entries matching pattern: {}", pattern);
cacheOperations.deleteByPattern(pattern)
.doOnSuccess(count -> logger.info("Evicted {} cache entries matching pattern: {}", count, pattern))
.subscribe();
}
public void clear() {
int size = cache.size();
cache.clear();
stats.clear();
logger.info("Cleared all cache entries. Removed {} entries", size);
cacheOperations.deleteByPattern(".*")
.doOnSuccess(count -> {
stats.clear();
logger.info("Cleared all cache entries. Removed {} entries", count);
})
.subscribe();
}
private String generateCacheKey(ServerHttpRequest request) {
@@ -106,7 +94,7 @@ public class RequestCacheService {
String query = request.getURI().getQuery();
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(method).append(":").append(path);
keyBuilder.append("gateway:").append(method).append(":").append(path);
if (query != null && !query.isEmpty()) {
keyBuilder.append("?").append(query);
@@ -115,25 +103,6 @@ public class RequestCacheService {
return keyBuilder.toString();
}
private boolean isExpired(CacheEntry entry) {
long currentTime = System.currentTimeMillis();
return (currentTime - entry.getCreatedAt()) > entry.getTtl();
}
private void evictOldestEntries() {
int entriesToRemove = maxCacheSize / 10;
cache.entrySet().stream()
.sorted((e1, e2) ->
Long.compare(e1.getValue().getCreatedAt(),
e2.getValue().getCreatedAt()))
.limit(entriesToRemove)
.map(Map.Entry::getKey)
.forEach(cache::remove);
logger.info("Evicted {} oldest cache entries", entriesToRemove);
}
private void recordHit(String cacheKey) {
stats.compute(cacheKey, (key, stat) -> {
if (stat == null) {
@@ -154,10 +123,6 @@ public class RequestCacheService {
});
}
public int getCacheSize() {
return cache.size();
}
public long getHitCount() {
return stats.values().stream()
.mapToLong(CacheStats::getHits)
@@ -197,30 +162,6 @@ public class RequestCacheService {
logger.info("Max cache size set to: {}", maxSize);
}
private static class CacheEntry {
private final String value;
private final long createdAt;
private final long ttl;
public CacheEntry(String value, long createdAt, long ttl) {
this.value = value;
this.createdAt = createdAt;
this.ttl = ttl;
}
public String getValue() {
return value;
}
public long getCreatedAt() {
return createdAt;
}
public long getTtl() {
return ttl;
}
}
private static class CacheStats {
private long hits = 0;
private long misses = 0;
@@ -241,4 +182,4 @@ public class RequestCacheService {
return misses;
}
}
}
}
@@ -64,6 +64,11 @@ rate:
limit-refresh-period: ${RATE_LIMIT_USER_PERIOD:1s}
timeout-duration: ${RATE_LIMIT_USER_TIMEOUT:0}
# 缓存配置(网关使用本地 Caffeine 缓存,无需 Redis
gym:
cache:
type: caffeine
signature:
enabled: ${SIGNATURE_ENABLED:true}
secret: ${SIGNATURE_SECRET:NovalonManageSystemSecretKey2026}
@@ -1,5 +1,8 @@
package cn.novalon.gym.manage.gateway.cache;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.common.cache.CaffeineCacheOperations;
import cn.novalon.gym.manage.common.cache.CacheProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -14,10 +17,14 @@ import static org.junit.jupiter.api.Assertions.*;
class RequestCacheServiceTest {
private RequestCacheService cacheService;
private CacheOperations cacheOperations;
@BeforeEach
void setUp() {
cacheService = new RequestCacheService();
CacheProperties cacheProperties = new CacheProperties();
cacheProperties.setType("caffeine");
cacheOperations = new CaffeineCacheOperations(cacheProperties);
cacheService = new RequestCacheService(cacheOperations);
}
@Test
@@ -52,7 +59,6 @@ class RequestCacheServiceTest {
String response = "{\"data\":\"test\"}";
cacheService.put(request, response);
cacheService.evict(request);
StepVerifier.create(cacheService.get(request))
@@ -71,9 +77,13 @@ class RequestCacheServiceTest {
cacheService.put(request1, "response1");
cacheService.put(request2, "response2");
cacheService.evictByPattern("GET:/api/test.*");
cacheService.evictByPattern(".*/api/test.*");
assertEquals(0, cacheService.getCacheSize());
// 验证缓存已清空
StepVerifier.create(cacheService.get(request1))
.verifyComplete();
StepVerifier.create(cacheService.get(request2))
.verifyComplete();
}
@Test
@@ -90,7 +100,11 @@ class RequestCacheServiceTest {
cacheService.clear();
assertEquals(0, cacheService.getCacheSize());
// 验证缓存已清空
StepVerifier.create(cacheService.get(request1))
.verifyComplete();
StepVerifier.create(cacheService.get(request2))
.verifyComplete();
}
@Test
@@ -105,8 +119,6 @@ class RequestCacheServiceTest {
StepVerifier.create(cacheService.get(request))
.verifyComplete();
assertEquals(0, cacheService.getCacheSize());
}
@Test
@@ -140,20 +152,6 @@ class RequestCacheServiceTest {
assertEquals(0.0, cacheService.getHitRate());
}
@Test
void testMaxCacheSize() {
cacheService.setMaxCacheSize(5);
for (int i = 0; i < 10; i++) {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test" + i)
.build();
cacheService.put(request, "response" + i);
}
assertTrue(cacheService.getCacheSize() <= 10);
}
@Test
void testCacheWithQueryParams() {
ServerHttpRequest request = MockServerHttpRequest
@@ -188,4 +186,4 @@ class RequestCacheServiceTest {
.expectNext("postResponse")
.verifyComplete();
}
}
}