1.商城业务-首页功能

280 阅读6分钟
  • 本文只针对后端服务进行阐述,前端内容不详细说明。
  • 雷神说:该板块不采用前后端分离的方式进行开发:前后端分离会屏蔽掉很多细节;

部署架构如下所示:

  • 客户发送请求,由nginx统一接收;
  • 使用nginx做反向代理,处理用户发出的请求,并将对应请求数据转发给网关;
  • 网关路由到各个服务模块;
  • 各个模块中的页面写在各个模块下,而页面引用的静态资源可以部署到nginx里面。实现部署的动静分离

相关资料表明:动静分离是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问,减轻后台压力

image-20221231150123754.png

开发说明:首页关于商品的相关信息就放在product模块中进行开发,而检索页就在search模块中进行开发

1.登录页&一级分类信息

  • 目的是将登录的时候输入/index、/index.html的时候同样会跑到首页,并在获取首页的同时将一级分类信息进行获取并由前端进行展示;

  • 创建web包,将web的接口定义在这里面,之前相关的controller内容主要是对接手机等进行访问的,将其放在app包下;

    @GetMapping({"/", "/index.html", "/index"})
    public String getIndex(Model model) {
        // 在访问首页的时候,同时需要获取到一级分类
        List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
        // 方便前端进行获取
        model.addAttribute("categorys", categoryEntities);
        // SpringBoot默认配置了前缀(classpath:/templates/)与后缀(.html),视图解析器会进行拼串
        return "index";
    }
    

    在对应的service层实现getLevel1Categorys()方法

    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        // 根据数据库信息,一级分类即父id为0的数据
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }
    

2.渲染二三级分类数据

  • 目的是在对应的一级分类下显示对应的二级分类以及三级分类 image-20230101145827142.png

  • 首先找到获取数据信息的位置

    $.getJSON("index/json/catalog.json",function (data) {
    

    之前是一个文件用来存储相关数据,将存放的json数据通过json在线解析之后,根据其数据格式封装成为一个vo,方便存储数据。

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Catelog2Vo {
        // 一级父分类id
        private String catalog1Id;
        // 三级子分类
        private List<Catelog3Vo> catalog3List;
        private String id;
        private String name;
    
        @NoArgsConstructor
        @AllArgsConstructor
        @Data
        public static class Catelog3Vo {
            // 父分类,2级分类id
            private String catalog2Id;
            private String id;
            private String name;
        }
    }
    
  • 数据是一个大的json对象,里面的key是一级分类的id,value是一个集合,集合中的每个元素就是我们上面封装的Catelog2Vo格式的数据;因此我们在对应接口方法上就需要使用@ResponseBody注解,将返回的Map格式的数据封装为json对象

    @ResponseBody
    @GetMapping("/index/catalog.json")
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        Map<String, List<Catelog2Vo>> map = categoryService.getCatelogJson();
        return map;
    }
    
  • 实现方法:流程就是先拿到1级分类的信息,然后根据1级分类的cat_id作为二级分类的parent_id来查找二级分类的信息,然后同样的方法找到三级分类;最终将VO中的实体类的各个属性封装好返回即可。

    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        // 1.先查出所有1级分类
        List<CategoryEntity> level1Categorys = this.getLevel1Categorys();
        // 2.封装数据,将对应的k封装成对应的catelogid,v封装称对应的Catelog2Vo
        Map<String, List<Catelog2Vo>> getCatelogJson = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1.根据对应的1级分类id查找到对应的二级分类
            List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
            // 2.继续将上面的得到的结果封装
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 1.找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Categorys = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
                    if(level3Categorys != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Categorys.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
    
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
    
            return catelog2Vos;
        }));
        return getCatelogJson;
    }
    
  • 在前端就可以使用thymeleaf获取数据并循环处理,进行页面显示。

3.Nginx + Windows搭建域名服务

声明:自身情况是阿里云服务器+备案过的域名+本地nginx

预备知识:正向代理与反向代理

  • 正向代理与反向代理的区别主要是前者由客户端使用,例如专用网络内的用户;而后者由互联网服务器使用。

  • 正向代理:在用户与其访问的网页服务器之间充当中介。这就是说用户的请求要通过正向代理后才能抵达网页。从互联网检索数据后,这些数据就会被发送到代理服务器并将其重定向后返回请求者。从互联网服务器的角度来看,这个请求是有代理服务器、而不是用户发送的。

    它可以提高专用网络用户的安全性,调节流量并通过隐藏原始 IP 地址而保持用户的匿名状态。

  • 反向代理:反向代理服务器位于后端服务器之前,将客户端请求转发至这些服务器。反向代理从客户端获取请求,将请求转发到其他服务器,然后将其转发回相关客户端,使它看起来像是初始代理服务器在处理请求。这类代理可以确保用户不会直接访问原始服务器,因此可为这类网页服务器提供匿名性

image-20230101150922026.png

image-20230101150759836.png

  • hosts文件中增加DNS映射(有域名的话,可配可不配,配的话相对来说访问快一些;建议大家都配置上,压测的时候如果使用域名,需要去本地DNS中查询

  • 下面服务器的ip地址是指你自己本地搭建的服务器的ip,如果像我使用阿里云(或其它)服务器,并且nginx部署在本地的话,而且需要在阿里云域名解析配置本地ip,这里就填写本地ip就好了。(nginx部署在本地省了内网穿透这一步)

    服务器的ip地址 域名
    
  • 测试访问es服务(9200端口)

  • 访问本地这些微服务是访问不了的,因此我们需要利用nginx做反向代理,代理到本地服务(商城首页)

nginx配置文件(部分)

  • 原理:nginx监听自己的域名:80,并代理到本机(proxy_pass配置的ip)
http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
    	# nginx监听域名:80端口,http服务的端口即80 
        listen       80;
        server_name  你自己的域名;   # 必须有分号

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
			proxy_pass 代理到的地址:端口;    # 必须有分号
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }
  • 自己备案过的域名还有一步,在阿里云服务器对域名进行解析设置

    之前我配置的是服务器的ip,它访问的是服务器里面的nginx image-20230101223253325.png

  • 可以成功访问了

这一块的总结:

  • 如果使用阿里云/腾讯等服务器的话,没有域名的话,要做内网穿透。否则访问的时候会报出没有域名备案的信息。
  • 当然最好的情况就是所有服务都在本机进行部署,就像课程中那样,这样也可以避免服务器内存不够使用的情况(狗头)。
  • 最好:nginx最好将监听的域名请求,再由网关转发给各个服务。

4.负载均衡到网关

目的:由网关从注册中心动态找到各个服务的位置

  • 配置上游服务器(即配置网关对应ip:端口,若由多个网关,可配置多个),在http块下进行配置

    upstream jiashun.fun{
    	server 192.168.101.1:88;
    }
    
  • 在server块下配置代理服务器(即配置代理给上有服务器组)

    注意:nginx代理给网关的时候,会丢失请求的host信息(可能会丢失更多的信息),可能会出现404的访问错误

    location / {
        #proxy_pass http://192.168.101.1:10001;
        # 设置nginx转发时保留host信息
        proxy_set_header Host $host;
        proxy_pass http://jiashun.fun;
    }
    
  • 在网关中,配置对应域名访问转发到哪个服务中(转发到product服务中),并配置Host域名信息

    image-20230101232159071.png

总流程:nginx监听域名:80下的请求信息,并将其代理到网关(88端口)中,再由网关对请求进行判断并转交给具体的服务。

image-20230103185121648.png

5.压力测试

影响性能考虑的点包括:

  • 数据库
  • 应用程序
  • 中间件(tomcat,nginx等)
  • 网络
  • 操作系统
  • ...

考虑的点:

  • 考虑自己的应用术语CPU密集型还是IO密集型

接下来使用JMETER对各个内容进行压测

  • 这里采用的是雷神的结果,作者将mysql放在了阿里云服务器,所以无论如何优化,由于服务器配置(1核2G)提升不是很明显。
压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx50233511944
Gateway5010367831
简单服务(直接请求,不通过nginx)5011341817
首页一级菜单渲染50270**(db,thymeleaf)**267365
首页渲染(开缓存)50290251365
首页渲染(开缓存、优化数据库、关日志)50700105183
三级分类数据获取502(db)/8(加索引)
三级分类(优化业务50111571896
三级分类 (使用redis作为缓存)50411153217
首页全量数据获取507**(静态资源)**
/11(动静分离)
Nginx+Gateway50
Gateway+简单服务50312630125
全链路5080088310

观察上表可得到影响吞吐量的因素

  1. 中间件

    中间件越多,性能损失越大,大多损失在网络交互(I/O)了

  2. 业务

    thymeleaf渲染:解决方案是开启缓存

    数据库:解决方案是mysql优化(添加索引);部分业务使用redis

    静态资源:每次访问静态资源也要请求一次服务器,非常耗性能。解决方案:将静态资源放在nginx服务器上,实现动态分离(如下图所示)。 image-20230102210752277.png

  • 将项目中static目录下的静态资源放在nginx的html目录中:E:\nginx-1.22.1\html

  • 将前端页面访问静态资源的路径与nginx存放静态资源的路径进行统一

  • 修改nginx配置文件,将获取指定静态资源的路径指向到nginx存放静态资源的目录中 image-20230103163229275.png

修改为动静分离之后,性能提升了一些,但不多。这次发现堆中的老年代内存饱满,慢在了老年代频繁GC,因此我们需要JVM调参。

-Xmx1024m -Xms1024m -Xmn512m

下面简单介绍一下JVM调优的参数

  • -Xms :设置JVM堆初始内存大小,此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。例如:-Xms1024m。
  • -Xmx :设置JVM堆最大可用内存,例如:-Xmx1024m
  • -Xmn :设置JVM堆中新生代的内存大小,Sun官方推荐配置为整个堆大小的3/8。
  • -Xss:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。操作系统对一个进程内的线程数有限制。

优化业务逻辑

  • 我们看到写的获取二三分类数据的时候与数据库做了很多次交互进行查询selectList(),使得性能降低;

    优化策略:可以在最开始预先查出所有的分类,然后保存起来;下面操作对应的数据的时候,直接从保存的集合中进行获取即可

  • 而我测的时候没有提升,是因为我把mysql放在了阿里云服务器上了,本身服务器的带宽就不是很高。

  • 总的来说,看雷神测试的结果提升也不是很明显,最重要的是接下来介绍的缓存优化

// 总查询
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 需要的时候调用下面这个方法
private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList, Long parentCid) {
    List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
    return collect;
}

声明

  1. 若存在错误的内容,请在评论区留言,及时修改。
  2. 笔记内容来自尚硅谷项目--谷粒商城。
  3. 欢迎各位大佬指正,共同进步!!