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:
+37
-96
@@ -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}
|
||||
|
||||
+20
-22
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user