product产品服务
1. 先写了三级菜单
- 三级菜单的一个递归查询展示
- 然后对菜单有个删除的功能:什么菜单可以删除呢?没有子菜单,且没有被其他地方引用。
- 只有是菜单没有子菜单的时候,后边才会显示delete按钮
- 只有菜单是一级或二级菜单的时候,后边才会显示append按钮
- 上面的这两个,可以通过前端vue来控制显示
2. 上传文件:阿里云
- 上传阿里云,设置公共读,这里需要配置云的账号密码等信息。
- 普通上传方式:服务器自己用账号密码把文件传给云oss
- 服务端签名后直传
上传之前先找服务器要一个令牌签名,还有oss地址,bucket之类的信息,得到之后浏览器直接上传到oss,oss对验签做验证。
- springboot的starter方式
引入common依赖的同时,不引入其中的mybatis
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
3.1 编写自定义的校验注解
@RestControllerAdvice = ControllerAdvice + ResponseBody
- @NotNull 不能为null,但可以为empty
- @NotEmpty 不能为null,而且长度必须大于0
- @NotBlank 只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
基础的jsr3校验
- 给bean添加校验注解如@Email,@NotNull等。import javax.validation.constraints.NotBlank;
- 在controller的方法,参数列表上开启校验注解,@Valid,这样校验才会生效。
- 一般调用接口返回400的错误,一般就是参数校验不通过。
- 如果想知道valid的具体校验结果,可以在参数后面紧接着跟一个BindingResult的参数,他会接收校验的结果,我们只需要遍历这个即可。
一般调用接口返回400的错误,一般就是参数校验不通过
我在这里的校验注解怎么都不生效,检测发现应该是引入的依赖不对。下面是正确的
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- NotBlank也可以自己编写错误的消息提示。
C:\Users\Administrator.m2\repository\org\hibernate\validator\hibernate-validator\6.0.18.Final\hibernate-validator-6.0.18.Final-sources.jar!\org\hibernate\validator\ValidationMessages.properties
- @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") 告诉类,扫描这个包下的controller,接受异常
- @ExceptionHandler(value = Exception.class),放在方法上。告诉springmvc,我们上面这个集中的处理异常的处理类的这个方法,能处理什么异常。
处理任意的异常@ExceptionHandler(value = Throwable.class)
上面的写处理具体的某种的异常,而其他所有
3 默认异常处理
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
return R.error(400,"数据校验出现问题");
}
4 错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程
中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
开发姿势:有什么异常尽管抛出去,让我们专门的异常处理类来感知
- 而由于业务和服务众多,我们会规定返回的错误状态码。即返回的error code会有规范。
分组校验的引入原因
·· 比如brandId字段,在新增的时候,我们是不需要知道的,但是在修改的时候,必须知道id是多少。所以这个字段在两种不同的情况下,校验逻辑是不一样的。
压测
最简单的优化
优化分析
首先考虑自己的应用是 CPU 密集型,还是 IO 密集型
- CPU密集型:大量的计算 对数据进行排序、过滤、整合等等 解决方法:升级服务器、加上CPU
- IO密集型: 网络传输 磁盘io 数据库io redisio 解决方法:换SSD、加内存条、升级网卡等等
jvm处的优化
根据下图发现,伊甸园区和老年代的内存经常发生清理内存的情况,带来很大的延迟,说明jvm中这两个地设置的太小了
设置jvm的堆内存大小:-Xmx 1024m -Xms1024m -Xmm512m
思路串讲
基础优化1
- 写好了基本的业务逻辑以后,压测,发现product服务的一些查询接口效果不理想,然后考虑优化
- 优化一:菜单渲染浪费时间:thymeleaf的渲染比较浪费资源。所以开启thymeleaf缓存,且Log日志打印的时候也会消耗一定资源,所以把日志级别设置为error。
- 优化二:nginx动静分离:请求包括了动态和静态资源,静态资源我们选择交给nginx来做,所以配置nginx的动静分离。这里先把静态资源放到nginx的路径下,然后把代码里请求静态资源的路径前全加上一个/staic,最后在nginx的配置里做全局路由,将带/staic的请求路由到自己的静态资源路径,这样就不耗费tomcat的资源了。
- 优化三:数据库加索引:给经常查询的数据字段,在db中添加索引,提高检索效果。
- 优化四:优化业务,getCatalogJson方法中,反复的嵌套查询db,带来很大的性能浪费。我们优化业务,选择在一开始就查出所有的db数据,后边的各种嵌套中,就基于全量数据来做stream筛选。
- 压测性能对比列表
- 优化五:以上优化后,压测的时候用jvisualvm查看内存情况,发现eden区和old老年代区频繁的GC,说明太小了,我们设置大点的内存来优化。
进阶优化2:引入分布式缓存reids
- 做了以上优化后,我们发现三级数据接口的性能还是很差,这个时候就不是业务所能优化的了。于是我们引入缓存来处理。引入缓存,避免查db的低效率。
-
- 引入本地缓存,最基本的可以用一个HashMap来存全局,但是这种本地的有个问题,本地缓存与当前工程在同一个项目里面,相当于是一个副本,单体应用没问题,但分布式部署就会有问题,一个服务启10个,由于缓存是本地的,A服务访问不了B访问的缓存,所以还是得去查db。且本地缓存A访问改了以后,还得去改其他剩余9个服务,不然会产生数据一致性问题。
- 问题:a.由于缓存是本地的,所以分布式部署的其他服务还是得去查db。b.分布式的多个服务之间的数据一致性问题。
-
- 引入分布式缓存redis。集中式的缓存中间件
- 好处:a.redis本身也可以集群部署和分片存储,这样理论上存储量可以无限。 b.维护起来简单,可以做高可用,高性能。
进阶优化3:高并发带来的缓存问题
-
- 缓存穿透、缓存雪崩、缓存击穿。
- 穿透是有不存在的数据来请求,所以都打在了db上。解决方法可以设置null值。
- 雪崩是多个热点数据设置了同样的超时时间,同时失效。解决方法是在expired_time的时候加随机值
- 击穿是热点key失效,大量请求打过来,这个时候就对其加锁来保证互斥访问,保证db的安全性,不被击穿。
-
- 前两个缓存穿透和雪崩都简单,设置超时时间或者null值都可以解决。而缓存击穿需要加锁,这个就有很大学问了。
进阶优化4:加锁
-
- 本地锁:syncronized(this)。this这个监视器可以锁住吗?由于springboot中的容器都是单例,也就是只有一个,所以即使来了一百万请求调用,也只有这一个实例,故可以锁住。但这个锁仅仅是在单体应用的情况下。在分布式部署中,每个微服务都有一个锁,这种情况下确实没有完全锁住。所以需要引入分布式锁。
-
- 分布式锁:
分布式锁在使用的时候,需要注意,加锁时操作的原子性,和删除时操作的原子性。加锁原子性用setnx_ex指令保证。删除时的原子性,需要用lua脚本来完成。
- 分布式锁:
- 加锁的一些问题和演进:基本的逻辑链条是:加锁=》执行业务=》解锁
-
执行业务的时候异常,则锁不会被释放,会永远造成死锁,所以要加超时时间,并且加锁和超时时间应该是原子操作。这个利用set_nx_ex指令完成。
-
删除锁的时候,会有删除的不是自己的锁的问题:expiredTime设置10s,而业务执行了30s,则请求A的业务还没完成,锁已经失效了,这时请求B抢锁进来,业务执行了9s后删除锁,但删除的是A的锁,不是自己的锁。请求C也是这样,每个人删除的都是自己前面的锁。所以这里删除之前要先判断是不是自己的锁。 锁永久失效(进程释放了不属于自己的锁)的问题 锁永久失效,意味着加锁失败,而加锁是为了解决缓存击穿的问题,加锁失败意味着不能解决。
- 解决方法:删除锁的时候,判断一下是不是自己的锁,是的话再删除。而判断和删除操作同样需要原子性保证,这里调用lua脚本来操作。
-
剩下的问题是:业务处理超时了怎么办?一般会引入自动续租的操作。这部分用redission来解决。
自旋式等待和 阻塞式等待。
- 自旋锁和阻塞锁的区别
阻塞锁,涉及到线程的状态切换,这种状态切换需要切换到内核态来完成。这个内核的切换开销,有时候会比业务执行还长。所以为了避免这种开销,又为了让当前的线程等一下,就可以让线程自旋。
- redission的使用:锁的阻塞式等待、自动续期和自动删除
- 看门狗机制:lock指定超时时间的话,是不会自动续期的,这点需要格外注意。
分布式锁-高级锁类JUC
闭锁的演示
信号量-可以用来做限流
分布式锁的名字key,会作为锁的粒度
缓存和db的数据一致性问题
- 双写模式和失效模式
缓存数据一致性-解决方案
- canal中间件,类似mysql的从机,监测主库一旦有变动,立刻同步
利用canal来解决一切。 另外分析这些不一致场景的时候,可以从读读、读写、写写的这种角度去区分和切入。
读写锁的特点
- 写写操作的话排队,读读操作无所谓。读写操作的话会互斥。
- 经常读和经常写的情况下,读写锁会有性能损耗,但是经常读,偶尔写的情况,因为读写锁的读锁相当于无锁,所以基本没有性能损耗。
最终解决方案:缓存设置过期时间+失效模式+读写锁
总结
用缓存的话,要考虑缓存的两种模式:读模式和写模式。
- 读模式:先查缓存,不命中的话再读db,读完写入db
- 写模式:双写模式或失效模式。