Jiaqi Xia

feat(cache): add cache dependencies and configuration for Redis and Caffeine

......@@ -51,6 +51,14 @@ dependencies {
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.2' // Jackson XML 模块
implementation 'org.springframework.boot:spring-boot-starter-websocket'// websocket
// 缓存相关依赖
implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis
implementation 'org.apache.commons:commons-pool2' // Redis连接池(增强版)
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' // Caffeine本地缓存
implementation 'org.springframework:spring-aspects' // Spring AOP
implementation 'io.micrometer:micrometer-core:1.14.2' // 监控指标
implementation 'io.micrometer:micrometer-registry-statsd:1.14.2' // StatsD监控
implementation 'com.aliyun:aliyun-java-sdk-core:4.6.4' //阿里云SDK核心库
implementation 'com.aliyun:aliyun-java-sdk-dysmsapi:2.2.1'//阿里云短信服务SDK
implementation 'com.aliyun:aliyun-java-sdk-dm:3.3.2'//阿里云邮件服务SDK
......
package com.infoloop.tianting.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 缓存自动配置类
* 在Spring Boot启动时自动配置缓存相关组件
*/
@Slf4j
@Configuration
@EnableCaching
@ConditionalOnClass({Caffeine.class, StringRedisTemplate.class})
@EnableConfigurationProperties(CacheProperties.class)
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true", matchIfMissing = true)
public class CacheAutoConfiguration {
@Autowired
private CacheProperties cacheProperties;
/**
* 配置Caffeine缓存管理器
*/
@Bean
@ConditionalOnMissingBean
public com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(cacheProperties.getCaffeine().getInitialCapacity())
.maximumSize(cacheProperties.getCaffeine().getMaximumSize())
.expireAfterWrite(cacheProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.SECONDS);
// 如果配置了访问后过期时间,则启用
if (cacheProperties.getCaffeine().getExpireAfterAccess() > 0) {
caffeine.expireAfterAccess(cacheProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.SECONDS);
}
// 优化并发性能:使用调用线程执行过期任务,减少线程切换开销
caffeine.executor(Runnable::run);
if (cacheProperties.getCaffeine().isRecordStats()) {
caffeine.recordStats();
}
log.info("Caffeine缓存配置完成: initialCapacity={}, maximumSize={}, expireAfterWrite={}s, expireAfterAccess={}s",
cacheProperties.getCaffeine().getInitialCapacity(),
cacheProperties.getCaffeine().getMaximumSize(),
cacheProperties.getCaffeine().getExpireAfterWrite(),
cacheProperties.getCaffeine().getExpireAfterAccess());
return caffeine.build();
}
/**
* 配置Caffeine缓存服务
*/
@Bean("caffeineCacheService")
@ConditionalOnMissingBean(name = "caffeineCacheService")
public CaffeineCacheService caffeineCacheService(com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache) {
log.info("Caffeine缓存服务配置完成");
return new CaffeineCacheService(caffeineCache);
}
/**
* 配置ObjectMapper用于JSON序列化
* 针对Lombok @Data类优化,支持标准的序列化/反序列化
*/
@Bean("cacheObjectMapper")
@ConditionalOnMissingBean(name = "cacheObjectMapper")
public com.fasterxml.jackson.databind.ObjectMapper cacheObjectMapper() {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
mapper.findAndRegisterModules(); // 注册所有模块,包括Java 8时间模块
// 基本配置
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
// 为Lombok @Data类启用类型信息,这样可以正确反序列化
mapper.activateDefaultTyping(
com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build(),
com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL,
com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
);
log.info("缓存专用ObjectMapper配置完成(优化支持Lombok @Data类)");
return mapper;
}
/**
* 配置Redis缓存服务
*/
@Bean("redisCacheService")
@ConditionalOnMissingBean(name = "redisCacheService")
public RedisCacheService redisCacheService(StringRedisTemplate redisTemplate,
@org.springframework.beans.factory.annotation.Qualifier("cacheObjectMapper")
com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
log.info("Redis缓存服务配置完成");
return new RedisCacheService(redisTemplate, objectMapper);
}
/**
* 配置缓存服务(主要使用多级缓存)
*/
@Bean("multiLevelCacheService")
@ConditionalOnMissingBean(name = "multiLevelCacheService")
public MultiLevelCacheService multiLevelCacheService(
CaffeineCacheService caffeineCacheService,
RedisCacheService redisCacheService,
CacheProperties cacheProperties) {
log.info("多级缓存服务配置完成");
return new MultiLevelCacheService(caffeineCacheService, redisCacheService, cacheProperties);
}
/**
* 配置Spring Cache管理器 - 桥接到多级缓存服务
*/
@Bean
@Primary
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager cacheManager(
@org.springframework.beans.factory.annotation.Qualifier("multiLevelCacheService")
CacheService cacheService,
CacheProperties cacheProperties) {
log.info("Spring Cache管理器配置完成,桥接到多级缓存服务");
return new MultiLevelCacheManager(cacheService, cacheProperties);
}
/**
* 配置缓存健康检查
*/
@Bean
@ConditionalOnMissingBean
public CacheHealthIndicator cacheHealthIndicator() {
log.info("缓存健康检查配置完成");
return new CacheHealthIndicator();
}
/**
* 配置缓存管理控制器
*/
@Bean
@ConditionalOnMissingBean
public CacheManagementController cacheManagementController() {
log.info("缓存管理控制器配置完成");
return new CacheManagementController();
}
}
package com.infoloop.tianting.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
/**
* 缓存健康检查指示器
*/
@Slf4j
public class CacheHealthIndicator implements HealthIndicator {
@Autowired
@Qualifier("multiLevelCacheService")
private CacheService cacheService;
@Override
public Health health() {
try {
// 执行简单的缓存操作来检查健康状态
String testKey = "health_check_" + System.currentTimeMillis();
cacheService.put(testKey, "test", 10);
Object value = cacheService.get(testKey);
cacheService.evict(testKey);
if ("test".equals(value)) {
return Health.up()
.withDetail("status", "缓存服务正常")
.build();
} else {
return Health.down()
.withDetail("status", "缓存读写异常")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("status", "缓存服务异常")
.withDetail("error", e.getMessage())
.build();
}
}
}
package com.infoloop.tianting.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 缓存管理控制器
* 提供缓存操作的REST端点
*/
@Slf4j
@RestController
@RequestMapping("/actuator/cache")
public class CacheManagementController {
@Autowired
@Qualifier("multiLevelCacheService")
private CacheService cacheService;
/**
* 获取缓存统计信息
*/
@GetMapping("/stats")
public CacheStats getCacheStats() {
return cacheService.getStats();
}
/**
* 清除指定缓存
*/
@DeleteMapping("/evict")
public ResponseEntity<?> evictCache(@RequestParam String key) {
try {
cacheService.evict(key);
log.info("缓存清除成功: {}", key);
return ResponseEntity.ok("缓存已清除: " + key);
} catch (Exception e) {
log.error("缓存清除失败: key={}, error={}", key, e.getMessage(), e);
return ResponseEntity.internalServerError().body("缓存清除失败: " + e.getMessage());
}
}
/**
* 根据前缀清除缓存
*/
@DeleteMapping("/evict/prefix")
public ResponseEntity<?> evictByPrefix(@RequestParam String prefix) {
try {
cacheService.evictByPrefix(prefix);
log.info("前缀缓存清除成功: {}", prefix);
return ResponseEntity.ok("前缀缓存已清除: " + prefix);
} catch (Exception e) {
log.error("前缀缓存清除失败: prefix={}, error={}", prefix, e.getMessage(), e);
return ResponseEntity.internalServerError().body("前缀缓存清除失败: " + e.getMessage());
}
}
/**
* 清空所有缓存
*/
@DeleteMapping("/clear")
public ResponseEntity<?> clearCache() {
try {
cacheService.clear();
log.info("所有缓存清空成功");
return ResponseEntity.ok("所有缓存已清除");
} catch (Exception e) {
log.error("缓存清空失败: error={}", e.getMessage(), e);
return ResponseEntity.internalServerError().body("缓存清空失败: " + e.getMessage());
}
}
/**
* 检查缓存是否存在
*/
@GetMapping("/exists")
public ResponseEntity<?> checkExists(@RequestParam String key) {
try {
boolean exists = cacheService.exists(key);
return ResponseEntity.ok(String.format("缓存key '%s' %s", key, exists ? "存在" : "不存在"));
} catch (Exception e) {
log.error("缓存存在性检查失败: key={}, error={}", key, e.getMessage(), e);
return ResponseEntity.internalServerError().body("缓存检查失败: " + e.getMessage());
}
}
/**
* 获取缓存过期时间
*/
@GetMapping("/expire")
public ResponseEntity<?> getExpire(@RequestParam String key) {
try {
long expire = cacheService.getExpire(key);
String message;
if (expire == -2) {
message = "缓存key不存在";
} else if (expire == -1) {
message = "缓存key永久有效";
} else {
message = String.format("缓存key剩余时间: %d秒", expire);
}
return ResponseEntity.ok(message);
} catch (Exception e) {
log.error("缓存过期时间获取失败: key={}, error={}", key, e.getMessage(), e);
return ResponseEntity.internalServerError().body("缓存过期时间获取失败: " + e.getMessage());
}
}
}
package com.infoloop.tianting.cache;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 缓存配置属性
*/
@Data
@ConfigurationProperties(prefix = "cache")
public class CacheProperties {
/**
* 是否启用缓存
*/
private boolean enabled = true;
/**
* 默认缓存过期时间(秒)
*/
private int defaultTtl = 300;
/**
* Caffeine本地缓存配置
*/
private CaffeineProperties caffeine = new CaffeineProperties();
/**
* Redis缓存配置
*/
private RedisProperties redis = new RedisProperties();
/**
* 监控配置
*/
private MetricsProperties metrics = new MetricsProperties();
@Data
public static class CaffeineProperties {
/**
* 初始容量
*/
private int initialCapacity = 100;
/**
* 最大容量
*/
private int maximumSize = 10000;
/**
* 写入后过期时间(秒)
*/
private int expireAfterWrite = 60;
/**
* 访问后过期时间(秒),0表示不启用
*/
private int expireAfterAccess = 0;
/**
* 是否记录统计信息
*/
private boolean recordStats = true;
}
@Data
public static class RedisProperties {
/**
* 是否启用Redis缓存
*/
private boolean enabled = true;
/**
* Redis主机
*/
private String host = "localhost";
/**
* Redis端口
*/
private int port = 6379;
/**
* Redis密码
*/
private String password;
/**
* Redis数据库索引
*/
private int database = 1;
/**
* 连接超时时间(毫秒)
*/
private int timeout = 2000;
}
@Data
public static class MetricsProperties {
/**
* 是否启用监控
*/
private boolean enabled = true;
/**
* 监控步长
*/
private String step = "1m";
}
}
package com.infoloop.tianting.cache;
/**
* 缓存服务接口
* 定义了缓存的基本操作方法
*/
public interface CacheService {
/**
* 获取缓存
* @param key 缓存key
* @return 缓存的值,如果不存在返回null
*/
Object get(String key);
/**
* 设置缓存
* @param key 缓存key
* @param value 缓存值
* @param ttlSeconds 过期时间(秒)
*/
void put(String key, Object value, int ttlSeconds);
/**
* 删除缓存
* @param key 缓存key
*/
void evict(String key);
/**
* 删除缓存(别名方法)
* @param key 缓存key
*/
default void delete(String key) {
evict(key);
}
/**
* 根据前缀删除缓存
* @param prefix 缓存key前缀
*/
void evictByPrefix(String prefix);
/**
* 根据模式删除缓存
* @param pattern 缓存key模式(支持通配符*)
*/
default void deleteByPattern(String pattern) {
evictByPrefix(pattern.replace("*", ""));
}
/**
* 清空所有缓存
*/
void clear();
/**
* 获取缓存统计信息
* @return 缓存统计信息
*/
CacheStats getStats();
/**
* 检查缓存是否存在
* @param key 缓存key
* @return true表示存在,false表示不存在
*/
boolean exists(String key);
/**
* 获取缓存剩余过期时间
* @param key 缓存key
* @return 剩余时间(秒),-2表示key不存在,-1表示永久
*/
long getExpire(String key);
}
package com.infoloop.tianting.cache;
import lombok.Builder;
import lombok.Data;
/**
* 缓存统计信息
*/
@Data
@Builder
public class CacheStats {
/**
* 本地缓存命中次数
*/
private long localHits;
/**
* 本地缓存未命中次数
*/
private long localMisses;
/**
* Redis缓存命中次数
*/
private long redisHits;
/**
* Redis缓存未命中次数
*/
private long redisMisses;
/**
* 总请求次数
*/
private long totalRequests;
/**
* 缓存命中率
*/
private double hitRate;
/**
* 本地缓存大小
*/
private long localSize;
/**
* Redis缓存大小(如果可获取)
*/
private long redisSize;
}
package com.infoloop.tianting.cache;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
/**
* Caffeine本地缓存服务实现
*/
@Slf4j
public class CaffeineCacheService implements CacheService {
private final Cache<String, Object> cache;
public CaffeineCacheService(Cache<String, Object> cache) {
this.cache = cache;
}
@Override
public Object get(String key) {
return cache.getIfPresent(key);
}
@Override
public void put(String key, Object value, int ttlSeconds) {
// Caffeine不支持单个key的TTL,使用全局配置的过期时间
cache.put(key, value);
}
@Override
public void evict(String key) {
cache.invalidate(key);
}
@Override
public void evictByPrefix(String prefix) {
cache.asMap().keySet().removeIf(key -> key.startsWith(prefix));
}
@Override
public void clear() {
cache.invalidateAll();
}
@Override
public CacheStats getStats() {
com.github.benmanes.caffeine.cache.stats.CacheStats caffeineStats = cache.stats();
return CacheStats.builder()
.localHits(caffeineStats.hitCount())
.localMisses(caffeineStats.missCount())
.totalRequests(caffeineStats.requestCount())
.hitRate(caffeineStats.hitRate())
.localSize(cache.estimatedSize())
.build();
}
@Override
public boolean exists(String key) {
return cache.getIfPresent(key) != null;
}
@Override
public long getExpire(String key) {
// Caffeine不支持获取单个key的过期时间
return exists(key) ? -1 : -2;
}
}
package com.infoloop.tianting.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 多级缓存管理器
* 实现Spring Cache CacheManager接口,桥接到现有的MultiLevelCacheService
*/
@Slf4j
public class MultiLevelCacheManager implements CacheManager {
private final CacheService cacheService;
private final CacheProperties cacheProperties;
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
public MultiLevelCacheManager(CacheService cacheService, CacheProperties cacheProperties) {
this.cacheService = cacheService;
this.cacheProperties = cacheProperties;
log.info("多级缓存管理器初始化完成,默认TTL: {}s", cacheProperties.getDefaultTtl());
}
@Override
public Cache getCache(String name) {
if (name == null) {
return null;
}
return cacheMap.computeIfAbsent(name, cacheName -> {
log.debug("创建新的缓存区域: {}", cacheName);
return new SpringCacheAdapter(cacheName, cacheService, cacheProperties.getDefaultTtl());
});
}
@Override
public Collection<String> getCacheNames() {
return cacheMap.keySet();
}
/**
* 清除所有缓存
*/
public void clearAll() {
cacheMap.values().forEach(Cache::clear);
log.info("已清除所有缓存区域");
}
/**
* 获取缓存统计信息
*/
public String getCacheStats() {
StringBuilder stats = new StringBuilder();
stats.append("缓存区域数量: ").append(cacheMap.size()).append("\n");
stats.append("缓存区域列表: ").append(String.join(", ", getCacheNames()));
return stats.toString();
}
}
package com.infoloop.tianting.cache;
import lombok.extern.slf4j.Slf4j;
/**
* 多级缓存服务实现
* 结合Caffeine本地缓存和Redis分布式缓存
* 实现L1 + L2缓存架构
*
* 缓存逻辑说明:
* 1. 查询逻辑:先查Caffeine(60s) -> 未命中再查Redis -> 未命中再查数据库
* 2. 写入逻辑:先写Redis(业务TTL) -> 再写Caffeine(固定60s)
* 3. Caffeine过期后正确从Redis读取数据,避免直接查数据库
*/
@Slf4j
public class MultiLevelCacheService implements CacheService {
private final CaffeineCacheService caffeineCacheService;
private final RedisCacheService redisCacheService;
private final CacheProperties cacheProperties;
// 缓存空值的特殊标记
private static final String NULL_VALUE_MARKER = "$$NULL$$";
public MultiLevelCacheService(CaffeineCacheService caffeineCacheService,
RedisCacheService redisCacheService,
CacheProperties cacheProperties) {
this.caffeineCacheService = caffeineCacheService;
this.redisCacheService = redisCacheService;
this.cacheProperties = cacheProperties;
log.info("多级缓存服务初始化完成,Caffeine TTL: {}s", cacheProperties.getCaffeine().getExpireAfterWrite());
}
@Override
public Object get(String key) {
if (key == null || key.trim().isEmpty()) {
return null;
}
log.debug("开始缓存查询: key={}", key);
// 1. 先查本地缓存(Caffeine - L1缓存)
Object localValue = caffeineCacheService.get(key);
if (localValue != null) {
if (NULL_VALUE_MARKER.equals(localValue)) {
log.debug("本地缓存命中(空值): {}", key);
return null;
}
log.debug("本地缓存命中: {}", key);
// 检查Redis中是否也存在,如果不存在则回写
if (!redisCacheService.exists(key)) {
try {
// 使用默认TTL回写到Redis
int defaultRedisTtl = cacheProperties.getDefaultTtl();
redisCacheService.put(key, localValue, defaultRedisTtl);
log.debug("本地缓存命中但Redis缺失,已回写: key={}, ttl={}s", key, defaultRedisTtl);
} catch (Exception e) {
log.error("Redis回写失败: key={}, error={}", key, e.getMessage());
}
}
return localValue;
}
// 2. 本地缓存未命中,查分布式缓存(Redis - L2缓存)
log.debug("Caffeine未命中,检查Redis分布式缓存: key={}", key);
Object redisValue = redisCacheService.get(key);
if (redisValue != null) {
if (NULL_VALUE_MARKER.equals(redisValue)) {
// 将空值标记同步到本地缓存,避免重复查询
caffeineCacheService.put(key, NULL_VALUE_MARKER, cacheProperties.getCaffeine().getExpireAfterWrite());
log.debug("分布式缓存命中(空值),并同步到本地缓存: {}", key);
return null;
}
// 同步到本地缓存
caffeineCacheService.put(key, redisValue, cacheProperties.getCaffeine().getExpireAfterWrite());
log.debug("分布式缓存命中,返回Redis数据: key={}", key);
return redisValue;
}
// 3. 两级缓存都未命中,需要查询数据库
log.debug("两级缓存都未命中,将查询数据库: key={}", key);
return null;
}
@Override
public void put(String key, Object value, int ttlSeconds) {
if (key == null || key.trim().isEmpty()) {
log.error("缓存key不能为空");
return;
}
if (ttlSeconds <= 0) {
log.error("缓存TTL必须大于0: key={}, ttl={}", key, ttlSeconds);
return;
}
// 处理空值缓存
Object actualValue = (value == null) ? NULL_VALUE_MARKER : value;
// 写入策略:先写Redis,再写Caffeine
boolean redisSuccess = false;
boolean localSuccess = false;
try {
// 写入分布式缓存Redis,使用注解指定的TTL
redisCacheService.put(key, actualValue, ttlSeconds);
redisSuccess = true;
log.debug("分布式缓存写入成功: key={}, ttl={}s", key, ttlSeconds);
} catch (Exception e) {
log.error("分布式缓存写入失败: key={}, error={}", key, e.getMessage(), e);
}
try {
// 2. 写入本地缓存Caffeine,使用配置的过期时间
int caffeineTtl = cacheProperties.getCaffeine().getExpireAfterWrite();
caffeineCacheService.put(key, actualValue, caffeineTtl);
localSuccess = true;
log.debug("本地缓存写入成功: key={}, ttl={}s", key, caffeineTtl);
} catch (Exception e) {
log.error("本地缓存写入失败: key={}, error={}", key, e.getMessage(), e);
}
if (!localSuccess && !redisSuccess) {
log.error("缓存写入完全失败: key={}", key);
}
}
@Override
public void evict(String key) {
// 双删策略:同时删除本地缓存和分布式缓存
try {
caffeineCacheService.evict(key);
log.debug("本地缓存删除成功: {}", key);
} catch (Exception e) {
log.error("本地缓存删除失败: key={}, error={}", key, e.getMessage());
}
try {
redisCacheService.evict(key);
log.debug("分布式缓存删除成功: {}", key);
} catch (Exception e) {
log.error("分布式缓存删除失败: key={}, error={}", key, e.getMessage());
}
}
@Override
public void evictByPrefix(String prefix) {
// 双删策略:同时删除本地缓存和分布式缓存的前缀
try {
caffeineCacheService.evictByPrefix(prefix);
log.debug("本地缓存前缀删除成功: {}", prefix);
} catch (Exception e) {
log.error("本地缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage());
}
try {
redisCacheService.evictByPrefix(prefix);
log.debug("分布式缓存前缀删除成功: {}", prefix);
} catch (Exception e) {
log.error("分布式缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage());
}
}
@Override
public void clear() {
// 清空所有缓存
try {
caffeineCacheService.clear();
log.info("本地缓存清空成功");
} catch (Exception e) {
log.error("本地缓存清空失败: error={}", e.getMessage());
}
try {
redisCacheService.clear();
log.info("分布式缓存清空成功");
} catch (Exception e) {
log.error("分布式缓存清空失败: error={}", e.getMessage());
}
}
@Override
public CacheStats getStats() {
CacheStats localStats = caffeineCacheService.getStats();
CacheStats redisStats = redisCacheService.getStats();
return CacheStats.builder()
.localHits(localStats.getLocalHits())
.localMisses(localStats.getLocalMisses())
.redisHits(redisStats.getRedisHits())
.redisMisses(redisStats.getRedisMisses())
.totalRequests(calculateTotalRequests(localStats, redisStats))
.hitRate(calculateHitRate(localStats, redisStats))
.localSize(localStats.getLocalSize())
.build();
}
@Override
public boolean exists(String key) {
// 只要任一缓存存在就返回true
return caffeineCacheService.exists(key) || redisCacheService.exists(key);
}
@Override
public long getExpire(String key) {
// 优先返回分布式缓存的过期时间
long redisExpire = redisCacheService.getExpire(key);
if (redisExpire >= 0) {
return redisExpire;
}
// 如果Redis没有,则返回本地缓存的过期时间
return caffeineCacheService.getExpire(key);
}
private long calculateTotalRequests(CacheStats localStats, CacheStats redisStats) {
return localStats.getLocalHits() + localStats.getLocalMisses() +
redisStats.getRedisHits() + redisStats.getRedisMisses();
}
private double calculateHitRate(CacheStats localStats, CacheStats redisStats) {
long totalHits = localStats.getLocalHits() + redisStats.getRedisHits();
long totalRequests = calculateTotalRequests(localStats, redisStats);
return totalRequests > 0 ? (double) totalHits / totalRequests : 0.0;
}
}
package com.infoloop.tianting.cache;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Redis分布式缓存服务实现(优化版)
* 包含缓存穿透保护、性能优化等功能
*/
@Slf4j
public class RedisCacheService implements CacheService {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
private static final String CACHE_PREFIX = "cache:";
private static final String NULL_VALUE_MARKER = "::NULL::";
public RedisCacheService(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Override
public Object get(String key) {
if (key == null || key.trim().isEmpty()) {
log.warn("Redis缓存key不能为空");
return null;
}
try {
String jsonValue = redisTemplate.opsForValue().get(CACHE_PREFIX + key);
if (jsonValue != null) {
// 处理null值标记,防止缓存穿透
if (NULL_VALUE_MARKER.equals(jsonValue)) {
return null;
}
// 使用TypeReference保持类型信息
return objectMapper.readValue(jsonValue, new TypeReference<Object>() {});
}
} catch (Exception e) {
log.error("Redis缓存读取失败: key={}, error={}", key, e.getMessage(), e);
}
return null;
}
@Override
public void put(String key, Object value, int ttlSeconds) {
if (key == null || key.trim().isEmpty()) {
log.warn("Redis缓存key不能为空");
return;
}
if (ttlSeconds <= 0) {
log.warn("Redis缓存TTL必须大于0: ttl={}", ttlSeconds);
return;
}
try {
String jsonValue;
if (value == null) {
// 缓存null值,防止缓存穿透
jsonValue = NULL_VALUE_MARKER;
} else {
jsonValue = objectMapper.writeValueAsString(value);
}
redisTemplate.opsForValue().set(CACHE_PREFIX + key, jsonValue,
Duration.ofSeconds(ttlSeconds));
log.debug("Redis缓存设置成功: key={}, ttl={}s", key, ttlSeconds);
} catch (Exception e) {
log.error("Redis缓存写入失败: key={}, error={}", key, e.getMessage(), e);
}
}
@Override
public void evict(String key) {
if (key == null || key.trim().isEmpty()) {
log.warn("Redis缓存key不能为空");
return;
}
try {
Boolean deleted = redisTemplate.delete(CACHE_PREFIX + key);
log.debug("Redis缓存删除: key={}, deleted={}", key, deleted);
} catch (Exception e) {
log.error("Redis缓存删除失败: key={}, error={}", key, e.getMessage(), e);
}
}
@Override
public void evictByPrefix(String prefix) {
if (prefix == null || prefix.trim().isEmpty()) {
log.warn("Redis缓存前缀不能为空");
return;
}
try {
// 使用SCAN命令替代KEYS命令,避免阻塞Redis
List<String> keysToDelete = new ArrayList<>();
String pattern = CACHE_PREFIX + prefix + "*";
// 使用RedisCallback执行SCAN操作
redisTemplate.execute((RedisCallback<Void>) connection -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
.match(pattern)
.count(100)
.build())) {
while (cursor.hasNext()) {
String key = new String(cursor.next());
keysToDelete.add(key);
// 批量删除,避免一次性删除过多key
if (keysToDelete.size() >= 100) {
Long deletedCount = redisTemplate.delete(keysToDelete);
log.debug("Redis缓存批量删除: count={}", deletedCount);
keysToDelete.clear();
}
}
} catch (Exception e) {
log.error("SCAN操作异常: {}", e.getMessage(), e);
}
return null;
});
// 删除剩余的key
if (!keysToDelete.isEmpty()) {
Long deletedCount = redisTemplate.delete(keysToDelete);
log.debug("Redis缓存前缀删除完成: prefix={}, total_deleted={}", prefix, deletedCount);
}
} catch (Exception e) {
log.error("Redis缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage(), e);
}
}
@Override
public void clear() {
try {
// 使用SCAN命令替代KEYS命令,避免阻塞Redis
List<String> keysToDelete = new ArrayList<>();
String pattern = CACHE_PREFIX + "*";
long totalDeleted = 0;
// 使用RedisCallback执行SCAN操作
Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {
long deleted = 0;
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
.match(pattern)
.count(1000)
.build())) {
while (cursor.hasNext()) {
String key = new String(cursor.next());
keysToDelete.add(key);
// 批量删除,避免一次性删除过多key
if (keysToDelete.size() >= 1000) {
Long deletedCount = redisTemplate.delete(keysToDelete);
if (deletedCount != null) {
deleted += deletedCount;
}
keysToDelete.clear();
}
}
} catch (Exception e) {
log.error("SCAN操作异常: {}", e.getMessage(), e);
}
return deleted;
});
if (result != null) {
totalDeleted += result;
}
// 删除剩余的key
if (!keysToDelete.isEmpty()) {
Long deletedCount = redisTemplate.delete(keysToDelete);
if (deletedCount != null) {
totalDeleted += deletedCount;
}
}
if (totalDeleted > 0) {
log.info("Redis缓存清空成功,删除{}个key", totalDeleted);
} else {
log.info("Redis缓存清空完成,无缓存数据");
}
} catch (Exception e) {
log.error("Redis缓存清空失败: error={}", e.getMessage(), e);
}
}
@Override
public CacheStats getStats() {
try {
// Redis没有内置统计信息,这里返回基本信息
return CacheStats.builder()
.redisHits(0)
.redisMisses(0)
.totalRequests(0)
.hitRate(0.0)
.build();
} catch (Exception e) {
log.error("Redis缓存统计获取失败: error={}", e.getMessage(), e);
return CacheStats.builder().build();
}
}
@Override
public boolean exists(String key) {
if (key == null || key.trim().isEmpty()) {
return false;
}
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(CACHE_PREFIX + key));
} catch (Exception e) {
log.error("Redis缓存存在性检查失败: key={}, error={}", key, e.getMessage(), e);
return false;
}
}
@Override
public long getExpire(String key) {
if (key == null || key.trim().isEmpty()) {
return -2;
}
try {
Long expire = redisTemplate.getExpire(CACHE_PREFIX + key, TimeUnit.SECONDS);
return expire != null ? expire : -2;
} catch (Exception e) {
log.error("Redis缓存过期时间获取失败: key={}, error={}", key, e.getMessage(), e);
return -2;
}
}
}
package com.infoloop.tianting.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import java.util.concurrent.Callable;
/**
* Spring Cache适配器
* 将Spring Cache接口桥接到现有的MultiLevelCacheService
*/
@Slf4j
public class SpringCacheAdapter implements Cache {
private final String name;
private final CacheService cacheService;
private final int defaultTtl;
public SpringCacheAdapter(String name, CacheService cacheService, int defaultTtl) {
this.name = name;
this.cacheService = cacheService;
this.defaultTtl = defaultTtl;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return cacheService;
}
@Override
public ValueWrapper get(Object key) {
if (key == null) {
return null;
}
String cacheKey = generateCacheKey(key);
Object value = cacheService.get(cacheKey);
if (value != null) {
log.debug("Spring Cache命中: cache={}, key={}", name, cacheKey);
return () -> value;
}
log.debug("Spring Cache未命中: cache={}, key={}", name, cacheKey);
return null;
}
@Override
public <T> T get(Object key, Class<T> type) {
ValueWrapper wrapper = get(key);
if (wrapper == null) {
return null;
}
Object value = wrapper.get();
if (value == null) {
return null;
}
if (type.isInstance(value)) {
return type.cast(value);
}
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper wrapper = get(key);
if (wrapper != null) {
return (T) wrapper.get();
}
// 缓存未命中,执行valueLoader
try {
T value = valueLoader.call();
put(key, value);
return value;
} catch (Exception e) {
throw new RuntimeException("ValueLoader execution failed", e);
}
}
@Override
public void put(Object key, Object value) {
if (key == null) {
return;
}
String cacheKey = generateCacheKey(key);
cacheService.put(cacheKey, value, defaultTtl);
log.debug("Spring Cache设置: cache={}, key={}, ttl={}s", name, cacheKey, defaultTtl);
}
@Override
public void evict(Object key) {
if (key == null) {
return;
}
String cacheKey = generateCacheKey(key);
cacheService.delete(cacheKey);
log.debug("Spring Cache清除: cache={}, key={}", name, cacheKey);
}
@Override
public void clear() {
// 清空整个缓存区域
cacheService.deleteByPattern(name + ":*");
log.debug("Spring Cache清空: cache={}", name);
}
/**
* 生成缓存key
* 格式: cacheName:key
*/
private String generateCacheKey(Object key) {
return name + ":" + key.toString();
}
}
......@@ -10,6 +10,7 @@ import com.infoloop.rpc.meizhongyiheservice.GetCustomersByConditionRpcRequest;
import com.infoloop.rpc.meizhongyiheservice.MeiZhongYiHeServiceRpcGrpc;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
......@@ -20,6 +21,7 @@ public class MeiZhongYiHeServiceClient {
private final MeiZhongYiHeServiceRpcGrpc.MeiZhongYiHeServiceRpcBlockingStub meiZhongYiHeServiceRpcBlockingStub;
@Cacheable(value = "customerAllergies", key = "#hisCustomerId")
public GetCustomerAllergiesByCustomerIdsRpcResponse getHisCustomerAllergiesByCustomerId(
String hisCustomerId) {
final var request = GetCustomerAllergiesByCustomerIdsRpcRequest.newBuilder()
......@@ -28,6 +30,7 @@ public class MeiZhongYiHeServiceClient {
return meiZhongYiHeServiceRpcBlockingStub.getCustomerAllergiesByCustomerIds(request);
}
@Cacheable(value = "customerMedicalAdvices", key = "#contractNo")
public GetCustomerMedicalAdvicesByHospitalRecordIdsRpcResponse getHisCustomerMedicalAdvicesByContractNo(String contractNo) {
final var request = GetCustomerMedicalAdvicesByHospitalRecordIdsRpcRequest.newBuilder()
.addAllHospitalRecordIds(List.of(contractNo))
......@@ -35,6 +38,7 @@ public class MeiZhongYiHeServiceClient {
return meiZhongYiHeServiceRpcBlockingStub.getCustomerMedicalAdvicesByHospitalRecordIds(request);
}
@Cacheable(value = "customerDetails", key = "#HISCustomerId + '_' + #contractNo")
public Customer getCustomerDetailById(String HISCustomerId, String contractNo) {
final var customer = meiZhongYiHeServiceRpcBlockingStub.getCustomerDetailById(GetCustomerDetailByIdRpcRequest.newBuilder()
.setCustomerId(HISCustomerId)
......
......@@ -6,6 +6,7 @@ import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest;
import com.infoloop.tianting.SingleCOperatorRpcResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
......@@ -14,6 +15,7 @@ public class OperatorServiceRpcClient {
private final COperatorServiceProtoRpcGrpc.COperatorServiceProtoRpcBlockingStub cOperatorServiceProtoRpcBlockingStub;
@Cacheable(value = "operators", key = "'mobile_email_' + #keyword")
public SingleCOperatorRpcResponse getCOperatorByMobileOrEmail(String keyword) {
final var request = GetCOperatorByMobileOrEmailRpcRequest.newBuilder()
.setKeyword(keyword)
......@@ -21,6 +23,7 @@ public class OperatorServiceRpcClient {
return cOperatorServiceProtoRpcBlockingStub.getCOperatorByMobileOrEmail(request).getResponse();
}
@Cacheable(value = "operators", key = "'id_' + #id")
public SingleCOperatorRpcResponse getCOperatorById(int id) {
final var request = GetCOperatorByIdRpcRequest.newBuilder().setId(id).build();
return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse();
......
......@@ -6,6 +6,7 @@ import com.infoloop.tianting.SingleSkuResponse;
import com.infoloop.tianting.SkuServiceProtoRpcGrpc;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
......@@ -15,6 +16,7 @@ import java.util.List;
public class SkuServiceRpcClient {
private final SkuServiceProtoRpcGrpc.SkuServiceProtoRpcBlockingStub skuServiceProtoRpcBlockingStub;
@Cacheable(value = "skus", key = "'dish_skus_' + T(String).join('_', #ids)")
public GeDishSkuByIdsRpcResponse getDishSkusByIds(List<Integer> ids) {
final var request = GetSkusByIdsRpcRequest.newBuilder()
.addAllIds(ids)
......@@ -23,6 +25,7 @@ public class SkuServiceRpcClient {
return skuServiceProtoRpcBlockingStub.getDishSkusByIds(request);
}
@Cacheable(value = "skus", key = "'skus_' + T(String).join('_', #ids)")
public List<SingleSkuResponse> getSkusByIds(List<Integer> ids) {
final var request = GetSkusByIdsRpcRequest.newBuilder()
.addAllIds(ids)
......
......@@ -128,3 +128,26 @@ knife4j.setting.enable-swagger-models=true
knife4j.setting.enable-reload-cache-parameter=true
knife4j.setting.enable-version=true
# 缓存配置
cache.enabled=true
cache.default-ttl=300
# Caffeine本地缓存配置
cache.caffeine.initial-capacity=100
cache.caffeine.maximum-size=10000
cache.caffeine.expire-after-write=60
cache.caffeine.expire-after-access=0
cache.caffeine.record-stats=true
# Redis缓存配置
cache.redis.enabled=true
cache.redis.host=${spring.redis.host}
cache.redis.port=${spring.redis.port}
cache.redis.password=${spring.redis.password}
cache.redis.database=11
cache.redis.timeout=2000
# 缓存监控配置
cache.metrics.enabled=true
cache.metrics.step=1m
......