Jiaqi Xia

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

...@@ -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 +
......