谷粒商城高级篇缓存与分布式锁
Posted 愿你满腹经纶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谷粒商城高级篇缓存与分布式锁相关的知识,希望对你有一定的参考价值。
谷粒商城笔记合集
分布式基础篇 | 分布式高级篇 | 高可用集群篇 |
---|---|---|
===简介&环境搭建=== | ===Elasticsearch=== | |
项目简介与分布式概念(第一、二章) | Elasticsearch:全文检索(第一章) | |
基础环境搭建(第三章) | ===商品服务开发=== | |
===整合SpringCloud=== | 商品服务 & 商品上架(第二章) | |
整合SpringCloud、SpringCloud alibaba(第四、五章) | ===商城首页开发=== | |
===前端知识=== | 商城业务:首页整合、Nginx 域名访问、性能优化与压力测试 (第三、四、五章) | |
前端开发基础知识(第六章) | 缓存与分布式锁(第六章) | |
===商品服务开发=== | ===商城检索开发=== | |
商品服务开发:基础概念、三级分类(第七、八章) | 商城业务:商品检索(第七章) | |
商品服务开发:品牌管理(第九章) | ||
商品服务开发:属性分组、平台属性(第十、十一章) | ||
商品服务:商品维护(第十二、十三章) | ||
===仓储服务开发=== | ||
仓储服务:仓库维护(第十四章) | ||
基础篇总结(第十五章) |
六、缓存与分布式锁⚠️
6.1 缓存⚠️
6.1.1 使用概述
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问,而 db 承担数据落盘工作
哪些数据适合放入缓存?
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多、写少)
举例:电商类应用、商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定)后台如果发布一个商品、买家需要 5 分钟才能看到新商品一般还是可以接受的。
使用方式:伪代码
data = cche.load(b); //从缓存中加载数据
if(data == null)
data = db.load(id); // 从数据库加载数据
cache.put(id,data); // 保存到 cache中
return data;
注意:在开发中,凡是放到缓存中的数据我们都应该制定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载的流程,避免业务奔溃导致的数据永久不一致的问题
6.1.2 本地缓存&分布式缓存⚠️
本地缓存在分布式下的问题⚠️
分布式缓存⚠️
- 集群或分片:理论上无限量的容量提升,打破本地缓存的容量限制
- 使用简单,单独维护
- 可以做到高可用,高性能
6.2 API:三级分类 整合Redis💡
-
引入依赖
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置:application.yaml
配置类:org.springframework.boot.autoconfigure.data.redis.RedisProperties
spring: redis: host: 114.132.162.129 password: bilimall port: 6379
-
测试使用:cn.lzwei.bilimall.product.BilimallProductApplicationTests
自动注入:org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
@RunWith(SpringRunner.class) @SpringBootTest public class BilimallProductApplicationTests @Resource StringRedisTemplate stringRedisTemplate; //redis测试 @Test public void testStringValueOperations() ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue(); valueOperations.set("hello","redis_"+ UUID.randomUUID().toString()); String hello = valueOperations.get("hello"); System.out.println(hello);
redis_f5aeb308-06bd-4b73-b4b5-c52b3ff29803
-
cn.lzwei.bilimall.product.web.IndexController:首页分类数据添加缓存
/** * 首页控制器 */ @Controller public class IndexController @Resource CategoryService categoryService; /** * 获取分类数据,添加缓存:用于渲染二级、三级分类 */ @ResponseBody @GetMapping(value = "/index/catalog.json") public Map<String, List<CategoryLevel2Vo>> getCategoryLevel2() Map<String, List<CategoryLevel2Vo>> categorys=categoryService.getCatalogJson(); return categorys;
-
CategoryService:首页分类数据添加缓存
public interface CategoryService extends IService<CategoryEntity> /** * 获取分类数据,添加缓存:用于渲染二级、三级分类 */ Map<String, List<CategoryLevel2Vo>> getCatalogJson();
-
CategoryServiceImpl:首页分类数据添加缓存
@Service("categoryService") public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService @Resource StringRedisTemplate stringRedisTemplate; /** * 获取分类数据,添加缓存:用于渲染二级、三级分类 */ @Override public Map<String, List<CategoryLevel2Vo>> getCatalogJson() //1.从缓存中获取 String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); //2.缓存中没有 if(StringUtils.isEmpty(catalogJson)) //2.1、从数据库获取 Map<String, List<CategoryLevel2Vo>> categoryLevel2 = getCategoryLevel2(); //2.2、转换为json字符串存进缓存中 String jsonString = JSON.toJSONString(categoryLevel2); stringRedisTemplate.opsForValue().set("catalogJson",jsonString); //2.3、返回 return categoryLevel2; //3.缓存中有解析为java对象后返回 Map<String, List<CategoryLevel2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<CategoryLevel2Vo>>>() ); return result; //从数据库查询 public Map<String, List<CategoryLevel2Vo>> getCategoryLevel2() //缓存所有三级分类数据 List<CategoryEntity> categoryEntities = baseMapper.selectList(null); //1.获取一级分类:将一级分类转换为map进行遍历,自定义key、value List<CategoryEntity> categoryLevel1s = getParent_cid(categoryEntities,0l); Map<String, List<CategoryLevel2Vo>> collect=null; if(categoryLevel1s!=null) collect = categoryLevel1s.stream().collect(Collectors.toMap(level1 -> level1.getCatId().toString(), level1 -> //2.通过一级分类id获取二级分类列表进行遍历:封装成CategoryLevel2Vo集合 List<CategoryEntity> level2s = getParent_cid(categoryEntities,level1.getCatId()); List<CategoryLevel2Vo> Level2Vos = null; if (level2s != null) //遍历二级分类:封装成CategoryLevel2Vo Level2Vos = level2s.stream().map(level2 -> CategoryLevel2Vo categoryLevel2Vo = new CategoryLevel2Vo(level2.getCatId().toString(), level2.getName(), level1.getCatId().toString(), null); //3.通过二级分类id获取三级分类列表:封装成CategoryLevel3Vo集合 List<CategoryEntity> level3s = getParent_cid(categoryEntities,level2.getCatId()); if (level3s != null) //遍历三级分类:封装成CategoryLevel3Vo List<CategoryLevel2Vo.CategoryLevel3Vo> level3Vos = level3s.stream().map(level3 -> CategoryLevel2Vo.CategoryLevel3Vo categoryLevel3Vo = new CategoryLevel2Vo.CategoryLevel3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName()); return categoryLevel3Vo; ).collect(Collectors.toList()); categoryLevel2Vo.setCatalog3List(level3Vos); return categoryLevel2Vo; ).collect(Collectors.toList()); return Level2Vos; )); return collect; //通过 parent_id 获取分类数据 private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntities,Long parentId) List<CategoryEntity> collect = categoryEntities.stream().filter(item -> item.getParentCid().equals(parentId) ).collect(Collectors.toList()); return collect;
-
启动nginx,访问接口:http://bilimall.com/index/catalog.json
6.3 压测异常:堆外内存溢出⚠️💡
6.3.1 异常出现
使用 JMeter 对接口 http://bilimall.com/index/catalog.json 进行压力测试,发现 直接内存(堆外内存) 溢出。
java.lang.OutOfMemoryError: Cannot reserve 46137344 bytes of direct buffer memory (allocated: 59236353, limit: 104857600)
at java.base/java.nio.Bits.reserveMemory(Bits.java:178) ~[na:na]
...
6.3.2 异常分析
-
springboot 2.0 以后默认使用lettuce作为操作redis的客户端,lettuce使用netty进行网络通信
-
lettuce默认直接内存容量为64m,但是由于lettuce对于直接内存回收处理不完善的原因,导致直接内存溢出
可以通过 -XX:MaxDirectMemorySize=<size> 进行直接内存容量设置
class Bits private static volatile long MAX_MEMORY = VM.maxDirectMemory(); static void reserveMemory(long size, long cap) //1.进行内存分配判断 if (tryReserveMemory(size, cap)) return; ... try ... //一系列重试机制 System.gc(); ... //一系列重试机制 //2.重试内存分配失败抛出异常 throw new OutOfMemoryError ("Cannot reserve " + size + " bytes of direct buffer memory (allocated: " + RESERVED_MEMORY.get() + ", limit: " + MAX_MEMORY +")"); finally ... //1.内存分配判断 private static boolean tryReserveMemory(long size, long cap) // -XX:MaxDirectMemorySize limits the total capacity rather than the // actual memory usage, which will differ when buffers are page // aligned. long totalCap; while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) RESERVED_MEMORY.addAndGet(size); COUNT.incrementAndGet(); return true; return false; public class VM private static long directMemory = 64 * 1024 * 1024; //可以通过 -XX:MaxDirectMemorySize=\\<size\\> 进行直接内存容量设置 public static void saveProperties(Map<String, String> props) // Set the maximum amount of direct memory. This value is controlled // by the vm option -XX:MaxDirectMemorySize=<size>. // The maximum amount of allocatable direct buffer memory (in bytes) // from the system property sun.nio.MaxDirectMemorySize set by the VM. // If not set or set to -1, the max memory will be used // The system property will be removed. String s = props.get("sun.nio.MaxDirectMemorySize"); if (s == null || s.isEmpty() || s.equals("-1")) // -XX:MaxDirectMemorySize not given, take default directMemory = Runtime.getRuntime().maxMemory(); else long l = Long.parseLong(s); if (l > -1) directMemory = l;
6.3.2 异常解决
-
升级 lettuce 客户端
-
切换使用 jedis(暂时使用此方案)
redisTemplate 为spring对 lettuce、jedis 的再封装
-
剔除 spring-boot-starter-data-redis 中 lettuce 的依赖:pom.xml
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency>
-
导入 jedis 依赖
<!-- Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
-
修改 jedis 连接池配置,减少由于网络吞吐量低造成的连接超时
spring: redis: jedis: pool: max-active: 8 max-idle: 8
-
重新启动 商品服务 ,再次对接口进行压测。异常减少
6.4 高并发下的缓存失效问题👇
6.4.1 缓存穿透
- 将空结果进行缓存
6.4.2 缓存雪崩
- 过期时间设置为随机值
6.4.3 缓存穿透
- 查询数据库加锁
分布式下如何加锁
6.5 API:本地锁解决缓存失效💡
6.5.1 初步解决:加本地锁💡
cn.lzwei.bilimall.product.service.impl.CategoryServiceImpl
- 添加空值缓存,解决缓存穿透
- 添加随机过期时间,解决缓存雪崩问题
- 查询数据库加锁,解决缓存击穿问题
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService
@Resource
StringRedisTemplate stringRedisTemplate;
/**
* 获取分类数据,添加缓存:用于渲染二级、三级分类
* TODO 产生堆外内存溢出 OutOfMemoryError
*/
@Override
public Map<String, List<CategoryLevel2Vo>> getCatalogJson()
//1.从缓存中获取
String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
//2.缓存中没有
if(StringUtils.isEmpty(catalogJson))
System.out.println("---没有缓存数据,需要查询数据库!---");
//2.1、从数据库获取
Map<String, List<CategoryLevel2Vo>> categoryLevel2 = getCategoryLevel2();
//2.2、转换为json字符串存进缓存中
String jsonString = JSON.toJSONString(categoryLevel2);
//改动:判空并解决缓存穿透
if(StringUtils.isEmpty(jsonString))
//改动:添加随机过期时间解决缓存雪崩
stringRedisTemplate.opsForValue().set("catalogJson","isNull",new Random().nextInt(180)+60,TimeUnit.SECONDS);
以上是关于谷粒商城高级篇缓存与分布式锁的主要内容,如果未能解决你的问题,请参考以下文章