feat(cache): add cache dependencies and configuration for Redis and Caffeine
Showing
16 changed files
with
1328 additions
and
0 deletions
| ... | @@ -51,6 +51,14 @@ dependencies { | ... | @@ -51,6 +51,14 @@ dependencies { |
| 51 | implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.2' // Jackson XML 模块 | 51 | implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.2' // Jackson XML 模块 |
| 52 | implementation 'org.springframework.boot:spring-boot-starter-websocket'// websocket | 52 | implementation 'org.springframework.boot:spring-boot-starter-websocket'// websocket |
| 53 | 53 | ||
| 54 | + // 缓存相关依赖 | ||
| 55 | + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis | ||
| 56 | + implementation 'org.apache.commons:commons-pool2' // Redis连接池(增强版) | ||
| 57 | + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' // Caffeine本地缓存 | ||
| 58 | + implementation 'org.springframework:spring-aspects' // Spring AOP | ||
| 59 | + implementation 'io.micrometer:micrometer-core:1.14.2' // 监控指标 | ||
| 60 | + implementation 'io.micrometer:micrometer-registry-statsd:1.14.2' // StatsD监控 | ||
| 61 | + | ||
| 54 | implementation 'com.aliyun:aliyun-java-sdk-core:4.6.4' //阿里云SDK核心库 | 62 | implementation 'com.aliyun:aliyun-java-sdk-core:4.6.4' //阿里云SDK核心库 |
| 55 | implementation 'com.aliyun:aliyun-java-sdk-dysmsapi:2.2.1'//阿里云短信服务SDK | 63 | implementation 'com.aliyun:aliyun-java-sdk-dysmsapi:2.2.1'//阿里云短信服务SDK |
| 56 | implementation 'com.aliyun:aliyun-java-sdk-dm:3.3.2'//阿里云邮件服务SDK | 64 | implementation 'com.aliyun:aliyun-java-sdk-dm:3.3.2'//阿里云邮件服务SDK | ... | ... |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import com.github.benmanes.caffeine.cache.Caffeine; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 6 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | ||
| 7 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
| 8 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| 9 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| 10 | +import org.springframework.cache.CacheManager; | ||
| 11 | +import org.springframework.cache.annotation.EnableCaching; | ||
| 12 | +import org.springframework.context.annotation.Bean; | ||
| 13 | +import org.springframework.context.annotation.Configuration; | ||
| 14 | +import org.springframework.context.annotation.Primary; | ||
| 15 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 16 | + | ||
| 17 | +import java.util.concurrent.TimeUnit; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * 缓存自动配置类 | ||
| 21 | + * 在Spring Boot启动时自动配置缓存相关组件 | ||
| 22 | + */ | ||
| 23 | +@Slf4j | ||
| 24 | +@Configuration | ||
| 25 | +@EnableCaching | ||
| 26 | +@ConditionalOnClass({Caffeine.class, StringRedisTemplate.class}) | ||
| 27 | +@EnableConfigurationProperties(CacheProperties.class) | ||
| 28 | +@ConditionalOnProperty(name = "cache.enabled", havingValue = "true", matchIfMissing = true) | ||
| 29 | +public class CacheAutoConfiguration { | ||
| 30 | + | ||
| 31 | + @Autowired | ||
| 32 | + private CacheProperties cacheProperties; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 配置Caffeine缓存管理器 | ||
| 36 | + */ | ||
| 37 | + @Bean | ||
| 38 | + @ConditionalOnMissingBean | ||
| 39 | + public com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache() { | ||
| 40 | + Caffeine<Object, Object> caffeine = Caffeine.newBuilder() | ||
| 41 | + .initialCapacity(cacheProperties.getCaffeine().getInitialCapacity()) | ||
| 42 | + .maximumSize(cacheProperties.getCaffeine().getMaximumSize()) | ||
| 43 | + .expireAfterWrite(cacheProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.SECONDS); | ||
| 44 | + | ||
| 45 | + // 如果配置了访问后过期时间,则启用 | ||
| 46 | + if (cacheProperties.getCaffeine().getExpireAfterAccess() > 0) { | ||
| 47 | + caffeine.expireAfterAccess(cacheProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.SECONDS); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + // 优化并发性能:使用调用线程执行过期任务,减少线程切换开销 | ||
| 51 | + caffeine.executor(Runnable::run); | ||
| 52 | + | ||
| 53 | + if (cacheProperties.getCaffeine().isRecordStats()) { | ||
| 54 | + caffeine.recordStats(); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + log.info("Caffeine缓存配置完成: initialCapacity={}, maximumSize={}, expireAfterWrite={}s, expireAfterAccess={}s", | ||
| 58 | + cacheProperties.getCaffeine().getInitialCapacity(), | ||
| 59 | + cacheProperties.getCaffeine().getMaximumSize(), | ||
| 60 | + cacheProperties.getCaffeine().getExpireAfterWrite(), | ||
| 61 | + cacheProperties.getCaffeine().getExpireAfterAccess()); | ||
| 62 | + | ||
| 63 | + return caffeine.build(); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * 配置Caffeine缓存服务 | ||
| 69 | + */ | ||
| 70 | + @Bean("caffeineCacheService") | ||
| 71 | + @ConditionalOnMissingBean(name = "caffeineCacheService") | ||
| 72 | + public CaffeineCacheService caffeineCacheService(com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache) { | ||
| 73 | + log.info("Caffeine缓存服务配置完成"); | ||
| 74 | + return new CaffeineCacheService(caffeineCache); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 配置ObjectMapper用于JSON序列化 | ||
| 79 | + * 针对Lombok @Data类优化,支持标准的序列化/反序列化 | ||
| 80 | + */ | ||
| 81 | + @Bean("cacheObjectMapper") | ||
| 82 | + @ConditionalOnMissingBean(name = "cacheObjectMapper") | ||
| 83 | + public com.fasterxml.jackson.databind.ObjectMapper cacheObjectMapper() { | ||
| 84 | + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); | ||
| 85 | + mapper.findAndRegisterModules(); // 注册所有模块,包括Java 8时间模块 | ||
| 86 | + | ||
| 87 | + // 基本配置 | ||
| 88 | + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | ||
| 89 | + mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS, false); | ||
| 90 | + mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); | ||
| 91 | + mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_SELF_REFERENCES, false); | ||
| 92 | + | ||
| 93 | + // 为Lombok @Data类启用类型信息,这样可以正确反序列化 | ||
| 94 | + mapper.activateDefaultTyping( | ||
| 95 | + com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator.builder() | ||
| 96 | + .allowIfSubType(Object.class) | ||
| 97 | + .build(), | ||
| 98 | + com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL, | ||
| 99 | + com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY | ||
| 100 | + ); | ||
| 101 | + | ||
| 102 | + log.info("缓存专用ObjectMapper配置完成(优化支持Lombok @Data类)"); | ||
| 103 | + return mapper; | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 配置Redis缓存服务 | ||
| 108 | + */ | ||
| 109 | + @Bean("redisCacheService") | ||
| 110 | + @ConditionalOnMissingBean(name = "redisCacheService") | ||
| 111 | + public RedisCacheService redisCacheService(StringRedisTemplate redisTemplate, | ||
| 112 | + @org.springframework.beans.factory.annotation.Qualifier("cacheObjectMapper") | ||
| 113 | + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { | ||
| 114 | + log.info("Redis缓存服务配置完成"); | ||
| 115 | + return new RedisCacheService(redisTemplate, objectMapper); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /** | ||
| 119 | + * 配置缓存服务(主要使用多级缓存) | ||
| 120 | + */ | ||
| 121 | + @Bean("multiLevelCacheService") | ||
| 122 | + @ConditionalOnMissingBean(name = "multiLevelCacheService") | ||
| 123 | + public MultiLevelCacheService multiLevelCacheService( | ||
| 124 | + CaffeineCacheService caffeineCacheService, | ||
| 125 | + RedisCacheService redisCacheService, | ||
| 126 | + CacheProperties cacheProperties) { | ||
| 127 | + log.info("多级缓存服务配置完成"); | ||
| 128 | + return new MultiLevelCacheService(caffeineCacheService, redisCacheService, cacheProperties); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * 配置Spring Cache管理器 - 桥接到多级缓存服务 | ||
| 133 | + */ | ||
| 134 | + @Bean | ||
| 135 | + @Primary | ||
| 136 | + @ConditionalOnMissingBean(CacheManager.class) | ||
| 137 | + public CacheManager cacheManager( | ||
| 138 | + @org.springframework.beans.factory.annotation.Qualifier("multiLevelCacheService") | ||
| 139 | + CacheService cacheService, | ||
| 140 | + CacheProperties cacheProperties) { | ||
| 141 | + log.info("Spring Cache管理器配置完成,桥接到多级缓存服务"); | ||
| 142 | + return new MultiLevelCacheManager(cacheService, cacheProperties); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + /** | ||
| 146 | + * 配置缓存健康检查 | ||
| 147 | + */ | ||
| 148 | + @Bean | ||
| 149 | + @ConditionalOnMissingBean | ||
| 150 | + public CacheHealthIndicator cacheHealthIndicator() { | ||
| 151 | + log.info("缓存健康检查配置完成"); | ||
| 152 | + return new CacheHealthIndicator(); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + /** | ||
| 156 | + * 配置缓存管理控制器 | ||
| 157 | + */ | ||
| 158 | + @Bean | ||
| 159 | + @ConditionalOnMissingBean | ||
| 160 | + public CacheManagementController cacheManagementController() { | ||
| 161 | + log.info("缓存管理控制器配置完成"); | ||
| 162 | + return new CacheManagementController(); | ||
| 163 | + } | ||
| 164 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 5 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
| 6 | +import org.springframework.boot.actuate.health.Health; | ||
| 7 | +import org.springframework.boot.actuate.health.HealthIndicator; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 缓存健康检查指示器 | ||
| 11 | + */ | ||
| 12 | +@Slf4j | ||
| 13 | +public class CacheHealthIndicator implements HealthIndicator { | ||
| 14 | + | ||
| 15 | + @Autowired | ||
| 16 | + @Qualifier("multiLevelCacheService") | ||
| 17 | + private CacheService cacheService; | ||
| 18 | + | ||
| 19 | + @Override | ||
| 20 | + public Health health() { | ||
| 21 | + try { | ||
| 22 | + // 执行简单的缓存操作来检查健康状态 | ||
| 23 | + String testKey = "health_check_" + System.currentTimeMillis(); | ||
| 24 | + cacheService.put(testKey, "test", 10); | ||
| 25 | + Object value = cacheService.get(testKey); | ||
| 26 | + cacheService.evict(testKey); | ||
| 27 | + | ||
| 28 | + if ("test".equals(value)) { | ||
| 29 | + return Health.up() | ||
| 30 | + .withDetail("status", "缓存服务正常") | ||
| 31 | + .build(); | ||
| 32 | + } else { | ||
| 33 | + return Health.down() | ||
| 34 | + .withDetail("status", "缓存读写异常") | ||
| 35 | + .build(); | ||
| 36 | + } | ||
| 37 | + } catch (Exception e) { | ||
| 38 | + return Health.down() | ||
| 39 | + .withDetail("status", "缓存服务异常") | ||
| 40 | + .withDetail("error", e.getMessage()) | ||
| 41 | + .build(); | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 5 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
| 6 | +import org.springframework.http.ResponseEntity; | ||
| 7 | +import org.springframework.web.bind.annotation.*; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 缓存管理控制器 | ||
| 11 | + * 提供缓存操作的REST端点 | ||
| 12 | + */ | ||
| 13 | +@Slf4j | ||
| 14 | +@RestController | ||
| 15 | +@RequestMapping("/actuator/cache") | ||
| 16 | +public class CacheManagementController { | ||
| 17 | + | ||
| 18 | + @Autowired | ||
| 19 | + @Qualifier("multiLevelCacheService") | ||
| 20 | + private CacheService cacheService; | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 获取缓存统计信息 | ||
| 24 | + */ | ||
| 25 | + @GetMapping("/stats") | ||
| 26 | + public CacheStats getCacheStats() { | ||
| 27 | + return cacheService.getStats(); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 清除指定缓存 | ||
| 32 | + */ | ||
| 33 | + @DeleteMapping("/evict") | ||
| 34 | + public ResponseEntity<?> evictCache(@RequestParam String key) { | ||
| 35 | + try { | ||
| 36 | + cacheService.evict(key); | ||
| 37 | + log.info("缓存清除成功: {}", key); | ||
| 38 | + return ResponseEntity.ok("缓存已清除: " + key); | ||
| 39 | + } catch (Exception e) { | ||
| 40 | + log.error("缓存清除失败: key={}, error={}", key, e.getMessage(), e); | ||
| 41 | + return ResponseEntity.internalServerError().body("缓存清除失败: " + e.getMessage()); | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 根据前缀清除缓存 | ||
| 47 | + */ | ||
| 48 | + @DeleteMapping("/evict/prefix") | ||
| 49 | + public ResponseEntity<?> evictByPrefix(@RequestParam String prefix) { | ||
| 50 | + try { | ||
| 51 | + cacheService.evictByPrefix(prefix); | ||
| 52 | + log.info("前缀缓存清除成功: {}", prefix); | ||
| 53 | + return ResponseEntity.ok("前缀缓存已清除: " + prefix); | ||
| 54 | + } catch (Exception e) { | ||
| 55 | + log.error("前缀缓存清除失败: prefix={}, error={}", prefix, e.getMessage(), e); | ||
| 56 | + return ResponseEntity.internalServerError().body("前缀缓存清除失败: " + e.getMessage()); | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /** | ||
| 61 | + * 清空所有缓存 | ||
| 62 | + */ | ||
| 63 | + @DeleteMapping("/clear") | ||
| 64 | + public ResponseEntity<?> clearCache() { | ||
| 65 | + try { | ||
| 66 | + cacheService.clear(); | ||
| 67 | + log.info("所有缓存清空成功"); | ||
| 68 | + return ResponseEntity.ok("所有缓存已清除"); | ||
| 69 | + } catch (Exception e) { | ||
| 70 | + log.error("缓存清空失败: error={}", e.getMessage(), e); | ||
| 71 | + return ResponseEntity.internalServerError().body("缓存清空失败: " + e.getMessage()); | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /** | ||
| 76 | + * 检查缓存是否存在 | ||
| 77 | + */ | ||
| 78 | + @GetMapping("/exists") | ||
| 79 | + public ResponseEntity<?> checkExists(@RequestParam String key) { | ||
| 80 | + try { | ||
| 81 | + boolean exists = cacheService.exists(key); | ||
| 82 | + return ResponseEntity.ok(String.format("缓存key '%s' %s", key, exists ? "存在" : "不存在")); | ||
| 83 | + } catch (Exception e) { | ||
| 84 | + log.error("缓存存在性检查失败: key={}, error={}", key, e.getMessage(), e); | ||
| 85 | + return ResponseEntity.internalServerError().body("缓存检查失败: " + e.getMessage()); | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + /** | ||
| 90 | + * 获取缓存过期时间 | ||
| 91 | + */ | ||
| 92 | + @GetMapping("/expire") | ||
| 93 | + public ResponseEntity<?> getExpire(@RequestParam String key) { | ||
| 94 | + try { | ||
| 95 | + long expire = cacheService.getExpire(key); | ||
| 96 | + String message; | ||
| 97 | + if (expire == -2) { | ||
| 98 | + message = "缓存key不存在"; | ||
| 99 | + } else if (expire == -1) { | ||
| 100 | + message = "缓存key永久有效"; | ||
| 101 | + } else { | ||
| 102 | + message = String.format("缓存key剩余时间: %d秒", expire); | ||
| 103 | + } | ||
| 104 | + return ResponseEntity.ok(message); | ||
| 105 | + } catch (Exception e) { | ||
| 106 | + log.error("缓存过期时间获取失败: key={}, error={}", key, e.getMessage(), e); | ||
| 107 | + return ResponseEntity.internalServerError().body("缓存过期时间获取失败: " + e.getMessage()); | ||
| 108 | + } | ||
| 109 | + } | ||
| 110 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | +import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 缓存配置属性 | ||
| 8 | + */ | ||
| 9 | +@Data | ||
| 10 | +@ConfigurationProperties(prefix = "cache") | ||
| 11 | +public class CacheProperties { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 是否启用缓存 | ||
| 15 | + */ | ||
| 16 | + private boolean enabled = true; | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 默认缓存过期时间(秒) | ||
| 20 | + */ | ||
| 21 | + private int defaultTtl = 300; | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * Caffeine本地缓存配置 | ||
| 25 | + */ | ||
| 26 | + private CaffeineProperties caffeine = new CaffeineProperties(); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * Redis缓存配置 | ||
| 30 | + */ | ||
| 31 | + private RedisProperties redis = new RedisProperties(); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 监控配置 | ||
| 35 | + */ | ||
| 36 | + private MetricsProperties metrics = new MetricsProperties(); | ||
| 37 | + | ||
| 38 | + @Data | ||
| 39 | + public static class CaffeineProperties { | ||
| 40 | + /** | ||
| 41 | + * 初始容量 | ||
| 42 | + */ | ||
| 43 | + private int initialCapacity = 100; | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 最大容量 | ||
| 47 | + */ | ||
| 48 | + private int maximumSize = 10000; | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 写入后过期时间(秒) | ||
| 52 | + */ | ||
| 53 | + private int expireAfterWrite = 60; | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 访问后过期时间(秒),0表示不启用 | ||
| 57 | + */ | ||
| 58 | + private int expireAfterAccess = 0; | ||
| 59 | + | ||
| 60 | + /** | ||
| 61 | + * 是否记录统计信息 | ||
| 62 | + */ | ||
| 63 | + private boolean recordStats = true; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Data | ||
| 67 | + public static class RedisProperties { | ||
| 68 | + /** | ||
| 69 | + * 是否启用Redis缓存 | ||
| 70 | + */ | ||
| 71 | + private boolean enabled = true; | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * Redis主机 | ||
| 75 | + */ | ||
| 76 | + private String host = "localhost"; | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * Redis端口 | ||
| 80 | + */ | ||
| 81 | + private int port = 6379; | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * Redis密码 | ||
| 85 | + */ | ||
| 86 | + private String password; | ||
| 87 | + | ||
| 88 | + /** | ||
| 89 | + * Redis数据库索引 | ||
| 90 | + */ | ||
| 91 | + private int database = 1; | ||
| 92 | + | ||
| 93 | + /** | ||
| 94 | + * 连接超时时间(毫秒) | ||
| 95 | + */ | ||
| 96 | + private int timeout = 2000; | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + @Data | ||
| 100 | + public static class MetricsProperties { | ||
| 101 | + /** | ||
| 102 | + * 是否启用监控 | ||
| 103 | + */ | ||
| 104 | + private boolean enabled = true; | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 监控步长 | ||
| 108 | + */ | ||
| 109 | + private String step = "1m"; | ||
| 110 | + } | ||
| 111 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 缓存服务接口 | ||
| 5 | + * 定义了缓存的基本操作方法 | ||
| 6 | + */ | ||
| 7 | +public interface CacheService { | ||
| 8 | + | ||
| 9 | + /** | ||
| 10 | + * 获取缓存 | ||
| 11 | + * @param key 缓存key | ||
| 12 | + * @return 缓存的值,如果不存在返回null | ||
| 13 | + */ | ||
| 14 | + Object get(String key); | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 设置缓存 | ||
| 18 | + * @param key 缓存key | ||
| 19 | + * @param value 缓存值 | ||
| 20 | + * @param ttlSeconds 过期时间(秒) | ||
| 21 | + */ | ||
| 22 | + void put(String key, Object value, int ttlSeconds); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 删除缓存 | ||
| 26 | + * @param key 缓存key | ||
| 27 | + */ | ||
| 28 | + void evict(String key); | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 删除缓存(别名方法) | ||
| 32 | + * @param key 缓存key | ||
| 33 | + */ | ||
| 34 | + default void delete(String key) { | ||
| 35 | + evict(key); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 根据前缀删除缓存 | ||
| 40 | + * @param prefix 缓存key前缀 | ||
| 41 | + */ | ||
| 42 | + void evictByPrefix(String prefix); | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 根据模式删除缓存 | ||
| 46 | + * @param pattern 缓存key模式(支持通配符*) | ||
| 47 | + */ | ||
| 48 | + default void deleteByPattern(String pattern) { | ||
| 49 | + evictByPrefix(pattern.replace("*", "")); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 清空所有缓存 | ||
| 54 | + */ | ||
| 55 | + void clear(); | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 获取缓存统计信息 | ||
| 59 | + * @return 缓存统计信息 | ||
| 60 | + */ | ||
| 61 | + CacheStats getStats(); | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 检查缓存是否存在 | ||
| 65 | + * @param key 缓存key | ||
| 66 | + * @return true表示存在,false表示不存在 | ||
| 67 | + */ | ||
| 68 | + boolean exists(String key); | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 获取缓存剩余过期时间 | ||
| 72 | + * @param key 缓存key | ||
| 73 | + * @return 剩余时间(秒),-2表示key不存在,-1表示永久 | ||
| 74 | + */ | ||
| 75 | + long getExpire(String key); | ||
| 76 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.Builder; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 缓存统计信息 | ||
| 8 | + */ | ||
| 9 | +@Data | ||
| 10 | +@Builder | ||
| 11 | +public class CacheStats { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 本地缓存命中次数 | ||
| 15 | + */ | ||
| 16 | + private long localHits; | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 本地缓存未命中次数 | ||
| 20 | + */ | ||
| 21 | + private long localMisses; | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * Redis缓存命中次数 | ||
| 25 | + */ | ||
| 26 | + private long redisHits; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * Redis缓存未命中次数 | ||
| 30 | + */ | ||
| 31 | + private long redisMisses; | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 总请求次数 | ||
| 35 | + */ | ||
| 36 | + private long totalRequests; | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 缓存命中率 | ||
| 40 | + */ | ||
| 41 | + private double hitRate; | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 本地缓存大小 | ||
| 45 | + */ | ||
| 46 | + private long localSize; | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * Redis缓存大小(如果可获取) | ||
| 50 | + */ | ||
| 51 | + private long redisSize; | ||
| 52 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import com.github.benmanes.caffeine.cache.Cache; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * Caffeine本地缓存服务实现 | ||
| 8 | + */ | ||
| 9 | +@Slf4j | ||
| 10 | +public class CaffeineCacheService implements CacheService { | ||
| 11 | + | ||
| 12 | + private final Cache<String, Object> cache; | ||
| 13 | + | ||
| 14 | + public CaffeineCacheService(Cache<String, Object> cache) { | ||
| 15 | + this.cache = cache; | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + @Override | ||
| 19 | + public Object get(String key) { | ||
| 20 | + return cache.getIfPresent(key); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + @Override | ||
| 24 | + public void put(String key, Object value, int ttlSeconds) { | ||
| 25 | + // Caffeine不支持单个key的TTL,使用全局配置的过期时间 | ||
| 26 | + cache.put(key, value); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public void evict(String key) { | ||
| 31 | + cache.invalidate(key); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public void evictByPrefix(String prefix) { | ||
| 36 | + cache.asMap().keySet().removeIf(key -> key.startsWith(prefix)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Override | ||
| 40 | + public void clear() { | ||
| 41 | + cache.invalidateAll(); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @Override | ||
| 45 | + public CacheStats getStats() { | ||
| 46 | + com.github.benmanes.caffeine.cache.stats.CacheStats caffeineStats = cache.stats(); | ||
| 47 | + return CacheStats.builder() | ||
| 48 | + .localHits(caffeineStats.hitCount()) | ||
| 49 | + .localMisses(caffeineStats.missCount()) | ||
| 50 | + .totalRequests(caffeineStats.requestCount()) | ||
| 51 | + .hitRate(caffeineStats.hitRate()) | ||
| 52 | + .localSize(cache.estimatedSize()) | ||
| 53 | + .build(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public boolean exists(String key) { | ||
| 58 | + return cache.getIfPresent(key) != null; | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public long getExpire(String key) { | ||
| 63 | + // Caffeine不支持获取单个key的过期时间 | ||
| 64 | + return exists(key) ? -1 : -2; | ||
| 65 | + } | ||
| 66 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | +import org.springframework.cache.Cache; | ||
| 5 | +import org.springframework.cache.CacheManager; | ||
| 6 | + | ||
| 7 | +import java.util.Collection; | ||
| 8 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 9 | +import java.util.concurrent.ConcurrentMap; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 多级缓存管理器 | ||
| 13 | + * 实现Spring Cache CacheManager接口,桥接到现有的MultiLevelCacheService | ||
| 14 | + */ | ||
| 15 | +@Slf4j | ||
| 16 | +public class MultiLevelCacheManager implements CacheManager { | ||
| 17 | + | ||
| 18 | + private final CacheService cacheService; | ||
| 19 | + private final CacheProperties cacheProperties; | ||
| 20 | + private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(); | ||
| 21 | + | ||
| 22 | + public MultiLevelCacheManager(CacheService cacheService, CacheProperties cacheProperties) { | ||
| 23 | + this.cacheService = cacheService; | ||
| 24 | + this.cacheProperties = cacheProperties; | ||
| 25 | + log.info("多级缓存管理器初始化完成,默认TTL: {}s", cacheProperties.getDefaultTtl()); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + @Override | ||
| 29 | + public Cache getCache(String name) { | ||
| 30 | + if (name == null) { | ||
| 31 | + return null; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + return cacheMap.computeIfAbsent(name, cacheName -> { | ||
| 35 | + log.debug("创建新的缓存区域: {}", cacheName); | ||
| 36 | + return new SpringCacheAdapter(cacheName, cacheService, cacheProperties.getDefaultTtl()); | ||
| 37 | + }); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public Collection<String> getCacheNames() { | ||
| 42 | + return cacheMap.keySet(); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 清除所有缓存 | ||
| 47 | + */ | ||
| 48 | + public void clearAll() { | ||
| 49 | + cacheMap.values().forEach(Cache::clear); | ||
| 50 | + log.info("已清除所有缓存区域"); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 获取缓存统计信息 | ||
| 55 | + */ | ||
| 56 | + public String getCacheStats() { | ||
| 57 | + StringBuilder stats = new StringBuilder(); | ||
| 58 | + stats.append("缓存区域数量: ").append(cacheMap.size()).append("\n"); | ||
| 59 | + stats.append("缓存区域列表: ").append(String.join(", ", getCacheNames())); | ||
| 60 | + return stats.toString(); | ||
| 61 | + } | ||
| 62 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 多级缓存服务实现 | ||
| 7 | + * 结合Caffeine本地缓存和Redis分布式缓存 | ||
| 8 | + * 实现L1 + L2缓存架构 | ||
| 9 | + * | ||
| 10 | + * 缓存逻辑说明: | ||
| 11 | + * 1. 查询逻辑:先查Caffeine(60s) -> 未命中再查Redis -> 未命中再查数据库 | ||
| 12 | + * 2. 写入逻辑:先写Redis(业务TTL) -> 再写Caffeine(固定60s) | ||
| 13 | + * 3. Caffeine过期后正确从Redis读取数据,避免直接查数据库 | ||
| 14 | + */ | ||
| 15 | +@Slf4j | ||
| 16 | +public class MultiLevelCacheService implements CacheService { | ||
| 17 | + | ||
| 18 | + private final CaffeineCacheService caffeineCacheService; | ||
| 19 | + private final RedisCacheService redisCacheService; | ||
| 20 | + private final CacheProperties cacheProperties; | ||
| 21 | + | ||
| 22 | + // 缓存空值的特殊标记 | ||
| 23 | + private static final String NULL_VALUE_MARKER = "$$NULL$$"; | ||
| 24 | + | ||
| 25 | + public MultiLevelCacheService(CaffeineCacheService caffeineCacheService, | ||
| 26 | + RedisCacheService redisCacheService, | ||
| 27 | + CacheProperties cacheProperties) { | ||
| 28 | + this.caffeineCacheService = caffeineCacheService; | ||
| 29 | + this.redisCacheService = redisCacheService; | ||
| 30 | + this.cacheProperties = cacheProperties; | ||
| 31 | + log.info("多级缓存服务初始化完成,Caffeine TTL: {}s", cacheProperties.getCaffeine().getExpireAfterWrite()); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public Object get(String key) { | ||
| 36 | + if (key == null || key.trim().isEmpty()) { | ||
| 37 | + return null; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + log.debug("开始缓存查询: key={}", key); | ||
| 41 | + | ||
| 42 | + // 1. 先查本地缓存(Caffeine - L1缓存) | ||
| 43 | + Object localValue = caffeineCacheService.get(key); | ||
| 44 | + if (localValue != null) { | ||
| 45 | + if (NULL_VALUE_MARKER.equals(localValue)) { | ||
| 46 | + log.debug("本地缓存命中(空值): {}", key); | ||
| 47 | + return null; | ||
| 48 | + } | ||
| 49 | + log.debug("本地缓存命中: {}", key); | ||
| 50 | + | ||
| 51 | + // 检查Redis中是否也存在,如果不存在则回写 | ||
| 52 | + if (!redisCacheService.exists(key)) { | ||
| 53 | + try { | ||
| 54 | + // 使用默认TTL回写到Redis | ||
| 55 | + int defaultRedisTtl = cacheProperties.getDefaultTtl(); | ||
| 56 | + redisCacheService.put(key, localValue, defaultRedisTtl); | ||
| 57 | + log.debug("本地缓存命中但Redis缺失,已回写: key={}, ttl={}s", key, defaultRedisTtl); | ||
| 58 | + } catch (Exception e) { | ||
| 59 | + log.error("Redis回写失败: key={}, error={}", key, e.getMessage()); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + return localValue; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // 2. 本地缓存未命中,查分布式缓存(Redis - L2缓存) | ||
| 66 | + log.debug("Caffeine未命中,检查Redis分布式缓存: key={}", key); | ||
| 67 | + | ||
| 68 | + Object redisValue = redisCacheService.get(key); | ||
| 69 | + if (redisValue != null) { | ||
| 70 | + if (NULL_VALUE_MARKER.equals(redisValue)) { | ||
| 71 | + // 将空值标记同步到本地缓存,避免重复查询 | ||
| 72 | + caffeineCacheService.put(key, NULL_VALUE_MARKER, cacheProperties.getCaffeine().getExpireAfterWrite()); | ||
| 73 | + log.debug("分布式缓存命中(空值),并同步到本地缓存: {}", key); | ||
| 74 | + return null; | ||
| 75 | + } | ||
| 76 | + // 同步到本地缓存 | ||
| 77 | + caffeineCacheService.put(key, redisValue, cacheProperties.getCaffeine().getExpireAfterWrite()); | ||
| 78 | + log.debug("分布式缓存命中,返回Redis数据: key={}", key); | ||
| 79 | + return redisValue; | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + // 3. 两级缓存都未命中,需要查询数据库 | ||
| 83 | + log.debug("两级缓存都未命中,将查询数据库: key={}", key); | ||
| 84 | + return null; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Override | ||
| 88 | + public void put(String key, Object value, int ttlSeconds) { | ||
| 89 | + if (key == null || key.trim().isEmpty()) { | ||
| 90 | + log.error("缓存key不能为空"); | ||
| 91 | + return; | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + if (ttlSeconds <= 0) { | ||
| 95 | + log.error("缓存TTL必须大于0: key={}, ttl={}", key, ttlSeconds); | ||
| 96 | + return; | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + // 处理空值缓存 | ||
| 100 | + Object actualValue = (value == null) ? NULL_VALUE_MARKER : value; | ||
| 101 | + | ||
| 102 | + // 写入策略:先写Redis,再写Caffeine | ||
| 103 | + boolean redisSuccess = false; | ||
| 104 | + boolean localSuccess = false; | ||
| 105 | + | ||
| 106 | + try { | ||
| 107 | + // 写入分布式缓存Redis,使用注解指定的TTL | ||
| 108 | + redisCacheService.put(key, actualValue, ttlSeconds); | ||
| 109 | + redisSuccess = true; | ||
| 110 | + log.debug("分布式缓存写入成功: key={}, ttl={}s", key, ttlSeconds); | ||
| 111 | + } catch (Exception e) { | ||
| 112 | + log.error("分布式缓存写入失败: key={}, error={}", key, e.getMessage(), e); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + try { | ||
| 116 | + // 2. 写入本地缓存Caffeine,使用配置的过期时间 | ||
| 117 | + int caffeineTtl = cacheProperties.getCaffeine().getExpireAfterWrite(); | ||
| 118 | + caffeineCacheService.put(key, actualValue, caffeineTtl); | ||
| 119 | + localSuccess = true; | ||
| 120 | + log.debug("本地缓存写入成功: key={}, ttl={}s", key, caffeineTtl); | ||
| 121 | + } catch (Exception e) { | ||
| 122 | + log.error("本地缓存写入失败: key={}, error={}", key, e.getMessage(), e); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + if (!localSuccess && !redisSuccess) { | ||
| 126 | + log.error("缓存写入完全失败: key={}", key); | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Override | ||
| 131 | + public void evict(String key) { | ||
| 132 | + // 双删策略:同时删除本地缓存和分布式缓存 | ||
| 133 | + try { | ||
| 134 | + caffeineCacheService.evict(key); | ||
| 135 | + log.debug("本地缓存删除成功: {}", key); | ||
| 136 | + } catch (Exception e) { | ||
| 137 | + log.error("本地缓存删除失败: key={}, error={}", key, e.getMessage()); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + try { | ||
| 141 | + redisCacheService.evict(key); | ||
| 142 | + log.debug("分布式缓存删除成功: {}", key); | ||
| 143 | + } catch (Exception e) { | ||
| 144 | + log.error("分布式缓存删除失败: key={}, error={}", key, e.getMessage()); | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + @Override | ||
| 149 | + public void evictByPrefix(String prefix) { | ||
| 150 | + // 双删策略:同时删除本地缓存和分布式缓存的前缀 | ||
| 151 | + try { | ||
| 152 | + caffeineCacheService.evictByPrefix(prefix); | ||
| 153 | + log.debug("本地缓存前缀删除成功: {}", prefix); | ||
| 154 | + } catch (Exception e) { | ||
| 155 | + log.error("本地缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage()); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + try { | ||
| 159 | + redisCacheService.evictByPrefix(prefix); | ||
| 160 | + log.debug("分布式缓存前缀删除成功: {}", prefix); | ||
| 161 | + } catch (Exception e) { | ||
| 162 | + log.error("分布式缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage()); | ||
| 163 | + } | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + @Override | ||
| 167 | + public void clear() { | ||
| 168 | + // 清空所有缓存 | ||
| 169 | + try { | ||
| 170 | + caffeineCacheService.clear(); | ||
| 171 | + log.info("本地缓存清空成功"); | ||
| 172 | + } catch (Exception e) { | ||
| 173 | + log.error("本地缓存清空失败: error={}", e.getMessage()); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + try { | ||
| 177 | + redisCacheService.clear(); | ||
| 178 | + log.info("分布式缓存清空成功"); | ||
| 179 | + } catch (Exception e) { | ||
| 180 | + log.error("分布式缓存清空失败: error={}", e.getMessage()); | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + @Override | ||
| 185 | + public CacheStats getStats() { | ||
| 186 | + CacheStats localStats = caffeineCacheService.getStats(); | ||
| 187 | + CacheStats redisStats = redisCacheService.getStats(); | ||
| 188 | + | ||
| 189 | + return CacheStats.builder() | ||
| 190 | + .localHits(localStats.getLocalHits()) | ||
| 191 | + .localMisses(localStats.getLocalMisses()) | ||
| 192 | + .redisHits(redisStats.getRedisHits()) | ||
| 193 | + .redisMisses(redisStats.getRedisMisses()) | ||
| 194 | + .totalRequests(calculateTotalRequests(localStats, redisStats)) | ||
| 195 | + .hitRate(calculateHitRate(localStats, redisStats)) | ||
| 196 | + .localSize(localStats.getLocalSize()) | ||
| 197 | + .build(); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + @Override | ||
| 201 | + public boolean exists(String key) { | ||
| 202 | + // 只要任一缓存存在就返回true | ||
| 203 | + return caffeineCacheService.exists(key) || redisCacheService.exists(key); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + @Override | ||
| 207 | + public long getExpire(String key) { | ||
| 208 | + // 优先返回分布式缓存的过期时间 | ||
| 209 | + long redisExpire = redisCacheService.getExpire(key); | ||
| 210 | + if (redisExpire >= 0) { | ||
| 211 | + return redisExpire; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + // 如果Redis没有,则返回本地缓存的过期时间 | ||
| 215 | + return caffeineCacheService.getExpire(key); | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + private long calculateTotalRequests(CacheStats localStats, CacheStats redisStats) { | ||
| 219 | + return localStats.getLocalHits() + localStats.getLocalMisses() + | ||
| 220 | + redisStats.getRedisHits() + redisStats.getRedisMisses(); | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + private double calculateHitRate(CacheStats localStats, CacheStats redisStats) { | ||
| 224 | + long totalHits = localStats.getLocalHits() + redisStats.getRedisHits(); | ||
| 225 | + long totalRequests = calculateTotalRequests(localStats, redisStats); | ||
| 226 | + return totalRequests > 0 ? (double) totalHits / totalRequests : 0.0; | ||
| 227 | + } | ||
| 228 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.core.type.TypeReference; | ||
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | +import org.springframework.data.redis.core.Cursor; | ||
| 7 | +import org.springframework.data.redis.core.RedisCallback; | ||
| 8 | +import org.springframework.data.redis.core.ScanOptions; | ||
| 9 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 10 | + | ||
| 11 | +import java.time.Duration; | ||
| 12 | +import java.util.ArrayList; | ||
| 13 | +import java.util.List; | ||
| 14 | +import java.util.concurrent.TimeUnit; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * Redis分布式缓存服务实现(优化版) | ||
| 18 | + * 包含缓存穿透保护、性能优化等功能 | ||
| 19 | + */ | ||
| 20 | +@Slf4j | ||
| 21 | +public class RedisCacheService implements CacheService { | ||
| 22 | + | ||
| 23 | + private final StringRedisTemplate redisTemplate; | ||
| 24 | + private final ObjectMapper objectMapper; | ||
| 25 | + private static final String CACHE_PREFIX = "cache:"; | ||
| 26 | + private static final String NULL_VALUE_MARKER = "::NULL::"; | ||
| 27 | + | ||
| 28 | + public RedisCacheService(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { | ||
| 29 | + this.redisTemplate = redisTemplate; | ||
| 30 | + this.objectMapper = objectMapper; | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + @Override | ||
| 34 | + public Object get(String key) { | ||
| 35 | + if (key == null || key.trim().isEmpty()) { | ||
| 36 | + log.warn("Redis缓存key不能为空"); | ||
| 37 | + return null; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + try { | ||
| 41 | + String jsonValue = redisTemplate.opsForValue().get(CACHE_PREFIX + key); | ||
| 42 | + if (jsonValue != null) { | ||
| 43 | + // 处理null值标记,防止缓存穿透 | ||
| 44 | + if (NULL_VALUE_MARKER.equals(jsonValue)) { | ||
| 45 | + return null; | ||
| 46 | + } | ||
| 47 | + // 使用TypeReference保持类型信息 | ||
| 48 | + return objectMapper.readValue(jsonValue, new TypeReference<Object>() {}); | ||
| 49 | + } | ||
| 50 | + } catch (Exception e) { | ||
| 51 | + log.error("Redis缓存读取失败: key={}, error={}", key, e.getMessage(), e); | ||
| 52 | + } | ||
| 53 | + return null; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void put(String key, Object value, int ttlSeconds) { | ||
| 58 | + if (key == null || key.trim().isEmpty()) { | ||
| 59 | + log.warn("Redis缓存key不能为空"); | ||
| 60 | + return; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + if (ttlSeconds <= 0) { | ||
| 64 | + log.warn("Redis缓存TTL必须大于0: ttl={}", ttlSeconds); | ||
| 65 | + return; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + try { | ||
| 69 | + String jsonValue; | ||
| 70 | + if (value == null) { | ||
| 71 | + // 缓存null值,防止缓存穿透 | ||
| 72 | + jsonValue = NULL_VALUE_MARKER; | ||
| 73 | + } else { | ||
| 74 | + jsonValue = objectMapper.writeValueAsString(value); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + redisTemplate.opsForValue().set(CACHE_PREFIX + key, jsonValue, | ||
| 78 | + Duration.ofSeconds(ttlSeconds)); | ||
| 79 | + log.debug("Redis缓存设置成功: key={}, ttl={}s", key, ttlSeconds); | ||
| 80 | + } catch (Exception e) { | ||
| 81 | + log.error("Redis缓存写入失败: key={}, error={}", key, e.getMessage(), e); | ||
| 82 | + } | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + @Override | ||
| 86 | + public void evict(String key) { | ||
| 87 | + if (key == null || key.trim().isEmpty()) { | ||
| 88 | + log.warn("Redis缓存key不能为空"); | ||
| 89 | + return; | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + try { | ||
| 93 | + Boolean deleted = redisTemplate.delete(CACHE_PREFIX + key); | ||
| 94 | + log.debug("Redis缓存删除: key={}, deleted={}", key, deleted); | ||
| 95 | + } catch (Exception e) { | ||
| 96 | + log.error("Redis缓存删除失败: key={}, error={}", key, e.getMessage(), e); | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @Override | ||
| 101 | + public void evictByPrefix(String prefix) { | ||
| 102 | + if (prefix == null || prefix.trim().isEmpty()) { | ||
| 103 | + log.warn("Redis缓存前缀不能为空"); | ||
| 104 | + return; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + try { | ||
| 108 | + // 使用SCAN命令替代KEYS命令,避免阻塞Redis | ||
| 109 | + List<String> keysToDelete = new ArrayList<>(); | ||
| 110 | + String pattern = CACHE_PREFIX + prefix + "*"; | ||
| 111 | + | ||
| 112 | + // 使用RedisCallback执行SCAN操作 | ||
| 113 | + redisTemplate.execute((RedisCallback<Void>) connection -> { | ||
| 114 | + try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions() | ||
| 115 | + .match(pattern) | ||
| 116 | + .count(100) | ||
| 117 | + .build())) { | ||
| 118 | + | ||
| 119 | + while (cursor.hasNext()) { | ||
| 120 | + String key = new String(cursor.next()); | ||
| 121 | + keysToDelete.add(key); | ||
| 122 | + | ||
| 123 | + // 批量删除,避免一次性删除过多key | ||
| 124 | + if (keysToDelete.size() >= 100) { | ||
| 125 | + Long deletedCount = redisTemplate.delete(keysToDelete); | ||
| 126 | + log.debug("Redis缓存批量删除: count={}", deletedCount); | ||
| 127 | + keysToDelete.clear(); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + } catch (Exception e) { | ||
| 131 | + log.error("SCAN操作异常: {}", e.getMessage(), e); | ||
| 132 | + } | ||
| 133 | + return null; | ||
| 134 | + }); | ||
| 135 | + | ||
| 136 | + // 删除剩余的key | ||
| 137 | + if (!keysToDelete.isEmpty()) { | ||
| 138 | + Long deletedCount = redisTemplate.delete(keysToDelete); | ||
| 139 | + log.debug("Redis缓存前缀删除完成: prefix={}, total_deleted={}", prefix, deletedCount); | ||
| 140 | + } | ||
| 141 | + } catch (Exception e) { | ||
| 142 | + log.error("Redis缓存前缀删除失败: prefix={}, error={}", prefix, e.getMessage(), e); | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + @Override | ||
| 147 | + public void clear() { | ||
| 148 | + try { | ||
| 149 | + // 使用SCAN命令替代KEYS命令,避免阻塞Redis | ||
| 150 | + List<String> keysToDelete = new ArrayList<>(); | ||
| 151 | + String pattern = CACHE_PREFIX + "*"; | ||
| 152 | + | ||
| 153 | + long totalDeleted = 0; | ||
| 154 | + | ||
| 155 | + // 使用RedisCallback执行SCAN操作 | ||
| 156 | + Long result = redisTemplate.execute((RedisCallback<Long>) connection -> { | ||
| 157 | + long deleted = 0; | ||
| 158 | + try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions() | ||
| 159 | + .match(pattern) | ||
| 160 | + .count(1000) | ||
| 161 | + .build())) { | ||
| 162 | + | ||
| 163 | + while (cursor.hasNext()) { | ||
| 164 | + String key = new String(cursor.next()); | ||
| 165 | + keysToDelete.add(key); | ||
| 166 | + | ||
| 167 | + // 批量删除,避免一次性删除过多key | ||
| 168 | + if (keysToDelete.size() >= 1000) { | ||
| 169 | + Long deletedCount = redisTemplate.delete(keysToDelete); | ||
| 170 | + if (deletedCount != null) { | ||
| 171 | + deleted += deletedCount; | ||
| 172 | + } | ||
| 173 | + keysToDelete.clear(); | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + } catch (Exception e) { | ||
| 177 | + log.error("SCAN操作异常: {}", e.getMessage(), e); | ||
| 178 | + } | ||
| 179 | + return deleted; | ||
| 180 | + }); | ||
| 181 | + | ||
| 182 | + if (result != null) { | ||
| 183 | + totalDeleted += result; | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + // 删除剩余的key | ||
| 187 | + if (!keysToDelete.isEmpty()) { | ||
| 188 | + Long deletedCount = redisTemplate.delete(keysToDelete); | ||
| 189 | + if (deletedCount != null) { | ||
| 190 | + totalDeleted += deletedCount; | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + if (totalDeleted > 0) { | ||
| 195 | + log.info("Redis缓存清空成功,删除{}个key", totalDeleted); | ||
| 196 | + } else { | ||
| 197 | + log.info("Redis缓存清空完成,无缓存数据"); | ||
| 198 | + } | ||
| 199 | + } catch (Exception e) { | ||
| 200 | + log.error("Redis缓存清空失败: error={}", e.getMessage(), e); | ||
| 201 | + } | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + @Override | ||
| 205 | + public CacheStats getStats() { | ||
| 206 | + try { | ||
| 207 | + // Redis没有内置统计信息,这里返回基本信息 | ||
| 208 | + return CacheStats.builder() | ||
| 209 | + .redisHits(0) | ||
| 210 | + .redisMisses(0) | ||
| 211 | + .totalRequests(0) | ||
| 212 | + .hitRate(0.0) | ||
| 213 | + .build(); | ||
| 214 | + } catch (Exception e) { | ||
| 215 | + log.error("Redis缓存统计获取失败: error={}", e.getMessage(), e); | ||
| 216 | + return CacheStats.builder().build(); | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + @Override | ||
| 221 | + public boolean exists(String key) { | ||
| 222 | + if (key == null || key.trim().isEmpty()) { | ||
| 223 | + return false; | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + try { | ||
| 227 | + return Boolean.TRUE.equals(redisTemplate.hasKey(CACHE_PREFIX + key)); | ||
| 228 | + } catch (Exception e) { | ||
| 229 | + log.error("Redis缓存存在性检查失败: key={}, error={}", key, e.getMessage(), e); | ||
| 230 | + return false; | ||
| 231 | + } | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + @Override | ||
| 235 | + public long getExpire(String key) { | ||
| 236 | + if (key == null || key.trim().isEmpty()) { | ||
| 237 | + return -2; | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + try { | ||
| 241 | + Long expire = redisTemplate.getExpire(CACHE_PREFIX + key, TimeUnit.SECONDS); | ||
| 242 | + return expire != null ? expire : -2; | ||
| 243 | + } catch (Exception e) { | ||
| 244 | + log.error("Redis缓存过期时间获取失败: key={}, error={}", key, e.getMessage(), e); | ||
| 245 | + return -2; | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | +} |
| 1 | +package com.infoloop.tianting.cache; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | +import org.springframework.cache.Cache; | ||
| 5 | + | ||
| 6 | +import java.util.concurrent.Callable; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * Spring Cache适配器 | ||
| 10 | + * 将Spring Cache接口桥接到现有的MultiLevelCacheService | ||
| 11 | + */ | ||
| 12 | +@Slf4j | ||
| 13 | +public class SpringCacheAdapter implements Cache { | ||
| 14 | + | ||
| 15 | + private final String name; | ||
| 16 | + private final CacheService cacheService; | ||
| 17 | + private final int defaultTtl; | ||
| 18 | + | ||
| 19 | + public SpringCacheAdapter(String name, CacheService cacheService, int defaultTtl) { | ||
| 20 | + this.name = name; | ||
| 21 | + this.cacheService = cacheService; | ||
| 22 | + this.defaultTtl = defaultTtl; | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + @Override | ||
| 26 | + public String getName() { | ||
| 27 | + return name; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + @Override | ||
| 31 | + public Object getNativeCache() { | ||
| 32 | + return cacheService; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public ValueWrapper get(Object key) { | ||
| 37 | + if (key == null) { | ||
| 38 | + return null; | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + String cacheKey = generateCacheKey(key); | ||
| 42 | + Object value = cacheService.get(cacheKey); | ||
| 43 | + | ||
| 44 | + if (value != null) { | ||
| 45 | + log.debug("Spring Cache命中: cache={}, key={}", name, cacheKey); | ||
| 46 | + return () -> value; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + log.debug("Spring Cache未命中: cache={}, key={}", name, cacheKey); | ||
| 50 | + return null; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + @Override | ||
| 54 | + public <T> T get(Object key, Class<T> type) { | ||
| 55 | + ValueWrapper wrapper = get(key); | ||
| 56 | + if (wrapper == null) { | ||
| 57 | + return null; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + Object value = wrapper.get(); | ||
| 61 | + if (value == null) { | ||
| 62 | + return null; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + if (type.isInstance(value)) { | ||
| 66 | + return type.cast(value); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + @Override | ||
| 73 | + @SuppressWarnings("unchecked") | ||
| 74 | + public <T> T get(Object key, Callable<T> valueLoader) { | ||
| 75 | + ValueWrapper wrapper = get(key); | ||
| 76 | + if (wrapper != null) { | ||
| 77 | + return (T) wrapper.get(); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + // 缓存未命中,执行valueLoader | ||
| 81 | + try { | ||
| 82 | + T value = valueLoader.call(); | ||
| 83 | + put(key, value); | ||
| 84 | + return value; | ||
| 85 | + } catch (Exception e) { | ||
| 86 | + throw new RuntimeException("ValueLoader execution failed", e); | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + @Override | ||
| 91 | + public void put(Object key, Object value) { | ||
| 92 | + if (key == null) { | ||
| 93 | + return; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + String cacheKey = generateCacheKey(key); | ||
| 97 | + cacheService.put(cacheKey, value, defaultTtl); | ||
| 98 | + log.debug("Spring Cache设置: cache={}, key={}, ttl={}s", name, cacheKey, defaultTtl); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + @Override | ||
| 102 | + public void evict(Object key) { | ||
| 103 | + if (key == null) { | ||
| 104 | + return; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + String cacheKey = generateCacheKey(key); | ||
| 108 | + cacheService.delete(cacheKey); | ||
| 109 | + log.debug("Spring Cache清除: cache={}, key={}", name, cacheKey); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + @Override | ||
| 113 | + public void clear() { | ||
| 114 | + // 清空整个缓存区域 | ||
| 115 | + cacheService.deleteByPattern(name + ":*"); | ||
| 116 | + log.debug("Spring Cache清空: cache={}", name); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /** | ||
| 120 | + * 生成缓存key | ||
| 121 | + * 格式: cacheName:key | ||
| 122 | + */ | ||
| 123 | + private String generateCacheKey(Object key) { | ||
| 124 | + return name + ":" + key.toString(); | ||
| 125 | + } | ||
| 126 | +} |
| ... | @@ -10,6 +10,7 @@ import com.infoloop.rpc.meizhongyiheservice.GetCustomersByConditionRpcRequest; | ... | @@ -10,6 +10,7 @@ import com.infoloop.rpc.meizhongyiheservice.GetCustomersByConditionRpcRequest; |
| 10 | import com.infoloop.rpc.meizhongyiheservice.MeiZhongYiHeServiceRpcGrpc; | 10 | import com.infoloop.rpc.meizhongyiheservice.MeiZhongYiHeServiceRpcGrpc; |
| 11 | import lombok.RequiredArgsConstructor; | 11 | import lombok.RequiredArgsConstructor; |
| 12 | import org.springframework.beans.factory.annotation.Autowired; | 12 | import org.springframework.beans.factory.annotation.Autowired; |
| 13 | +import org.springframework.cache.annotation.Cacheable; | ||
| 13 | import org.springframework.stereotype.Service; | 14 | import org.springframework.stereotype.Service; |
| 14 | 15 | ||
| 15 | import java.util.List; | 16 | import java.util.List; |
| ... | @@ -20,6 +21,7 @@ public class MeiZhongYiHeServiceClient { | ... | @@ -20,6 +21,7 @@ public class MeiZhongYiHeServiceClient { |
| 20 | 21 | ||
| 21 | private final MeiZhongYiHeServiceRpcGrpc.MeiZhongYiHeServiceRpcBlockingStub meiZhongYiHeServiceRpcBlockingStub; | 22 | private final MeiZhongYiHeServiceRpcGrpc.MeiZhongYiHeServiceRpcBlockingStub meiZhongYiHeServiceRpcBlockingStub; |
| 22 | 23 | ||
| 24 | + @Cacheable(value = "customerAllergies", key = "#hisCustomerId") | ||
| 23 | public GetCustomerAllergiesByCustomerIdsRpcResponse getHisCustomerAllergiesByCustomerId( | 25 | public GetCustomerAllergiesByCustomerIdsRpcResponse getHisCustomerAllergiesByCustomerId( |
| 24 | String hisCustomerId) { | 26 | String hisCustomerId) { |
| 25 | final var request = GetCustomerAllergiesByCustomerIdsRpcRequest.newBuilder() | 27 | final var request = GetCustomerAllergiesByCustomerIdsRpcRequest.newBuilder() |
| ... | @@ -28,6 +30,7 @@ public class MeiZhongYiHeServiceClient { | ... | @@ -28,6 +30,7 @@ public class MeiZhongYiHeServiceClient { |
| 28 | return meiZhongYiHeServiceRpcBlockingStub.getCustomerAllergiesByCustomerIds(request); | 30 | return meiZhongYiHeServiceRpcBlockingStub.getCustomerAllergiesByCustomerIds(request); |
| 29 | } | 31 | } |
| 30 | 32 | ||
| 33 | + @Cacheable(value = "customerMedicalAdvices", key = "#contractNo") | ||
| 31 | public GetCustomerMedicalAdvicesByHospitalRecordIdsRpcResponse getHisCustomerMedicalAdvicesByContractNo(String contractNo) { | 34 | public GetCustomerMedicalAdvicesByHospitalRecordIdsRpcResponse getHisCustomerMedicalAdvicesByContractNo(String contractNo) { |
| 32 | final var request = GetCustomerMedicalAdvicesByHospitalRecordIdsRpcRequest.newBuilder() | 35 | final var request = GetCustomerMedicalAdvicesByHospitalRecordIdsRpcRequest.newBuilder() |
| 33 | .addAllHospitalRecordIds(List.of(contractNo)) | 36 | .addAllHospitalRecordIds(List.of(contractNo)) |
| ... | @@ -35,6 +38,7 @@ public class MeiZhongYiHeServiceClient { | ... | @@ -35,6 +38,7 @@ public class MeiZhongYiHeServiceClient { |
| 35 | return meiZhongYiHeServiceRpcBlockingStub.getCustomerMedicalAdvicesByHospitalRecordIds(request); | 38 | return meiZhongYiHeServiceRpcBlockingStub.getCustomerMedicalAdvicesByHospitalRecordIds(request); |
| 36 | } | 39 | } |
| 37 | 40 | ||
| 41 | + @Cacheable(value = "customerDetails", key = "#HISCustomerId + '_' + #contractNo") | ||
| 38 | public Customer getCustomerDetailById(String HISCustomerId, String contractNo) { | 42 | public Customer getCustomerDetailById(String HISCustomerId, String contractNo) { |
| 39 | final var customer = meiZhongYiHeServiceRpcBlockingStub.getCustomerDetailById(GetCustomerDetailByIdRpcRequest.newBuilder() | 43 | final var customer = meiZhongYiHeServiceRpcBlockingStub.getCustomerDetailById(GetCustomerDetailByIdRpcRequest.newBuilder() |
| 40 | .setCustomerId(HISCustomerId) | 44 | .setCustomerId(HISCustomerId) | ... | ... |
| ... | @@ -6,6 +6,7 @@ import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest; | ... | @@ -6,6 +6,7 @@ import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest; |
| 6 | import com.infoloop.tianting.SingleCOperatorRpcResponse; | 6 | import com.infoloop.tianting.SingleCOperatorRpcResponse; |
| 7 | import lombok.RequiredArgsConstructor; | 7 | import lombok.RequiredArgsConstructor; |
| 8 | import org.springframework.beans.factory.annotation.Autowired; | 8 | import org.springframework.beans.factory.annotation.Autowired; |
| 9 | +import org.springframework.cache.annotation.Cacheable; | ||
| 9 | import org.springframework.stereotype.Service; | 10 | import org.springframework.stereotype.Service; |
| 10 | 11 | ||
| 11 | @Service | 12 | @Service |
| ... | @@ -14,6 +15,7 @@ public class OperatorServiceRpcClient { | ... | @@ -14,6 +15,7 @@ public class OperatorServiceRpcClient { |
| 14 | 15 | ||
| 15 | private final COperatorServiceProtoRpcGrpc.COperatorServiceProtoRpcBlockingStub cOperatorServiceProtoRpcBlockingStub; | 16 | private final COperatorServiceProtoRpcGrpc.COperatorServiceProtoRpcBlockingStub cOperatorServiceProtoRpcBlockingStub; |
| 16 | 17 | ||
| 18 | + @Cacheable(value = "operators", key = "'mobile_email_' + #keyword") | ||
| 17 | public SingleCOperatorRpcResponse getCOperatorByMobileOrEmail(String keyword) { | 19 | public SingleCOperatorRpcResponse getCOperatorByMobileOrEmail(String keyword) { |
| 18 | final var request = GetCOperatorByMobileOrEmailRpcRequest.newBuilder() | 20 | final var request = GetCOperatorByMobileOrEmailRpcRequest.newBuilder() |
| 19 | .setKeyword(keyword) | 21 | .setKeyword(keyword) |
| ... | @@ -21,6 +23,7 @@ public class OperatorServiceRpcClient { | ... | @@ -21,6 +23,7 @@ public class OperatorServiceRpcClient { |
| 21 | return cOperatorServiceProtoRpcBlockingStub.getCOperatorByMobileOrEmail(request).getResponse(); | 23 | return cOperatorServiceProtoRpcBlockingStub.getCOperatorByMobileOrEmail(request).getResponse(); |
| 22 | } | 24 | } |
| 23 | 25 | ||
| 26 | + @Cacheable(value = "operators", key = "'id_' + #id") | ||
| 24 | public SingleCOperatorRpcResponse getCOperatorById(int id) { | 27 | public SingleCOperatorRpcResponse getCOperatorById(int id) { |
| 25 | final var request = GetCOperatorByIdRpcRequest.newBuilder().setId(id).build(); | 28 | final var request = GetCOperatorByIdRpcRequest.newBuilder().setId(id).build(); |
| 26 | return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse(); | 29 | return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse(); | ... | ... |
| ... | @@ -6,6 +6,7 @@ import com.infoloop.tianting.SingleSkuResponse; | ... | @@ -6,6 +6,7 @@ import com.infoloop.tianting.SingleSkuResponse; |
| 6 | import com.infoloop.tianting.SkuServiceProtoRpcGrpc; | 6 | import com.infoloop.tianting.SkuServiceProtoRpcGrpc; |
| 7 | import lombok.RequiredArgsConstructor; | 7 | import lombok.RequiredArgsConstructor; |
| 8 | import org.springframework.beans.factory.annotation.Autowired; | 8 | import org.springframework.beans.factory.annotation.Autowired; |
| 9 | +import org.springframework.cache.annotation.Cacheable; | ||
| 9 | import org.springframework.stereotype.Service; | 10 | import org.springframework.stereotype.Service; |
| 10 | 11 | ||
| 11 | import java.util.List; | 12 | import java.util.List; |
| ... | @@ -15,6 +16,7 @@ import java.util.List; | ... | @@ -15,6 +16,7 @@ import java.util.List; |
| 15 | public class SkuServiceRpcClient { | 16 | public class SkuServiceRpcClient { |
| 16 | private final SkuServiceProtoRpcGrpc.SkuServiceProtoRpcBlockingStub skuServiceProtoRpcBlockingStub; | 17 | private final SkuServiceProtoRpcGrpc.SkuServiceProtoRpcBlockingStub skuServiceProtoRpcBlockingStub; |
| 17 | 18 | ||
| 19 | + @Cacheable(value = "skus", key = "'dish_skus_' + T(String).join('_', #ids)") | ||
| 18 | public GeDishSkuByIdsRpcResponse getDishSkusByIds(List<Integer> ids) { | 20 | public GeDishSkuByIdsRpcResponse getDishSkusByIds(List<Integer> ids) { |
| 19 | final var request = GetSkusByIdsRpcRequest.newBuilder() | 21 | final var request = GetSkusByIdsRpcRequest.newBuilder() |
| 20 | .addAllIds(ids) | 22 | .addAllIds(ids) |
| ... | @@ -23,6 +25,7 @@ public class SkuServiceRpcClient { | ... | @@ -23,6 +25,7 @@ public class SkuServiceRpcClient { |
| 23 | return skuServiceProtoRpcBlockingStub.getDishSkusByIds(request); | 25 | return skuServiceProtoRpcBlockingStub.getDishSkusByIds(request); |
| 24 | } | 26 | } |
| 25 | 27 | ||
| 28 | + @Cacheable(value = "skus", key = "'skus_' + T(String).join('_', #ids)") | ||
| 26 | public List<SingleSkuResponse> getSkusByIds(List<Integer> ids) { | 29 | public List<SingleSkuResponse> getSkusByIds(List<Integer> ids) { |
| 27 | final var request = GetSkusByIdsRpcRequest.newBuilder() | 30 | final var request = GetSkusByIdsRpcRequest.newBuilder() |
| 28 | .addAllIds(ids) | 31 | .addAllIds(ids) | ... | ... |
| ... | @@ -128,3 +128,26 @@ knife4j.setting.enable-swagger-models=true | ... | @@ -128,3 +128,26 @@ knife4j.setting.enable-swagger-models=true |
| 128 | knife4j.setting.enable-reload-cache-parameter=true | 128 | knife4j.setting.enable-reload-cache-parameter=true |
| 129 | knife4j.setting.enable-version=true | 129 | knife4j.setting.enable-version=true |
| 130 | 130 | ||
| 131 | +# 缓存配置 | ||
| 132 | +cache.enabled=true | ||
| 133 | +cache.default-ttl=300 | ||
| 134 | + | ||
| 135 | +# Caffeine本地缓存配置 | ||
| 136 | +cache.caffeine.initial-capacity=100 | ||
| 137 | +cache.caffeine.maximum-size=10000 | ||
| 138 | +cache.caffeine.expire-after-write=60 | ||
| 139 | +cache.caffeine.expire-after-access=0 | ||
| 140 | +cache.caffeine.record-stats=true | ||
| 141 | + | ||
| 142 | +# Redis缓存配置 | ||
| 143 | +cache.redis.enabled=true | ||
| 144 | +cache.redis.host=${spring.redis.host} | ||
| 145 | +cache.redis.port=${spring.redis.port} | ||
| 146 | +cache.redis.password=${spring.redis.password} | ||
| 147 | +cache.redis.database=11 | ||
| 148 | +cache.redis.timeout=2000 | ||
| 149 | + | ||
| 150 | +# 缓存监控配置 | ||
| 151 | +cache.metrics.enabled=true | ||
| 152 | +cache.metrics.step=1m | ||
| 153 | + | ... | ... |
-
Please register or login to post a comment