业务梳理

125 阅读10分钟

product产品服务

1. 先写了三级菜单

  • 三级菜单的一个递归查询展示
  • 然后对菜单有个删除的功能:什么菜单可以删除呢?没有子菜单,且没有被其他地方引用。
  • 只有是菜单没有子菜单的时候,后边才会显示delete按钮
  • 只有菜单是一级或二级菜单的时候,后边才会显示append按钮
  • 上面的这两个,可以通过前端vue来控制显示

2. 上传文件:阿里云

  • 上传阿里云,设置公共读,这里需要配置云的账号密码等信息。
  • 普通上传方式:服务器自己用账号密码把文件传给云oss image.png
  • 服务端签名后直传 上传之前先找服务器要一个令牌签名,还有oss地址,bucket之类的信息,得到之后浏览器直接上传到oss,oss对验签做验证。 image.png
  • springboot的starter方式 image.png

引入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

image.png

基础的jsr3校验

  1. 给bean添加校验注解如@Email,@NotNull等。import javax.validation.constraints.NotBlank;
  2. 在controller的方法,参数列表上开启校验注解,@Valid,这样校验才会生效。
  3. 一般调用接口返回400的错误,一般就是参数校验不通过。
  4. 如果想知道valid的具体校验结果,可以在参数后面紧接着跟一个BindingResult的参数,他会接收校验的结果,我们只需要遍历这个即可。

image.png

一般调用接口返回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 image.png

image.png

  • @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") 告诉类,扫描这个包下的controller,接受异常
  • @ExceptionHandler(value = Exception.class),放在方法上。告诉springmvc,我们上面这个集中的处理异常的处理类的这个方法,能处理什么异常。

image.png

处理任意的异常@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会有规范。

image.png

分组校验的引入原因

·· 比如brandId字段,在新增的时候,我们是不需要知道的,但是在修改的时候,必须知道id是多少。所以这个字段在两种不同的情况下,校验逻辑是不一样的。

压测

最简单的优化

image.png 优化分析 首先考虑自己的应用是 CPU 密集型,还是 IO 密集型

  • CPU密集型:大量的计算 对数据进行排序、过滤、整合等等 解决方法:升级服务器、加上CPU
  • IO密集型: 网络传输 磁盘io 数据库io redisio 解决方法:换SSD、加内存条、升级网卡等等

jvm处的优化

根据下图发现,伊甸园区和老年代的内存经常发生清理内存的情况,带来很大的延迟,说明jvm中这两个地设置的太小了

image.png 设置jvm的堆内存大小:-Xmx 1024m -Xms1024m -Xmm512m image.png

思路串讲

基础优化1

  • 写好了基本的业务逻辑以后,压测,发现product服务的一些查询接口效果不理想,然后考虑优化
  • 优化一:菜单渲染浪费时间:thymeleaf的渲染比较浪费资源。所以开启thymeleaf缓存,且Log日志打印的时候也会消耗一定资源,所以把日志级别设置为error。
  • 优化二:nginx动静分离:请求包括了动态和静态资源,静态资源我们选择交给nginx来做,所以配置nginx的动静分离。这里先把静态资源放到nginx的路径下,然后把代码里请求静态资源的路径前全加上一个/staic,最后在nginx的配置里做全局路由,将带/staic的请求路由到自己的静态资源路径,这样就不耗费tomcat的资源了。
  • 优化三:数据库加索引:给经常查询的数据字段,在db中添加索引,提高检索效果。
  • 优化四:优化业务,getCatalogJson方法中,反复的嵌套查询db,带来很大的性能浪费。我们优化业务,选择在一开始就查出所有的db数据,后边的各种嵌套中,就基于全量数据来做stream筛选。
  • 压测性能对比列表 image.png
  • 优化五:以上优化后,压测的时候用jvisualvm查看内存情况,发现eden区和old老年代区频繁的GC,说明太小了,我们设置大点的内存来优化。

进阶优化2:引入分布式缓存reids

  • 做了以上优化后,我们发现三级数据接口的性能还是很差,这个时候就不是业务所能优化的了。于是我们引入缓存来处理。引入缓存,避免查db的低效率。
    1. 引入本地缓存,最基本的可以用一个HashMap来存全局,但是这种本地的有个问题,本地缓存与当前工程在同一个项目里面,相当于是一个副本,单体应用没问题,但分布式部署就会有问题,一个服务启10个,由于缓存是本地的,A服务访问不了B访问的缓存,所以还是得去查db。且本地缓存A访问改了以后,还得去改其他剩余9个服务,不然会产生数据一致性问题。
    • 问题:a.由于缓存是本地的,所以分布式部署的其他服务还是得去查db。b.分布式的多个服务之间的数据一致性问题。
    1. 引入分布式缓存redis。集中式的缓存中间件
    • 好处:a.redis本身也可以集群部署和分片存储,这样理论上存储量可以无限。 b.维护起来简单,可以做高可用,高性能。

进阶优化3:高并发带来的缓存问题

    1. 缓存穿透、缓存雪崩、缓存击穿。
  • 穿透是有不存在的数据来请求,所以都打在了db上。解决方法可以设置null值。
  • 雪崩是多个热点数据设置了同样的超时时间,同时失效。解决方法是在expired_time的时候加随机值
  • 击穿是热点key失效,大量请求打过来,这个时候就对其加锁来保证互斥访问,保证db的安全性,不被击穿。
    1. 前两个缓存穿透和雪崩都简单,设置超时时间或者null值都可以解决。而缓存击穿需要加锁,这个就有很大学问了。

进阶优化4:加锁

    1. 本地锁:syncronized(this)。this这个监视器可以锁住吗?由于springboot中的容器都是单例,也就是只有一个,所以即使来了一百万请求调用,也只有这一个实例,故可以锁住。但这个锁仅仅是在单体应用的情况下。在分布式部署中,每个微服务都有一个锁,这种情况下确实没有完全锁住。所以需要引入分布式锁。

image.png

    1. 分布式锁: image.png 分布式锁在使用的时候,需要注意,加锁时操作的原子性,和删除时操作的原子性。加锁原子性用setnx_ex指令保证。删除时的原子性,需要用lua脚本来完成。
  1. 加锁的一些问题和演进:基本的逻辑链条是:加锁=》执行业务=》解锁
  • 执行业务的时候异常,则锁不会被释放,会永远造成死锁,所以要加超时时间,并且加锁和超时时间应该是原子操作。这个利用set_nx_ex指令完成。

  • 删除锁的时候,会有删除的不是自己的锁的问题:expiredTime设置10s,而业务执行了30s,则请求A的业务还没完成,锁已经失效了,这时请求B抢锁进来,业务执行了9s后删除锁,但删除的是A的锁,不是自己的锁。请求C也是这样,每个人删除的都是自己前面的锁。所以这里删除之前要先判断是不是自己的锁。 锁永久失效(进程释放了不属于自己的锁)的问题 锁永久失效,意味着加锁失败,而加锁是为了解决缓存击穿的问题,加锁失败意味着不能解决。

    • 解决方法:删除锁的时候,判断一下是不是自己的锁,是的话再删除。而判断和删除操作同样需要原子性保证,这里调用lua脚本来操作。
  • 剩下的问题是:业务处理超时了怎么办?一般会引入自动续租的操作。这部分用redission来解决。

自旋式等待和 阻塞式等待。

image.png

image.png

  • 自旋锁和阻塞锁的区别 阻塞锁,涉及到线程的状态切换,这种状态切换需要切换到内核态来完成。这个内核的切换开销,有时候会比业务执行还长。所以为了避免这种开销,又为了让当前的线程等一下,就可以让线程自旋。 image.png
  • redission的使用:锁的阻塞式等待、自动续期和自动删除

image.png

  • 看门狗机制:lock指定超时时间的话,是不会自动续期的,这点需要格外注意。

分布式锁-高级锁类JUC

闭锁的演示

image.png

信号量-可以用来做限流

分布式锁的名字key,会作为锁的粒度

image.png

缓存和db的数据一致性问题

image.png

  • 双写模式和失效模式

image.png

image.png

缓存数据一致性-解决方案

image.png

  • canal中间件,类似mysql的从机,监测主库一旦有变动,立刻同步 image.png 利用canal来解决一切。 另外分析这些不一致场景的时候,可以从读读、读写、写写的这种角度去区分和切入。

读写锁的特点

  • 写写操作的话排队,读读操作无所谓。读写操作的话会互斥。
  • 经常读和经常写的情况下,读写锁会有性能损耗,但是经常读,偶尔写的情况,因为读写锁的读锁相当于无锁,所以基本没有性能损耗。

image.png

最终解决方案:缓存设置过期时间+失效模式+读写锁

总结

用缓存的话,要考虑缓存的两种模式:读模式和写模式。

  • 读模式:先查缓存,不命中的话再读db,读完写入db
  • 写模式:双写模式或失效模式。

Spring Cache

image.png