document
API test
存在缓存

获取二三级分类

GET
http://localhost:12000/index/json/catalog.json

Response Parameters

parameter
type
description
required
1
array
数据列表
required
catalog1Id
string
示例:1
required
catalog3List
array
数据列表
required
catalog2Id
string
示例:22
required
id
string
示例:165
required
name
string
示例:电子书
required
id
string
示例:22
required
name
string
示例:电子书刊
required
21
array
数据列表
required
catalog1Id
string
示例:21
required
catalog3List
array
数据列表
required
catalog2Id
string
示例:163
required
id
string
示例:1404
required
name
string
示例:微型车
required
id
string
示例:163
required
name
string
示例:全新整车
required

Description or Example

# 核心代码 ## 原生本地锁缓存版 ```java @GetMapping("/json/catalog.json") @ResponseBody public Map<String, List<Catalog2VO>> getCategories2Or3() { return categoryService.getCateGoryLevel2Or3(); } ``` ```java @Override public Map<String, List<Catalog2VO>> getCategoryLevel2Or3() { String category2Or3Json = redisTemplate.opsForValue().get(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3); Map<String, List<Catalog2VO>> map = null; if (StringUtils.isNotBlank(category2Or3Json)) { // 找到对应的缓存 // 将缓存中的JSON转换成对象 map = JSON.parseObject(category2Or3Json, new TypeReference<Map<String, List<Catalog2VO>>>() { }); } else { // 没有找到对应的缓存 map = getDataAndCacheItWithLocalSync(); // TODO: 本地锁需要升级为分布式锁 } return map; } /** * 将缓存的保存也放入本地重量级锁的范围内 * @return */ @Deprecated private synchronized Map<String, List<Catalog2VO>> getDataAndCacheItWithLocalSync() { // 获取数据并缓存起来 Map<String, List<Catalog2VO>> map; map = getCategoryLevel2Or3FromDBLocalSync(); String map2Json = ""; // 记录不存在, 把空串缓存起来 if (map != null && !map.isEmpty()) { // 查询出来的记录有可能为空, 因此需要解决缓存穿透问题 // 数据库成功查询出对应的记录, 需要转换JSON, 然后存储 map2Json = JSON.toJSONString(map); } // 缓存需要设置过期时间, 避免因宕机造成永久数据不一致问题 // 创建随机值, 避免缓存雪崩, 随即量 1min ~ 1day long randomTime = RandomUtils.nextLong(1, 1440); redisTemplate.opsForValue().set(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3, map2Json, (ProductConstant.Cache.CACHE_DEPRECATED_MINUTES + randomTime), TimeUnit.MINUTES); return map; } /** * 为双端检锁加上本地重量级锁 * @return */ @Deprecated private synchronized Map<String, List<Catalog2VO>> getCategoryLevel2Or3FromDBLocalSync() { // 本地锁 return getCategoryLevel2Or3FromDB(); } // private Map<String, List<Catalog2VO>> getCategoryLevel2Or3FromDBHadoopSync() { // // } /** * 双端检锁核心逻辑, 锁的添加不在此处, 锁是未知的, 可以是本地锁, 可以是分布式锁<br/> * <i>架构模式: 扩展所范围的方法 (调用) 锁数据库查询的双端检锁逻辑 -> 双端检锁逻辑</i> * @return */ private Map<String, List<Catalog2VO>> getCategoryLevel2Or3FromDB() { String category2Or3Json = redisTemplate.opsForValue().get(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3); // 这里是双端检锁, 目的是为了.... if (StringUtils.isNotBlank(category2Or3Json)) { // 找到对应的缓存 // 将缓存中的JSON转换成对象 return JSON.parseObject(category2Or3Json, new TypeReference<Map<String, List<Catalog2VO>>>() { }); } System.out.println("查询数据库"); // 1. 将所有的分类都查询出来 List<CategoryEntity> categoryEntities = this.list(); // 防止空指针, 增强健壮性 if (categoryEntities != null && !categoryEntities.isEmpty()) { // 2. 过滤出一级分类, 并获取其id List<Long> level1Ids = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid() == 0L) .map(CategoryEntity::getCatId) .collect(Collectors.toList()); if (!level1Ids.isEmpty()) { return level1Ids.stream().collect(Collectors.toMap(Object::toString, level1Id -> { // 1. 获取一级分类下所有的二级分类 List<CategoryEntity> category2Entities = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid().equals(level1Id)) .collect(Collectors.toList()); // 2. 初始化每一个二级分类 List<Catalog2VO> catalog2VOS = category2Entities.stream().map(level2 -> { Catalog2VO catalog2VO = new Catalog2VO(); // 3, 获取二级分类下的三级分类 List<CategoryEntity> category3Entities = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid().equals(level2.getCatId())) .collect(Collectors.toList()); // 4. 初始化每一个三级分类 List<Catalog3VO> catalog3VOS = category3Entities.stream().map(level3 -> { Catalog3VO catalog3VO = new Catalog3VO(); catalog3VO.setCatalog2Id(level2.getCatId().toString()) .setId(level3.getCatId().toString()) .setName(level3.getName()); return catalog3VO; }).collect(Collectors.toList()); catalog2VO.setCatalog1Id(level1Id.toString()) .setId(level2.getCatId().toString()) .setName(level2.getName()) .setCatalog3List(catalog3VOS); return catalog2VO; }).collect(Collectors.toList()); return catalog2VOS; })); } } return null; } ``` ## Redisson分布式锁版本 ```java public Map<String, List<Catalog2VO>> getCategoryLevel2Or3() { String category2Or3Json = redisTemplate.opsForValue().get(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3); Map<String, List<Catalog2VO>> map = null; if (StringUtils.isNotBlank(category2Or3Json)) { // 找到对应的缓存 // 将缓存中的JSON转换成对象 map = JSON.parseObject(category2Or3Json, new TypeReference<Map<String, List<Catalog2VO>>>() { }); } else { // 没有找到对应的缓存 map = getDataAndCacheItWithHadoopSyncRedisson(); } return map; } private Map<String, List<Catalog2VO>> getDataAndCacheItWithHadoopSyncRedisson() { RLock lock = redissonClient.getLock(ProductConstant.Cache.LOCK_PREFIX + ProductConstant.Cache.MICROSERVICE_PREFIX + "getCategory2Or3"); lock.lock(); try { Map<String, List<Catalog2VO>> map = getCategoryLevel2Or3FromDBLocalSync(); String map2Json = ""; // 记录不存在, 把空串缓存起来 if (map != null && !map.isEmpty()) { // 查询出来的记录有可能为空, 因此需要解决缓存穿透问题 // 数据库成功查询出对应的记录, 需要转换JSON, 然后存储 map2Json = JSON.toJSONString(map); } // 缓存需要设置过期时间, 避免因宕机造成永久数据不一致问题 // 创建随机值, 避免缓存雪崩, 随即量 1min ~ 1day long randomTime = RandomUtils.nextLong(1, 1440); redisTemplate.opsForValue().set(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3, map2Json, (ProductConstant.Cache.CACHE_DEPRECATED_MINUTES + randomTime), TimeUnit.MINUTES); return map; }finally { lock.unlock(); } } ``` ## SpringCache版本 ```java @Override @Cacheable(cacheNames = "categories", key = "#root.methodName", sync = true) public Map<String, List<Catalog2VO>> getCategoryLevel2Or3() { return getCategoryLevel2Or3FromDB(); } private Map<String, List<Catalog2VO>> getCategoryLevel2Or3FromDB() { String category2Or3Json = redisTemplate.opsForValue().get(ProductConstant.Cache.CACHE_CATEGORY_2_OR_3); // 这里是双端检锁, 目的是为了.... if (StringUtils.isNotBlank(category2Or3Json)) { // 找到对应的缓存 // 将缓存中的JSON转换成对象 return JSON.parseObject(category2Or3Json, new TypeReference<Map<String, List<Catalog2VO>>>() { }); } // 1. 将所有的分类都查询出来 List<CategoryEntity> categoryEntities = this.list(); // 防止空指针, 增强健壮性 if (categoryEntities != null && !categoryEntities.isEmpty()) { // 2. 过滤出一级分类, 并获取其id List<Long> level1Ids = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid() == 0L) .map(CategoryEntity::getCatId) .collect(Collectors.toList()); if (!level1Ids.isEmpty()) { return level1Ids.stream().collect(Collectors.toMap(Object::toString, level1Id -> { // 1. 获取一级分类下所有的二级分类 List<CategoryEntity> category2Entities = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid().equals(level1Id)) .collect(Collectors.toList()); // 2. 初始化每一个二级分类 List<Catalog2VO> catalog2VOS = category2Entities.stream().map(level2 -> { Catalog2VO catalog2VO = new Catalog2VO(); // 3, 获取二级分类下的三级分类 List<CategoryEntity> category3Entities = categoryEntities.stream() .filter(categoryEntity -> categoryEntity.getParentCid().equals(level2.getCatId())) .collect(Collectors.toList()); // 4. 初始化每一个三级分类 List<Catalog3VO> catalog3VOS = category3Entities.stream().map(level3 -> { Catalog3VO catalog3VO = new Catalog3VO(); catalog3VO.setCatalog2Id(level2.getCatId().toString()) .setId(level3.getCatId().toString()) .setName(level3.getName()); return catalog3VO; }).collect(Collectors.toList()); catalog2VO.setCatalog1Id(level1Id.toString()) .setId(level2.getCatId().toString()) .setName(level2.getName()) .setCatalog3List(catalog3VOS); return catalog2VO; }).collect(Collectors.toList()); return catalog2VOS; })); } } return null; } ``` # 扩展知识 ## 老师写的第一种方案为什么很差, 很挫? > 整体上, 都是先查找一级分类, 然后通过一级分类找到二级分类, 再通过二级分类找到三级分类 > 但是, 第一种方案是通过一级分类的id, 在数据库中查询, 再通过二级分类的id, 在数据库中查询 > 第二种方案是查询出所有的分类, 然后过滤出一级分类的id, 通过一级分类的id过滤出二级分类, 通过二级分类的id过滤出三级分类 > 两者最大的区别是一次查询数据库和N次查询数据库, 如果N此查询数据库, 需要频繁的建立和销毁连接, 而且需要磁盘IO, 效率非常低, 延迟很大, 占用带宽, 很拉跨, 而第二种方案只需要查询一次, 其他都在内存层面过滤, 效率极高 ## 为什么最终采取了本地锁 > 详情请看SPringCache的整合专题