目录
- 需求描述:
- 工程结构:
- 截图
- 代码
- 工程配置:
- pom.xml
- application.yml
- 缓存配置:
- CacheConfig
- CacheBase
- CachePolicy
- 如何使用:
- User
- IUserService
- UserServiceImpl
- UserServiceCache
- UserController
- 如何测试:
- 数据分布:
需求描述:
传入主键集合获取对象列表,要求高命中率,主键类型可以是Integer、Long、String等类型,并且与业务解耦,不要求实时性。
工程结构:
截图
代码
链接:https://pan.baidu.com/s/1nO7fzPhSoA5WqNSmJ1dkrA
提取码:jv1p
工程配置:
pom.xml
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-starter-redis --> <dependency> <groupId>com.alicp.jetcache</groupId> <artifactId>jetcache-starter-redis</artifactId> <version>2.6.7</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
application.yml
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21jetcache: statIntervalMinutes: 1 areaInCacheName: false local: default: type: linkedhashmap keyConvertor: fastjson limit: 100 remote: default: type: redis keyConvertor: fastjson valueEncoder: java valueDecoder: java poolConfig: minIdle: 100 maxIdle: 100 maxTotal: 100 host: 127.0.0.1 port: 6379
缓存配置:
CacheConfig
仓库地址:https://github.com/alibaba/jetcache
复制代码
1
2
3
4
5@Configuration @EnableMethodCache(basePackages = "com.example.demo.cache") @EnableCreateCacheAnnotation public class CacheConfig {}
CacheBase
核心思想:对于批量请求主键集合数据,首先到缓存中查询,将查询到的数据保存下来,然后用批量请求主键集合和刚才查询出来的数据主键集合做一个差值,如果差值为空,说明所有数据都命中了,此时只需要直接返回即可。如果差值存在,那么需要将差值主键集合传入到Lambda中,调用业务进行查找然后返回,我们需要对差值集合主键查询出来的数据,保存到缓存中,同时也要对空值进行处理,防止缓存穿透,以及要设置随机过期时间,防止缓存雪崩,最终返回查询结果。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204public abstract class CacheBase { /** * 集合加载因子 */ public static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 获取缓存集合 * * @return */ public abstract Cache<String, String> getCacheMap(); /** * 获取缓存策略 * * @return */ public abstract CachePolicy getCachePolicy(); /** * 获取缓存数据 * * @param guidList 业务主键集合 * @param clazz 返回集合类型 * @param keyMapper 获取对象主键 * @param businessLoader 未命中主键查询 * @param args 缓存键的键前缀 * @param <P> 主键类型,例如:Integer、Long、String、... * @param <T> 对象类型,例如:User、Student、... * @return */ public <P, T> List<T> getAll(List<P> guidList, Class<T> clazz, Function<? super T, ? extends P> keyMapper, Function<List<P>, List<T>> businessLoader, Object... args) { // 获取业务缓存策略 CachePolicy policy = getCachePolicy(); policy.inspect(); String emptyObject = policy.getEmptyObject(); int emptyObjectMinExpiredTime = policy.getEmptyObjectMinExpiredTime(); int emptyObjectMaxExpiredTime = policy.getEmptyObjectMaxExpiredTime(); TimeUnit emptyObjectExpiredTimeUnit = policy.getEmptyObjectExpiredTimeUnit(); int plainObjectMinExpiredTime = policy.getPlainObjectMinExpiredTime(); int plainObjectMaxExpiredTime = policy.getPlainObjectMaxExpiredTime(); TimeUnit plainObjectExpiredTimeUnit = policy.getPlainObjectExpiredTimeUnit(); // 创建最终返回集合 List<T> resultList = new ArrayList(guidList.size()); // 业务主键集合检查 if (CollectionUtils.isEmpty(guidList)) { return resultList; } // 批量从缓存中查询 int initialCapacity = (int) (guidList.size() / DEFAULT_LOAD_FACTOR + 1); Map<String, String> queryCacheMap = new HashMap<>(initialCapacity); List<Set<String>> matchKeyList = buildGuidMatchKeyList(guidList, args); for (Set<String> keySet : matchKeyList) { Map<String, String> returnMap = getCacheMap().getAll(keySet); if (!CollectionUtils.isEmpty(returnMap)) { queryCacheMap.putAll(returnMap); } } // 未命中的主键集合 List<P> missGuidList = new ArrayList<>(guidList.size()); if (queryCacheMap.keySet().size() > 0) { for (P guid : guidList) { String matchKey = buildGuidMatchKey(guid, args); String cacheValue = queryCacheMap.get(matchKey); // 缓存未命中 if (StringUtils.isEmpty(cacheValue)) { missGuidList.add(guid); continue; } // 缓存被穿透 if (emptyObject.equals(cacheValue)) { continue; } resultList.add(JSON.parseObject(cacheValue, clazz)); } } else { missGuidList = guidList; } // 如果全部命中 if (CollectionUtils.isEmpty(missGuidList)) { return resultList; } // 设置初始大小 initialCapacity = (int) (missGuidList.size() / DEFAULT_LOAD_FACTOR + 1); // 查询业务逻辑 List<T> applyResultList = businessLoader.apply(missGuidList); Map<P, T> applyResultMap = applyResultList.stream().collect(Collectors.toMap(keyMapper, Function.identity())); Map<String, T> missGuidMap = new HashMap<>(initialCapacity); for (Map.Entry<P, T> entry : applyResultMap.entrySet()) { P guid = entry.getKey(); T value = entry.getValue(); String matchKey = buildGuidMatchKey(guid, args); missGuidMap.put(matchKey, value); } // 防止缓存穿透 Map<String, String> nullMap = new HashMap<>(initialCapacity); for (P guid : missGuidList) { String matchKey = buildGuidMatchKey(guid, args); if (!missGuidMap.containsKey(matchKey)) { nullMap.put(matchKey, emptyObject); } } if (!CollectionUtils.isEmpty(nullMap)) { long expiredTime = getRandomNumber(emptyObjectMinExpiredTime, emptyObjectMaxExpiredTime); getCacheMap().putAll(nullMap, expiredTime, emptyObjectExpiredTimeUnit); } // 保存未命中的 Map<String, String> saveMap = new HashMap<>(initialCapacity); for (Map.Entry<String, T> entry : missGuidMap.entrySet()) { String matchKey = entry.getKey(); T value = entry.getValue(); String cacheValue = JSON.toJSON(value).toString(); saveMap.put(matchKey, cacheValue); } if (!CollectionUtils.isEmpty(saveMap)) { long expiredTime = getRandomNumber(plainObjectMinExpiredTime, plainObjectMaxExpiredTime); getCacheMap().putAll(saveMap, expiredTime, plainObjectExpiredTimeUnit); } // 返回最终结果 resultList.addAll(applyResultMap.values()); return resultList; } /** * 构建MatchKey集合 * * @param guidList * @param args * @return */ private <P> List<Set<String>> buildGuidMatchKeyList(List<P> guidList, Object... args) { // 获取业务缓存策略 CachePolicy policy = getCachePolicy(); policy.inspect(); int batchSize = policy.getBatchSize(); // 批量切分主键集合 List<Set<String>> matchKeyList = new ArrayList<>(); List<List<P>> partitionList = Lists.partition(guidList, batchSize); for (List<P> guids : partitionList) { Set<String> guidSet = new HashSet<>(); for (P guid : guids) { guidSet.add(buildGuidMatchKey(guid, args)); } matchKeyList.add(guidSet); } return matchKeyList; } /** * 构建MatchKey对象 * * @param guid * @param args * @return */ private <P> String buildGuidMatchKey(P guid, Object... args) { // 获取业务缓存策略 CachePolicy policy = getCachePolicy(); policy.inspect(); String matchKeySplicer = policy.getMatchKeySplicer(); // 构建缓存主键对象 StringBuilder matchKey = new StringBuilder(); for (Object arg : args) { matchKey.append(arg).append(matchKeySplicer); } matchKey.append(guid); return matchKey.toString(); } /** * 获取指定范围数字 * * @param lowerValue * @param upperValue * @return */ private long getRandomNumber(int lowerValue, int upperValue) { return (long) Math.floor(Math.random() * (upperValue - lowerValue + 1) + lowerValue); } /** * 删除缓存数据 * * @param guidList * @param args * @return */ public <P> boolean removeAll(List<P> guidList, Object... args) { if (CollectionUtils.isEmpty(guidList)) { return false; } List<Set<String>> matchKeyList = buildGuidMatchKeyList(guidList, args); for (Set<String> keySet : matchKeyList) { getCacheMap().removeAll(keySet); } return true; } }
CachePolicy
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95@Getter @Setter @ToString public class CachePolicy { /** * 批量处理大小 */ private int batchSize; /** * 匹配键拼接符 */ private String matchKeySplicer; /** * 空值对象数据 */ private String emptyObject; /** * 空值对象数据:最小过期时间 */ private int emptyObjectMinExpiredTime; /** * 空值对象数据:最大过期时间 */ private int emptyObjectMaxExpiredTime; /** * 空值对象数据:过期时间单位 */ private TimeUnit emptyObjectExpiredTimeUnit; /** * 普通对象数据:最小过期时间 */ private int plainObjectMinExpiredTime; /** * 普通对象数据:最大过期时间 */ private int plainObjectMaxExpiredTime; /** * 普通对象数据:过期时间单位 */ private TimeUnit plainObjectExpiredTimeUnit; /** * 默认缓存策略 */ public CachePolicy() { this.batchSize = 100; this.matchKeySplicer = "_"; this.emptyObject = "empty_object"; this.emptyObjectMinExpiredTime = 5; this.emptyObjectMaxExpiredTime = 10; this.emptyObjectExpiredTimeUnit = TimeUnit.MINUTES; this.plainObjectMinExpiredTime = 5; this.plainObjectMaxExpiredTime = 10; this.plainObjectExpiredTimeUnit = TimeUnit.MINUTES; } /** * 检验缓存策略 */ public void inspect() { if (this.batchSize <= 0) { throw new IllegalArgumentException("batchSize Must be greater than 0"); } if (this.matchKeySplicer == null) { throw new IllegalArgumentException("matchKeySplicer Must be not null"); } if (this.emptyObject == null) { throw new IllegalArgumentException("emptyObject Must be not null"); } if (this.emptyObjectMinExpiredTime <= 0) { throw new IllegalArgumentException("emptyObjectMinExpiredTime Must be greater than 0"); } if (this.emptyObjectMaxExpiredTime <= 0) { throw new IllegalArgumentException("emptyObjectMaxExpiredTime Must be greater than 0"); } if (this.plainObjectMinExpiredTime <= 0) { throw new IllegalArgumentException("plainObjectMinExpiredTime Must be greater than 0"); } if (this.plainObjectMaxExpiredTime <= 0) { throw new IllegalArgumentException("plainObjectMaxExpiredTime Must be greater than 0"); } if (this.plainObjectExpiredTimeUnit == null) { throw new IllegalArgumentException("plainObjectExpiredTimeUnit Must be not null"); } } }
如何使用:
User
复制代码
1
2
3
4
5
6
7
8
9@Data @Builder @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Long id; private String name; }
IUserService
复制代码
1
2
3
4
5
6
7
8
9
10public interface IUserService { /** * 根据主键集合获取用户列表 * * @param ids * @return */ List<User> selectList(List<Long> ids); }
UserServiceImpl
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28@Slf4j @Service public class UserServiceImpl implements IUserService { /** * 模拟数据库中数据 */ static Map<Long, User> db = new HashMap<>(); static { db.put(1L, User.builder().id(1L).name("张三").build()); db.put(2L, User.builder().id(2L).name("李四").build()); db.put(3L, User.builder().id(3L).name("王五").build()); db.put(4L, User.builder().id(4L).name("赵六").build()); } @Override public List<User> selectList(List<Long> ids) { log.info("UserServiceImpl selectList ids = {}", ids); List<User> result = new ArrayList<>(); for (Long id : ids) { User user = db.get(id); if (user != null) { result.add(user); } } return result; } }
UserServiceCache
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49@Slf4j @Service public class UserServiceCache extends CacheBase { /** * ================================================== * 全局缓存配置 * ================================================== */ private static final String AREA = "default"; private static final String NAME = "user:list:"; @CachePenetrationProtect @CreateCache(area = AREA, name = NAME, cacheType = CacheType.REMOTE, expire = 60) private Cache<String, String> matchCache; @Override public Cache<String, String> getCacheMap() { return this.matchCache; } @Override public CachePolicy getCachePolicy() { CachePolicy policy = new CachePolicy(); policy.setBatchSize(100); policy.setMatchKeySplicer("_"); policy.setEmptyObject("empty_object"); policy.setEmptyObjectMinExpiredTime(5); policy.setEmptyObjectMaxExpiredTime(10); policy.setEmptyObjectExpiredTimeUnit(TimeUnit.MINUTES); policy.setPlainObjectMinExpiredTime(5); policy.setPlainObjectMaxExpiredTime(10); policy.setPlainObjectExpiredTimeUnit(TimeUnit.MINUTES); return policy; } /** * ================================================== * 业务缓存处理 * ================================================== */ @Autowired private IUserService userService; public List<User> selectList(List<Long> ids) { log.info("UserServiceCache selectList ids = {}", ids); return super.getAll(ids, User.class, User::getId, userService::selectList, "user", "id"); } }
UserController
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@RestController @RequestMapping("user") public class UserController { @Autowired private UserServiceCache userServiceCache; @PostMapping("selectList") public List<User> selectList(@RequestBody List<Long> ids) { return userServiceCache.selectList(ids); } @PostMapping("removeList") public boolean removeList(@RequestBody List<Long> ids) { return userServiceCache.removeAll(ids, "user", "id"); } }
如何测试:
数据分布:
第一次访问,缓存中不存在,因此没有命中,hit=0,数据每隔1分钟统计一次,统计间隔可修改
第二次访问,缓存中已存在,因此肯定命中,hit=5,数据每隔1分钟统计一次,统计间隔可修改
以下内容则是数据在Redis中的分布情况,每个Key的前缀参数列表是user、id,_ 是参数连接符
最后
以上就是愤怒小松鼠最近收集整理的关于如何设计缓存中间层需求描述:工程结构:工程配置:缓存配置:如何使用:如何测试:数据分布:的全部内容,更多相关如何设计缓存中间层需求描述内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复