谷粒商城高级篇笔记1


0、ElasticSearch

ElasticSearch笔记


1、Nginx配置域名问题


01、Nginx(反向代理) 配置


> 正向代理和反向代理

Nginx6


配置反向代理

我们打算访问 wulawula.com 网址转到我们本地的localhost:10000

1、使用 SwitchHosts 工具 配置域名 使用管理员方式打开

Nginx1

2、使用 自定义的域名访问 默认访问的是Nginx自定义的html页面

Nginx2

3、打开 nginx.conf 路径:/mydata/nginx/conf/nginx.conf

nginx.conf的介绍图:

在这里插入图片描述

发现把server块放到了 conf.d文件夹下

http {
    include       /etc/nginx/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  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

4、打开 default.conf 路径:/mydata/nginx/conf/conf.d/default.conf

修改以下箭头部分

server {
    listen       80;
    server_name  wulawula.com;  <------

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
    	proxy_pass http://192.168.56.1:10000;	<------ 注意分号结尾
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #

重启nginx docker restart nginx

5、访问 wulawula.com 发现访问的是我们原来的localhost:10000的页面

在这里插入图片描述


02、Nginx(负载均衡)+ 网关 配置


1、打开 nginx.conf 路径:/mydata/nginx/conf/nginx.conf

http {
    include       /etc/nginx/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  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    
    upstream gulimall{			   <------ 上游服务器组 名称为 gulimall
        server 192.168.56.1:88;    <------ 配置服务器 (此处配置网关)
    }							   <------

    include /etc/nginx/conf.d/*.conf;
}   

2、打开 default.conf 路径:/mydata/nginx/conf/conf.d/default.conf

server {
    listen       80;
    server_name  wulawula.com;	<------

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_pass http://gulimall;  <------找到上游服务器组,此处上有服务器组的名称为 gulimall
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html

3、增加gateway配置文件的网关规则

注意:此规则一定要放到其他所有规则的最后面

# nginx + gateway 配置域名访问 wulawula.com
- id: gulimall_host_route
	# lb 代表的就是负载均衡
	uri: lb://gulimall-product
	predicates:
		- Host=**.wulawula.com

4、然后发现报错404 原因:nginx代理给网关的时候,会丢失请求的host信息 需要添加 proxy_set_header Host $host;

server {
    listen       80;
    server_name  wulawula.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_set_header Host $host;	<------
        proxy_pass http://gulimall;
    }

重启nginx docker restart nginx

5、访问 wulawula.com 发现访问的是我们原来的localhost:10000的页面

在这里插入图片描述


原理图

在这里插入图片描述


03、Nginx动静分离


1、在html文件夹下 新建static文件夹 路径: /mydata/nginx/html/static

2、把静态资源放到了 static下 【static例包含index index下包含js、css、img】 把项目resources/static的静态资源删除

3、修改 default.conf 路径: /mydata/nginx/conf/conf.d/default.conf

动静分离1

4、重启 nginx docker restart nginx

5、访问 wulawula.com 静态资源正常显示

动静分离2


2、JMeter 压力测试


影响性能考虑点包括:
  • 数据库、应用程序、中间件( tomact、Nginx)、网络和操作系统等方面首先考虑自己的应用属于CPu密集型还是I0密集型

01、基本测试


1、创建一个 **线程组**

JMeter1

2、设置线程组参数 所谓线程数就是并发用户数

JMeter2

3、在线程组下创建 HTTP请求

JMeter3

4、设置 HTTP参数(此处以百度为例)

在这里插入图片描述

5、在线程组下创建 监听器 此处以:查看结果树、汇总报告、聚合报告、汇总图 为例

JMeter4

6、点击执行 执行完后 自动停止

JMeter11

点击执行完后可以看到数据

  • 查看结果树
  • 可以发现每次 HTTP请求都完成

JMeter6

  • 汇总报告 单位毫秒
  • 平均1.241s 最小0.015s 最大5.265s 异常0%

JMeter7

  • 聚合报告 单位毫秒
  • 90%的在 1.465s完成 95%的在1.649s完成 99%的在2.514s完成

在这里插入图片描述

  • 汇总图 单位毫秒
  • 可以看到图表信息

JMeter9

在这里插入图片描述

注意:自己写的项目。压测数据的时候 测试的数据和此处的内存大小也有关系

在这里插入图片描述


02、尝试用大于5000的TCP端口连接时发生错误


JMeter Address Already in use错误解决 `地址被占用`

原因

windows本身提供的端口访问机制的问题。

Windows提供给TCP/IP链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。

在这里插入图片描述

在这里插入图片描述

解决


1.cmd中,用regedit命令打开注册表

2.在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,

​ 1.右击parameters,添加一个新的 DWORD,名字为 MaxUserPort

JMeter15

​ 2 .然后双击MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作哦)

JMeter16

也可以设置这个:

  • 右击parameters,添加一个新的 DWORD,名字为 TCPTimedWaitDelay
  • 双击TCPTimedWaitDelay 输入数值数据为 30 (原来的回收时间是4分钟,此处的含义为回收时间改为30秒

在这里插入图片描述

3.修改配置完毕之后记得重启机器才会生效


3、性能监控


01、jconsole


1、cmd打开命令窗口 输入 jconsole 回车

在这里插入图片描述

2、选择我们想要查看的 微服务 ,此处以红色箭头处为例

在这里插入图片描述


02、jvisualvm


1、cmd打开命令窗口 输入 jvisualvm回车

在这里插入图片描述

2、选择我们想要查看的 微服务 ,此处以红色箭头处为例

在这里插入图片描述

  • 运行:正在运行的
  • 休眠:调用sleep方法的
  • 等待:调用wait方法的
  • 驻留:线程池里面的空闲线程
  • 监视:阻塞的线程,正在等待锁的

扩展插件

1、点击 工具 -> 插件 -> 可用插件 -> 检查最新插件

在这里插入图片描述

2、安装完成后 退出 重新打开 jvisualvm

在这里插入图片描述


扩展插件 报错

如果点击 检查最新版本 时有以下错误

在这里插入图片描述

解决:

  • 1、cmd查看自己的 jdk 的版本 java -version 举例:此次电脑上的jdk版本为 java version “1.8.0_231”
  • 2、打开 https://visualvm.github.io/pluginscenters.html 找到自己的 jdk对应的版本 点击链接进去

jvisualvm4

  • 3、复制 此处URL

jvisualvm5

  • 4、复制到此处 注意URL的后缀一定是 updates.xml.gz

在这里插入图片描述


03、jvisualvm + JMeter 结合测试

测试截图数据略…


因为我们的这个测试项目的构架是这样的:

请求 -> Nginx -> GateWay -> 服务集群的商品服务 --- 服务处理完成 -> GateWay -> Nginx -> 请求发送者

所以我们需要先测试中间件 Nginx GateWay

测试Nginx

Nginx的端口为80 本地Linux地址为 192.168.56.10 压力测试为1秒200个线程循环次数为无限

测试GateWay

Nginx的端口为88 地址为 localhost 压力测试为1秒200个线程循环次数为无限

开始压力测试 发现GateWay的CPU占用率高

测试简单服务

//测试简单服务 用于调优
@ResponseBody
@GetMapping("/hello")
public String hello(){
  return "hello";
}

简单服务的端口为10000 地址为 localhost http://localhost:10000/hello 压力测试为1秒200个线程循环次数为无限

测试GateWay+简单服务

  • 加入网关配置yml
- id: product_route
	uri: lb://gulimall-product
    predicates:
		- Path=/api/product/**,/hello  
    filters:
    	- RewritePath=/api/(?<segment>.*),/$\{segment}  #路径重写

GateWay+简单服务的端口为88 地址为localhost http://localhost:88/hello 压力测试为1秒200个线程循环次数为无限

全链路

GateWay + Nginx + 简单服务

全链路的端口为80 地址为wulawula.com http://wulawula.com/hello 压力测试为1秒200个线程循环次数为无限

页面一级菜单渲染

页面一级菜单渲染的端口为10000 地址为localhost 压力测试为1秒200个线程循环次数为无限

需要渲染前台。慢

三级分类数据获取

三级分类数据获取的端口为10000 http://localhost:10000/index/catalog.json 地址为localhost 压力测试为1秒200个线程循环次数为无限

需要反复查数据库。慢

首页全量数据获取

首页全量数据获取的端口为10000 http://localhost:10000 地址为localhost 压力测试为1秒200个线程循环次数为无限

高级 如下设置: 并行下载可以限制一下 默认为6

首页全量数据获取 (开缓存、优化数据库、关日志) 优化

  • 开启缓存 调整日志级别
#开启缓存
spring  
  thymeleaf:
	cache: true
      
#调整日志级别
logging:
  level:
    com.wulawula.gulimall: error
  • 添加索引

4、优化


Nginx动静分离

  • 1.03、有详细

首页全量数据获取 (开缓存、优化数据库、关日志) 优化

  • 开启缓存 调整日志级别
#开启缓存
spring  
  thymeleaf:
	cache: true
      
#调整日志级别
logging:
  level:
    com.wulawula.gulimall: error
  • 添加索引

优化测试22

设置JVM

-Xmx1024m -Xms1024m -Xmn512m

  • -Xms:JVM 初始分配的内存 -> 数值大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。
  • -Xmx:JVM最大可用内存 -> 如果程序运行需要占用更多的内存,超出了这个设置值,会抛出OutOfMemory异常
  • -Xss: 每个线程的堆栈大小 -> 根据应用的线程所需内存大小进行调整。
  • -Xmn:年轻代大小 -> 整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小

3级分类优化业务

  • 优化: 抽取方法 —> 将数据库的多次查询变为1次
//处理2级和3级
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        /*
        *   【优化】
        *   1、将数据库的多次查询变为1次
        */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);

        //2、封装1级的数据                                                                         // k:一级分类的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
            //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
            List<CategoryEntity> level2Catelog = getParent_cid(selectList,v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

            //2、封装2级的数据
            List<Catelog2Vo> catelog2Vos = null;

            /*2级分类的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一级分类的id
                     * catalo3List在下面
                     * l2.getCatId() 二级分类的id
                     * l2.getName()  二级分类的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                    //1、找当前二级分类的三级分类封装成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                    if (level3Catelog != null){
                        /*3及分类的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            /***
                             * item.getCatId() 2级节点的id
                             * l3.getCatId() 当前3级节点的id
                             * l3.getName()  当前3级节点的名字
                             */
                            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 parent_cid;
    }

    //抽取方法
    /*
    *   List<CategoryEntity> selectList 从这个里面查询
    *   Long parent_cid 条件
    */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

优化1

缓存Redis

压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx2001363.761949
GateWay2005070.1251340
简单服务20011596.63770
页面一级菜单渲染200290.78041443
三级分类数据获取20010.7(db)1857418610
三级分类数据获取(优化业务)200131.622203781
三级分类(Redis)200408.56341374
首页全量数据获取20011.6(静态)2347724138
首页全量(开缓存、优化数据库、关日志)20058.81543314579
Nginx+GateWay200
GateWay+简单服务2001780.21401975
全链路200567.115864956

中间件越多,性能损失越大,大多都损失在网络交互了

业务:DB(MySql优化)、模板的渲染速度(上线需要打开缓存)、静态资源 影响


5、缓存


为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访间。而db承担数据落盘工作。

哪些数据适合放入缓存:

  • 即时性、数据一致性要求不高的
  • 访问量大且更新频字不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

缓存1


01、本地缓存

不推荐

在这里插入图片描述

/* 【优化2  将数据库的多次查询变为1次 且加入本地缓存 】 --------------------------------------------------------------------------------------------*/
    //处理2级和3级
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        /* 自定义缓存  本地缓存 */
        //1、如果缓存中有 就用缓存的
        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) cache.get("catalogJson");
        //2、如果没有就查数据库   且最后给缓存放一份
        if (catalogJson == null) {

            /*
             *   【优化】
             *   1、将数据库的多次查询变为1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封装1级的数据                                                                         // k:一级分类的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
                //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

                //2、封装2级的数据
                List<Catelog2Vo> catelog2Vos = null;

                /*2级分类的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一级分类的id
                         * catalo3List在下面
                         * l2.getCatId() 二级分类的id
                         * l2.getName()  二级分类的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                        //1、找当前二级分类的三级分类封装成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                        if (level3Catelog != null) {
                            /*3及分类的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封装成指定格式
                                /***
                                 * item.getCatId() 2级节点的id
                                 * l3.getCatId() 当前3级节点的id
                                 * l3.getName()  当前3级节点的名字
                                 */
                                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;
            }));
            //3、给缓存放一份
            cache.put("catalogJson",parent_cid);
            return parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 从这个里面查询
     *   Long parent_cid 条件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

02、分布式缓存 Redis

推荐

缓存3

  • pom
<!--引入Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

优化3:Redis 【注意:value转为了JSON字符串存取】

【注意:因为给Redis存入的是JSON字符串,取出JSON字符串的时候需要逆转为能用的对象类型 --> 序列化与反序列化】

Alibaba.fastJson

  • 对象 -> JSON:
String str = JSON.toJSONString(infoDo);
  • JSON -> 对象:
InfoDo infoDo = JSON.parseObject(strInfoDo, InfoDo.class);
  • 对象集合 -> JSON:
String users = JSON.toJSONString(users);
  • JSON -> 对象集合:
List<User> userList = JSON.parseArray(userStr, User.class);
  • 这样写会出现问题 注意看 18、03、 堆外内存溢出问题
 /*
 *   优化3:Redis  【注意:value转为了JSON字符串存取】
 *	【注意:因为给Redis存入的是JSON字符串,取出JSON字符串的时候需要逆转为能用的对象类型 --> 序列化与反序列化】
 */
    @Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        //1、加入缓存逻辑
        //从Redis中获取缓存
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判断是否为空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果缓存中没有数据
            //查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
            //3、查到的数据再放入缓存,
            //因为用的是StringRedisTemplate 接受的value值是一个String 所以 catalogJsonDb 不能直接放入,需要转换一下
            //将查到的对象转为 JSON 放入缓存中
            /* 缓存中存的是 JSON 字符串,优点:JSON 有跨语言跨平台的特点 */
            String s = JSON.toJSONString(catalogJsonDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
        }
        //4、转为我们指定的对象(查完数据库 或者 Redis有缓存)
                                                             // 要转换的JSON    要转换的类型 把自己想要的类型给 TypeReference 这个方法是受保护的,我们需要自己写一个静态内部类来处理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回转换后的结果
        return result;
    }   


//处理2级和3级
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {
        /*
         *   【优化】
         *   1、将数据库的多次查询变为1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封装1级的数据                                                                         // k:一级分类的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
            //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

            //2、封装2级的数据
            List<Catelog2Vo> catelog2Vos = null;

            /*2级分类的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一级分类的id
                     * catalo3List在下面
                     * l2.getCatId() 二级分类的id
                     * l2.getName()  二级分类的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                    //1、找当前二级分类的三级分类封装成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                    if (level3Catelog != null) {
                        /*3及分类的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            /***
                             * item.getCatId() 2级节点的id
                             * l3.getCatId() 当前3级节点的id
                             * l3.getName()  当前3级节点的名字
                             */
                            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 parent_cid;

    }


    //抽取方法
    /*
     *   List<CategoryEntity> selectList 从这个里面查询
     *   Long parent_cid 条件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

03、堆外内存溢出问题 OutOfDirectMemoryError


原因:

  1. springboot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行通信
  2. lettuce 的 bug 导致 netty 堆外内存溢出
  3. -Xmx300m:如果没有指定堆外内存,netty 默认使用 堆内存(Xmx) 作为 堆外内存

解决方案:

注意:不能使用 -Dio.netty.maxDirectMemory 只调大堆外内存

  1. 升级lettuce客户端
  2. 切换使用jedis [推荐]
  • pom
<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>
<!--  移除默认的 lettuce-core 核心,加入 jedis 核心  -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • lettuce、jedis 都是操作redis的底层客户端 Spring 将其两个进行了再封装 --> RedisTemplate

缓存6


04、缓存穿透、雪崩、击穿


  • 缓存穿透 --> 空结果缓存
  • 缓存雪崩 --> 设置过期时间(加随机值)
  • 缓存击穿 --> 加锁

缓存穿透

指高并发情况下一直查询一个不存在的数据。查询到null,但是并没有将null放入缓存,就进行多次查数据库,导致数据库瞬时压力增大,最终导致崩溃

缓存7

缓存雪崩

指缓存设置的过期时间一致,在某一时刻缓存大面积失效,高并发情况下的查询,进而转到了数据库进行查询,导致数据库瞬时压力过大,最终导致崩溃

在这里插入图片描述

缓存击穿

指失效的缓存为查询频率很高的某一个单点key --> 热点数据,在某一时刻失效,高并发情况下的查询,进而转到了数据库进行查询,导致数据库瞬时压力过大,最终导致崩溃

缓存9


6、本地锁和分布式锁 解决击穿


01、本地锁 synchronized 不推荐


使用this的方式

只要是同一把锁,就能锁住需要这个锁的所有线程

  • 1、代码块 使用 synchronized

    synchronized(this) :SpringBoot所有的组件在容器中都是单例的 this就是当前实例对象

  • 2、方法 使用 synchronized

    直接给方法加synchronized也是可以的

    public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { }
    
@Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        

        //1、加入缓存逻辑
        //从Redis中获取缓存
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判断是否为空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果缓存中没有数据
          	System.out.println("缓存不命中...查询数据库...");
            //查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
            //3、查到的数据再放入缓存,
            //因为用的是StringRedisTemplate 接受的value值是一个String 所以 catalogJsonDb 不能直接放入,需要转换一下
            //将查到的对象转为 JSON 放入缓存中
            /* 缓存中存的是 JSON 字符串,优点:JSON 有跨语言跨平台的特点 */
            String s = JSON.toJSONString(catalogJsonDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
        }
        //4、转为我们指定的对象(查完数据库 或者 Redis有缓存)
      	System.out.println("缓存命中...");
        // 要转换的JSON    要转换的类型 把自己想要的类型给 TypeReference 这个方法是受保护的,我们需要自己写一个静态内部类来处理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回转换后的结果
        return result;
    }

/* 【优化2  将数据库的多次查询变为1次 且加入本地缓存 】 --------------------------------------------------------------------------------------------*/
    //处理2级和3级
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {


        /***
         *  缓存击穿 解决一:synchronized
         *  只要是同一把锁,就能锁住需要这个锁的所有线程
         *
         *  1、场景一:使用this的方式
         *      代码块 使用 synchronized
         *      synchronized(this) :SpringBoot所有的组件在容器中都是单例的  this就是当前实例对象
         *      方法 使用 synchronized
         *      public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { 直接给方法加synchronized也是可以的
         */
        //TODO 本地锁:synchronized、JUC的Lock锁,在分布式的情况下,想要锁住所有,必须使用 “分布式锁”

        synchronized (this){

            /***
             * 得到锁之后,我们应该再去缓存中确定一次,如果没有才需要继续查询
             */
          	
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            //如果缓存不为空  直接 转换对象return就可以了
            if( ! StringUtils.isEmpty(catalogJSON)){
                //JSON --> 对象
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
                //返回转换后的结果
                return result;
            }
          	System.out.println("查询了数据库 进入锁------");

            /*
             *   【优化】
             *   1、将数据库的多次查询变为1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封装1级的数据                                                                         // k:一级分类的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
                //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

                //2、封装2级的数据
                List<Catelog2Vo> catelog2Vos = null;

                /*2级分类的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一级分类的id
                         * catalo3List在下面
                         * l2.getCatId() 二级分类的id
                         * l2.getName()  二级分类的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                        //1、找当前二级分类的三级分类封装成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                        if (level3Catelog != null) {
                            /*3及分类的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封装成指定格式
                                /***
                                 * item.getCatId() 2级节点的id
                                 * l3.getCatId() 当前3级节点的id
                                 * l3.getName()  当前3级节点的名字
                                 */
                                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 parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 从这个里面查询
     *   Long parent_cid 条件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;
    }

注意:

本地锁:synchronized、JUC的Lock锁

synchronized(this) 为本地锁,只能锁住当前进程,在分布式的情况下,想要锁住所有,必须使用 分布式锁

this锁的是当前实例对象,加入集群环境下,需要每一台机器都去加synchronized(this),每一个this都是不同的锁,进而情况就是,1号机器有很多请求但是只放进了1个,2号机器也只放进了1个,导致有几台机器就放进多少个线程进来,去数据库查询相同的数据

在这里插入图片描述

  • 且存在锁的时序问题
  • 查询了数据库 进入锁------ 控制台输出了两次以上 表示不止一个线程去查了数据库

02、本地锁 的时序问题


在这里插入图片描述

会造成高并发情况下,至少两个线程以上进入 synchronized (this) 来查询数据库,

原因:假如1号线程查询完数据库,释放锁,然后1号线程往Redis中放数据,这是一次网络交互,是有时间的,在这很短的时间内,紧接着2号线程进来了,发现缓存中没有,继续查询数据库,然后继续这种循环,可能在这一段时间内,还会有更多的线程进来,所以我们需要把查询到的数据放入缓存中的这一部分放在锁里面

@Override
    public Map<String,List<Catelog2Vo>> getCatalogJson() {

        //1、加入缓存逻辑
        //从Redis中获取缓存
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判断是否为空
        if (StringUtils.isEmpty(catalogJSON)){
            //2、如果缓存中没有数据
            System.out.println("缓存不命中...查询数据库...");
            //查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDB();
			
 //===============================锁的时序问题=====================
          
        	 /***
             *  【注意】
             *
             * 注释这样写会造成高并发情况下,至少两个线程以上进入 synchronized (this) 来查询数据库,
             * 原因:假如1号线程查询完数据库,释放锁,然后1号线程往Redis中放数据,这是一次网络交互,是有时间的,在这很短的时间内,紧接着2号线程进来了,发现缓存中没有,继续查询数据库,然后继续这种循环,可能在这一段时间内,还会有更多的线程进来,所以我们需要把查询到的数据放入缓存中的这一部分放在锁里面
             */
          
//            //3、查到的数据再放入缓存,
//            //因为用的是StringRedisTemplate 接受的value值是一个String 所以 catalogJsonDb 不能直接放入,需要转换一下
//            //将查到的对象转为 JSON 放入缓存中
//            /* 缓存中存的是 JSON 字符串,优点:JSON 有跨语言跨平台的特点 */
//            String s = JSON.toJSONString(catalogJsonDb);
//            /***
//             *  设置过期时间 解决缓存雪崩
//             */
//            redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);
          
            return catalogJsonDb;

			          
        }
        //4、转为我们指定的对象(查完数据库 或者 Redis有缓存)
      	System.out.println("缓存命中...");
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        //5、返回转换后的结果
        return result;
    }

/* 【优化2  将数据库的多次查询变为1次 且加入本地缓存 】 --------------------------------------------------------------------------------------------*/
    //处理2级和3级
    public Map<String,List<Catelog2Vo>> getCatalogJsonDB() {

		==
        /*** 
         *  缓存击穿 解决一:synchronized
         *  只要是同一把锁,就能锁住需要这个锁的所有线程
         *
         *  1、场景一:使用this的方式
         *      代码块 使用 synchronized
         *      synchronized(this) :SpringBoot所有的组件在容器中都是单例的  this就是当前实例对象
         *      方法 使用 synchronized
         *      public synchronized Map<String,List<Catelog2Vo>> getCatalogJsonDB() { 直接给方法加synchronized也是可以的
         */
        //TODO 本地锁:synchronized、JUC的Lock锁,在分布式的情况下,想要锁住所有,必须使用 “分布式锁”

        synchronized (this){

            /***
             * 得到锁之后,我们应该再去缓存中确定一次,如果没有才需要继续查询
             */
          	
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            //如果缓存不为空  直接 转换对象return就可以了
            if( ! StringUtils.isEmpty(catalogJSON)){
                //JSON --> 对象
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
                //返回转换后的结果
                return result;
            }
          
          	System.out.println("查询了数据库 进入锁------");

            /*
             *   【优化】
             *   1、将数据库的多次查询变为1次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //2、封装1级的数据                                                                         // k:一级分类的id
            Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
                //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
                List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

                //2、封装2级的数据
                List<Catelog2Vo> catelog2Vos = null;

                /*2级分类的集合*/
                if (level2Catelog != null) {
                    catelog2Vos = level2Catelog.stream().map(l2 -> {
                        /***
                         * v.getCatId()  一级分类的id
                         * catalo3List在下面
                         * l2.getCatId() 二级分类的id
                         * l2.getName()  二级分类的名字
                         */
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                        //1、找当前二级分类的三级分类封装成VO
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                        if (level3Catelog != null) {
                            /*3及分类的集合*/
                            List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                                //2、封装成指定格式
                                /***
                                 * item.getCatId() 2级节点的id
                                 * l3.getCatId() 当前3级节点的id
                                 * l3.getName()  当前3级节点的名字
                                 */
                                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;
            }));
          	
 //===============================锁的时序问题=====================
          
          	//【注意这一段是从上面优化到下面的】  //在此处将数据库查询到的数据放入缓存,解决锁的时序问题
            //3、查到的数据再放入缓存,
            //因为用的是StringRedisTemplate 接受的value值是一个String 所以 catalogJsonDb 不能直接放入,需要转换一下
            //将查到的对象转为 JSON 放入缓存中
            /* 缓存中存的是 JSON 字符串,优点:JSON 有跨语言跨平台的特点 */
            String s = JSON.toJSONString(parent_cid);
            /***
             *  设置过期时间 解决缓存雪崩
             */
            redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);

          
            return parent_cid;
        }
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 从这个里面查询
     *   Long parent_cid 条件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;
    }
  • 只输出了一次:查询了数据库 进入锁------ 表示一直是只有一个线程进入了锁,锁的时序问题得以解决

03、本地锁 压力测试


1、启动 4个微服务

在这里插入图片描述

2、设置参数 清空Redis的数据

锁5

3、测试 每个微服务只打印了一次 查询了数据库 进入锁------ 一个微服务一把锁

在这里插入图片描述


04、分布式锁


在这里插入图片描述

分布式锁演化1

docker exec -it redis redis-cli   //连接客户端
docker lock wula NX  //使用NX参数 占位

分布式锁1

在这里插入图片描述

分布式锁演化2

在这里插入图片描述

EX:设置过期时间 NX:占位 设置过期时间和加锁必须是同步的、原子的

127.0.0.1:6379> set lock wula EX 300  //NX 设置过期时间为300s,且用NX占位 
OK
127.0.0.1:6379> ttl lock  //查询过期时间
(integer) 295
127.0.0.1:6379> ttl lock
(integer) 294
127.0.0.1:6379> ttl lock
(integer) 291
127.0.0.1:6379> ttl lock
(integer) 288
127.0.0.1:6379> ttl lock
(integer) 281

分布式锁演化3

分布式锁5

分布式锁演化4

在这里插入图片描述

分布式锁演化5

使用Lua脚本

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua脚本

在这里插入图片描述

@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        //1、加入缓存逻辑
        //从Redis中获取缓存
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //判断是否为空
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、如果缓存中没有数据
            System.out.println("缓存不命中...查询数据库...");
            //查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonDb = getCatalogJsonDBWithRedisLock();

            return catalogJsonDb;
        }
        //4、转为我们指定的对象(查完数据库 或者 Redis有缓存)
        System.out.println("缓存命中...");
        // 要转换的JSON    要转换的类型 把自己想要的类型给 TypeReference 这个方法是受保护的,我们需要自己写一个静态内部类来处理
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        //5、返回转换后的结果
        return result;
    }

    /***
     *  分布式锁
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedisLock() {

        //4、设置value为随机值 根据value来删锁 每个线程都有自己唯一的uuid
        String uuid = UUID.randomUUID().toString();
//        //1、占分布式锁。去Redis占坑
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", String.valueOf(wula)); //setIfAbsent 相当于 NX参数
//        //3、设置过期时间和加锁必须是同步的、原子的
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "wula", 300, TimeUnit.SECONDS); //setIfAbsent 相当于 EX参数 和 NX参数
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); //setIfAbsent 相当于 EX参数 和 NX参数

        if (lock){
            System.out.println("获取分布式锁成功");
            //加锁成功...执行业务
            2、设置过期时间 --> 防止死锁(不是原子的)
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //获取值对比,对比成功才删除  --> 原子操作 Lua脚本
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua脚本
                //删除锁 --> 原子删锁
                Long lua = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }
//            redisTemplate.delete("lock"); // 业务成功之后,删除锁

//            //获取值对比,对比成功才删除  这两步也应该是一个原子操作
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue)){
//                //如果 Redis例存的值和设置的值一样,才进行删除
//                redisTemplate.delete("lock"); // 业务成功之后,删除锁
//            }

            return dataFromDb;
        }else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("获取分布式锁失败............");
            return getCatalogJsonDBWithRedisLock(); /* 自旋 */
        }
    }

    //抽取方法二
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //再次判断缓存是否有数据
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        //如果缓存不为空  直接 转换对象return就可以了
        if (!StringUtils.isEmpty(catalogJSON)) {
            //JSON --> 对象
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            //返回转换后的结果
            return result;
        }
        System.out.println("查询了数据库 进入锁------");

        /*
         *   【优化】
         *   1、将数据库的多次查询变为1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封装1级的数据                                                                         // k:一级分类的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

//-------------------------------二级---------------------------------------
            //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

            //2、封装2级的数据
            List<Catelog2Vo> catelog2Vos = null;

            /*2级分类的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一级分类的id
                     * catalo3List在下面
                     * l2.getCatId() 二级分类的id
                     * l2.getName()  二级分类的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装

//-------------------------------三级---------------------------------------
                    //1、找当前二级分类的三级分类封装成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                    if (level3Catelog != null) {
                        /*3及分类的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            /***
                             * item.getCatId() 2级节点的id
                             * l3.getCatId() 当前3级节点的id
                             * l3.getName()  当前3级节点的名字
                             */
                            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;
        }));

        //3、查到的数据再放入缓存,
        //因为用的是StringRedisTemplate 接受的value值是一个String 所以 catalogJsonDb 不能直接放入,需要转换一下
        //将查到的对象转为 JSON 放入缓存中
        /* 缓存中存的是 JSON 字符串,优点:JSON 有跨语言跨平台的特点 */
        String s = JSON.toJSONString(parent_cid);
        /***
         *  设置过期时间 解决缓存雪崩
         */
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);

        return parent_cid;
    }

    //抽取方法
    /*
     *   List<CategoryEntity> selectList 从这个里面查询
     *   Long parent_cid 条件
     */
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        return collect;

    }
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedisLock() {
        //设置value为随机值 根据value来删锁 每个线程都有自己唯一的uuid
        String uuid = UUID.randomUUID().toString();
        //占分布式锁。去Redis占坑	设置过期时间和加锁必须是同步的、原子的
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); //setIfAbsent 相当于 EX参数 和 NX参数

        if (lock){
            System.out.println("获取分布式锁成功");
            //加锁成功...执行业务
            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //获取值对比,对比成功才删除  --> 原子操作 Lua脚本
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; //Lua脚本
                //删除锁 --> 原子删锁
                Long lua = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("获取分布式锁失败............");
            return getCatalogJsonDBWithRedisLock(); /* 重试:自旋 */
        }
	 }

7、Redisson 分布式锁

Redisson分布式锁


8、SpringCache


简介

cache1

cache3


01、整合SpringCache

  • pom
<!--  整合SpringCache  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 配置redis使用
<!--引入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>
<!--  移除默认的 lettuce-core 核心,加入 jedis 核心  -->
<dependency>
	<groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • 配置

自动配置了哪些

CacheAutoConfiguration,会导入RedisCacheConfiguration 自动配置了缓存管理器RedisCacheManager

配置使用redis作为缓存 (在Redis配置好的前提下)

spring.cache.type=redis

常用注解:

  •  `@Cacheable` :触发将数据保存到缓存的操作
    
  •  `@CacheEvict` :触发将数据从缓存中删除的操作      --> 失效模式
    
  •  `@CachePut` :在不影响方法执行的情况下更新缓存   --> 双写模式
    
  •  `@Caching` :组合以上多个操作
    
  •  `@CacheConfig` :在类级别共享缓存的相同设置。
    

测试使用缓存:

  • 开启缓存功能,在主启动类上,标注 @EnableCaching
  • 只需要使用注解,就可以完成缓存操作
  • 业务方法的头部标上@Cacheable,表示当前方法的结果需要缓存,如果缓存中有,该方法不会调用。如果缓存中没有,就会调用该方法,最终将方法的结果放入缓存
  • 指定缓存分区。每一次需要缓存的数据,我们都需要指定要放到哪个名字的缓存【缓存的分区】通常按照业务类型进行划
  • 示例:将前台:查询一级分类 放入缓存中 分区为 category
@Cacheable({"category"}) //表示当前缓存最终放到 category 分区里了
@Override
public List<CategoryEntity> getLevel1Categorys() {
	System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntities;
}

cache5


细节

上面我们将一级分类数据的信息缓存到Redis中了,缓存到Redis中数据具有如下的特点:

  • 如果缓存中有,方法不会被调用;

  • key默认自动生成;形式为缓存的名字::SimpleKey [](自动生成的key值)

  • 缓存的value值,默认使用jdk序列化机制,将序列化后的数据缓存到redis;

  • 默认TTL时间为-1,表示永不过期

然而这些并不能够满足我们的需要,我们希望:

  1. 能够指定生成缓存所使用的key;
  2. 指定缓存的数据的存活时间;
  3. 将缓存的数据保存为json形式;

改进

针对第一点:可以使用@Cacheable注解的时候,设置key属性,接收一个SpEL 注意:加上单引号,让其成为字符串

@Cacheable(value = {"category"},key = "'level1Categorys'") 

针对第二点:在配置文件中指定TTL

#设置缓存(TTL)存活时间  单位为毫秒
spring.cache.redis.time-to-live= 3600000

清空Redis,进行测试: http://localhost:10000

在这里插入图片描述

SpEL的详细语法,在文档中给予了详细的说明:https://docs.spring.io/spring-framework/docs/5.3.0-SNAPSHOT/spring-framework-reference/integration.html#cache-spel-context

在这里插入图片描述

示例: 将方法的名字设置为key值

@Cacheable(value = {"category"},key = "#root.method.name") 

在这里插入图片描述


02、整合–自定义缓存配置


针对第三点:将缓存的数据保存为json形式

这涉及到修改缓存管理器的设置,CacheAutoConfiguration导入了RedisCacheConfiguration,而RedisCacheConfiguration中自动配置了缓存管理器RedisCacheManager,而RedisCacheManager要初始化所有的缓存,每个缓存决定使用什么样的配置,如果RedisCacheConfiguration有,就用已有的,没有就用默认配置。

想要修改缓存的配置,只需要给容器中放一个redisCacheConfiguration即可,这样就会应用到当前RedisCacheManager管理的所有缓存分区中。

  • Config
/***
 *  Cache 自定义缓存配置
 */

@EnableConfigurationProperties(CacheProperties.class) //开启配置文件的绑定功能
@Configuration
@EnableCaching
public class MyCacheConfig {

//    注入 CacheProperties 方式1
//    @Autowired
//    CacheProperties cacheProperties;

    /***
     *  原来配置文件重的东西都没用上:
     *      1、原来配置文件中的绑定的配置类是这样子的:
     *          @ConfigurationProperties(prefix="spring.cache")
     *          public class CacheProperties
     *      2、让他生效
     *          @EnableConfigurationProperties(CacheProperties.class)
     *          @return
     */
    @Bean
    RedisCacheConfiguration rcc(CacheProperties cacheProperties){ //注入 CacheProperties 方式2

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config = config.entryTtl();

        //key的序列化用什么
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        //value的序列化用什么
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        //将配置文件中的所有配置都生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

在这里插入图片描述

  • 其他properties配置
#配置使用Redis作为缓存
spring.cache.type=redis
#设置缓存(TTL)存活时间  单位为毫秒
spring.cache.redis.time-to-live= 3600000
#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀 --> 不建议使用
spring.cache.redis.key-prefix=CACHE_
#是否使用前缀功能 --> 不建议使用
spring.cache.redis.use-key-prefix=false 
#是否缓存空值 --> 防止缓存穿透问题
spring.cache.redis.cache-null-values=true

在这里插入图片描述


@CacheEvict:将数据从缓存中删除 --> 用于失效模式

修改菜单之后,删除缓存中的仅有一条原有缓存缓存,重新查询才会重新生成最新的数据

@CacheEvict(value = "category",key = "'getLevel1Categorys'") //value:指定删除的片区  key:指定删除的缓存名
@Transactional //事务
@Override
public void updateCascade(CategoryEntity category) {
	this.updateById(category); //更新自己
    //更新关联表                                 //三级分类的id        更新的名字
	categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}

cache10


1级2级3级分类数据 联合添加到缓存

另外在修改了一级缓存时,对应的二级缓存也需要更新,需要修改原来二级分类的执行逻辑。

getCatelogJson恢复成为原来的逻辑,但是设置@Cacheable,非侵入的方式将查询结果缓存到redis中:

  • 修改1级分类
    @Override
    //使用SpEL来指定value,value为该方法的名字
    @Cacheable(value = {"category"},key = "#root.method.name")
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys......");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }
  • 根据1级分类修改对应的2级、3级分类
@Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        /*
         *   【优化】
         *   1、将数据库的多次查询变为1次
         */
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //1、查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        //2、封装1级的数据                                                                         // k:一级分类的id
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//-------------------------------二级---------------------------------------
            //1、通过每一个的一级分类,查到这个一级分类的二级分类                                                              //v:当前遍历的一级分类
            List<CategoryEntity> level2Catelog = getParent_cid(selectList, v.getCatId()); //二级分类的parent_cid 【eq】 一级分类的catid

            //2、封装2级的数据
            List<Catelog2Vo> catelog2Vos = null;

            /*2级分类的集合*/
            if (level2Catelog != null) {
                catelog2Vos = level2Catelog.stream().map(l2 -> {
                    /***
                     * v.getCatId()  一级分类的id
                     * catalo3List在下面
                     * l2.getCatId() 二级分类的id
                     * l2.getName()  二级分类的名字
                     */
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());//封装
//-------------------------------三级---------------------------------------
                    //1、找当前二级分类的三级分类封装成VO
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId()); //三级分类的parent_cid 【eq】 二级分类的catid
                    if (level3Catelog != null) {
                        /*3及分类的集合*/
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            /***
                             * item.getCatId() 2级节点的id
                             * l3.getCatId() 当前3级节点的id
                             * l3.getName()  当前3级节点的名字
                             */
                            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 parent_cid;
    }

访问:wulawula.com 且重复访问时,没有重新查数据库 发现Redis里有两个缓存

在这里插入图片描述


@Caching:组合多个Cache操作

1级2级3级分类数据 联合添加到缓存 的基础上操作

修改菜单之后,删除缓存中的两条以上原有缓存数据,重新查询才会重新生成最新的数据

@Caching(evict = { //组合多个cache操作
	@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    @CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional //事务
@Override
public void updateCascade(CategoryEntity category) {
	this.updateById(category); //更新自己
    //更新关联表                                 //三级分类的id        更新的名字
    categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}

在这里插入图片描述
cache11


只使用@CacheEvict也可以删除多个缓存数据

@CacheEvict(value = "category",allEntries = true)

它表示要删除category分区下的所有数据。

注意:

可以看到存储同一类型的数据,都可以指定未同一个分区,可以批量删除这个分区下的数据。以后不建议使用分区前缀,而是使用默认的分区前缀

#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀 --> 不建议使用
#spring.cache.redis.key-prefix=CACHE_
#是否使用前缀功能 --> 不建议使用
#spring.cache.redis.use-key-prefix=false

在这里插入图片描述


03、 总结


1)读模式

  • 缓存穿透:查询一个null值。解决,缓存空数据;cache-null-value=true;

  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方法,是进行加锁,默认是没有加锁的,查询时设置Cacheable的sync=true即可解决缓存击穿。

  • 缓存雪崩:大量的key同时过期。解决方法:加上随机时间;加上过期时间。spring.cache.redis.time-to-live=3600000

2)写模式(为了保证 --> 缓存与数据一致)

  1. 读写加锁;
  2. 引入canal,感知到mysql的更新去更新数据库;
  3. 读多写少,直接去数据库查询就行;

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据):完全可以使用spring-cache;写模式,只要缓存的数据有过期时间就足够了;

  • 特殊数据:特殊设计;


9、前台检索


检索条件分析

  • 全文检索:skuTitle->keyword
  • 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
  • 过滤:hasStock(是否有货 0/1)、skuPrice(价格区间)、brandId、catalog3Id、attrs
  • 聚合:attrs

完整查询参数 keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏

PUT product
{
  "mappings": {
    "properties": {
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      },
      "autoGeneratedTimestamp": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "brandImg": {
        "type": "keyword"
      },
      "brandName": {
        "type": "keyword"
      },
      "catalogId": {
        "type": "long"
      },
      "catalogName": {
        "type": "keyword"
      },
      "description": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "hasStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "parentTask": {
        "properties": {
          "id": {
            "type": "long"
          },
          "nodeId": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "set": {
            "type": "boolean"
          }
        }
      },
      "refreshPolicy": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "retry": {
        "type": "boolean"
      },
      "saleCount": {
        "type": "long"
      },
      "shouldStoreResult": {
        "type": "boolean"
      },
      "skuId": {
        "type": "long"
      },
      "skuImg": {
        "type": "keyword"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "spuId": {
        "type": "keyword"
      }
    }
  }
}

完整查询参数 es.wulawula.com/list.html?catalog3Id=225&keyword=华为&brandId=10&brand=20&attrs=12_海思(HS)&attrs=12_骁龙&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&catalog3Id=1&at trs=1_3G:4G:5G

GET product/_search
{
  "query": {=== 检索 ===
    "bool": {
      "must": [  <-- 必须
        {
          "match": { <--全文匹配
            "skuTitle": "华为" //商品标题
          }
        }
      ],
      "filter": [ <--过滤写在filter
        {
          "term": { <-- 精确查询
            "catalogId": "225" //根据3级分类Id
          }
        },
        {
          "terms": { <-- 多个值匹配
            "brandId": [ //根据品牌Id
              "10",
              "20"
            ]
          }
        },
        {
          "nested": { < ** 嵌入式查询
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {<-- 精确查询
                      "attrs.attrId": { //根据属性Id
                        "value": "12"
                      }
                    }
                  },
                  {
                    "terms": { <-- 多个精确查询
                      "attrs.attrValue": [ //根据属性的值
                        "海思(HS)",
                        "骁龙"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": { //根据是否有库存
              "value": true
            }
          }
        },
        {
          "range": { <--区间检索
            "skuPrice": { //根据价格区间
              "gte": 0, // >=0
              "lte": 6000 // <=6000
            }
          }
        }
      ]
    }
  },
  "sort": [=== 排序 ===
    {
      "skuPrice": { //根据价格降序
        "order": "desc"
      }
    }
  ],
  "from": 0, <--分页  从几开始
  "size": 4, <--分页  查询几个
  "highlight": {===  高亮 ===
    "fields": {"skuTitle": {}}, //指定哪个属性高亮 
    "pre_tags": "<b style='color:red'>", //前置标签
    "post_tags": "</b>" //后置标签
  },
  "aggs": {=== 聚合 ===
    "brand_agg": { <---聚合1
      "terms": {
        "field": "brandId", //根据品牌Id聚合
        "size": 50
      },
      "aggs": { <-- 子聚合
        "brand_name_agg": {
          "terms": {
            "field": "brandName", //根据品牌Id得到 品牌名称聚合
            "size": 1
          }
        },
        "brand_img_agg": {
          "terms": {
            "field": "brandImg", //根据品牌Id得到 品牌图片聚合
            "size": 1
          }
        }
      }
    },
    "catalog_agg": { <---聚合2
      "terms": {
        "field": "catalogId", //根据分类Id聚合
        "size": 20
      },
      "aggs": { <-- 子聚合
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName", //根据分类Id得到 分类名称聚合
            "size": 1
          }
        }
      }
    },
    "attr_agg": { <---聚合3
      "nested": { <** 嵌入式聚合
        "path": "attrs" < ** 需要先声明是嵌入式聚合
      },
      "aggs": { <-- 子聚合
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId", //根据属性得到 属性Id聚合
            "size": 10
          },
          "aggs": { <-- 子聚合
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName", //根据属性Id得到 属性名称聚合
                "size": 1
              }
            },
            "attr_value_agg": {
              "terms": {
                "field": "attrs.attrValue", //根据属性Id得到 属性值聚合
                "size": 50
              }
            }
          }
        }
      }
    }
  }
}

10、页面渲染


01、基本效果渲染


<!--排序内容 商品每四个是一组-->
<div class="rig_tab">
    <!-- 遍历每个商品-->
    <div th:each="product:${result.getProducts()}">
        <div class="ico">
            <i class="iconfont icon-weiguanzhu"></i>
            <a href="/static/es/#">关注</a>
        </div>
        <p class="da">
            <!--图片 -->
            <a href="/static/es/#">
                <img th:src="${product.skuImg}" class="dim">
            </a>
        </p>
        <ul class="tab_im">
            <li><a href="/static/es/#" title="黑色">
                <img th:src="${product.skuImg}"></a>
            </li>
        </ul>
        <p class="tab_R">
            <!-- 价格 -->
            <span th:text="''+${product.skuPrice}">¥5199.00</span>
        </p>
        <p class="tab_JE">
            <!-- 标题 -->
            <!-- 使用utext标签,使检索时高亮不会被转义-->
            <a href="/static/es/#" th:utext="${product.skuTitle}">
                Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
            </a>
        </p>
        <p class="tab_PI">已有<span>11万+</span>热门评价
            <a href="/static/es/#">二手有售</a>
        </p>
        <p class="tab_CP"><a href="/static/es/#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
            <a href='#' title="联系供应商进行咨询">
                <img src="/static/es/img/xcxc.png">
            </a>
        </p>
        <div class="tab_FO">
            <div class="FO_one">
                <p>自营
                    <span>谷粒商城自营,品质保证</span>
                </p>
                <p>满赠
                    <span>该商品参加满赠活动</span>
                </p>
            </div>
        </div>
    </div>
</div>

02、筛选条件渲染


将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接url进行跳转

  • html
<div class="JD_selector">
            <!--手机商品筛选-->
            <div class="title">
                <h3><b>手机</b><em>商品筛选</em></h3>
                <div class="st-ext">&nbsp;<span>10135</span>个商品</div>
            </div>
            <div class="JD_nav_logo">
                <!--品牌-->
                <div class="JD_nav_wrap">
                    <div class="sl_key">
                        <span><b>品牌:</b></span>
                    </div>
                    <div class="sl_value">
                        <div class="sl_value_logo">
                            <ul>
                                <li th:each="brand:${result.brands}">
                                    <!--拼接URL-->
                                    <a href="/static/es/#" th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}">
                                        <img th:src="${brand.brandImg}" alt="">
                                        <div th:text="${brand.brandName}">
                                            华为(HUAWEI)
                                        </div>
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </div>
                    <div class="sl_ext">
                        <a href="/static/es/#">
                            更多
                            <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
                            <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
                        </a>
                        <a href="/static/es/#">
                            多选
                            <i>+</i>
                            <span>+</span>
                        </a>
                    </div>
                </div>

                <!--分类-->
                <div class="JD_pre">
                    <div class="sl_key">
                        <span><b>分类:</b></span>
                    </div>
                    <div class="sl_value">
                        <ul>
                            <li th:each="catalog:${result.catalogs}">
                              <!--拼接URL-->  
                              <a href="/static/es/#"
                                   		th:href="${'javascript:searchProducts(&quot;catalog3Id&quot;,'+catalog.catalogId+')'}"
                                   th:text="${catalog.catalogName}">分类名称</a>
                            </li>
                        </ul>
                    </div>
                    <div class="sl_ext">
                        <a href="/static/es/#">
                            更多
                            <i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
                            <b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
                        </a>
                        <a href="/static/es/#">
                            多选
                            <i>+</i>
                            <span>+</span>
                        </a>
                    </div>
                </div>


                <!--其他的所有需要展示的属性-->
                <div class="JD_pre" th:each="attr:${result.attrs}">
                    <div class="sl_key">
                        <span th:text="${attr.attrName}">其他属性:</span>
                    </div>
                    <div class="sl_value">
                        <ul>
                            <!--此处的value也是一个list,也需要遍历-->
                            <li th:each="val:${attr.attrValue}">
                            <!--拼接URL-->
                            <a href="/static/es/#"
                                                                   th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
                                                                   th:text="${val}">其他属性</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
            <div class="JD_show">
                <a href="/static/es/#">
                    <span>
                        更多选项( CPU核数、网络、机身颜色 等)
                    </span>
                </a>
            </div>
        </div>
  • js
//按照属性筛选
function searchProducts(name, value) {
	//原来的页面
	location.href = replaceParamVal(location.href,name,value,true);
}

搜索页面1


03、搜索栏


  • html
<!--搜索导航-->
<div class="header_sous">
    <div class="logo">
        <a href="http://wulawula.com"><img src="/static/es/./image/logo1.jpg" alt=""></a>
    </div>
    <div class="header_form">
        <input id="keyword_input" type="text" placeholder="手机"/>
        <a href="javascript:searchByKeyword();">搜索</a>
    </div>
</div>
  • js
//搜索栏
function searchByKeyword() {
	searchProducts("keyword",$("#keyword_input").val())
}

搜索页面2


04、分页


  • html
<!--分页-->
<div class="filter_page">
    <div class="page_wrap">
        <span class="page_span1">
            <!--上一页-->
            <!--th:attr="pn=${result.pageNum - 1}" 自定义属性:当前页+1 -->
            <a class="page_a"
               href="#"
               th:attr="pn=${result.pageNum - 1}"
               th:if="${result.pageNum >1}"><!-- 当前页>1 -->
                《 上一页
            </a>
            <!--当前页-->
            <a class="page_a"
               href="#"
               th:attr="pn=${nav},style=${nav == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
               th:each="nav:${result.pageNavs}">[[${nav}]]</a>
            <!--下一页-->
            <a class="page_a"
               href="#"
               th:attr="pn=${result.pageNum + 1}"
               th:if="${result.pageNum < result.totalPages}"><!-- 当前页>总页码 -->
                下一页 》
            </a>
        </span>
        <span class="page_span2">
            <em><b>[[${result.totalPages}]]</b>&nbsp;&nbsp;到第</em>
            <input type="number" value="1">
            <em></em>
            <a class="page_submit">确定</a>
        </span>
    </div>
</div>
  • js
//分页 1
$(".page_a").click(function () {
    var pn=$(this).attr("pn");
    location.href=replaceParamVal(location.href,"pageNum",pn,false);
    console.log(replaceParamVal(location.href,"pageNum",pn,false))
})
//分页2
/**
 * @param url 目前的url
 * @param paramName 需要替换的参数属性名
 * @param replaceVal 需要替换的参数的新属性值
 * @param forceAdd 该参数是否可以重复查询(attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏)
 * @returns {string} 替换或添加后的url
 */
function replaceParamVal(url, paramName, replaceVal, forceAdd) {
    var oUrl = url.toString();
    var nUrl;
    if (oUrl.indexOf(paramName) != -1) {
        if (forceAdd && oUrl.indexOf(paramName + "=" + replaceVal) == -1) {
            if (oUrl.indexOf("?") != -1) {
                nUrl = oUrl + "&" + paramName + "=" + replaceVal;
            } else {
                nUrl = oUrl + "?" + paramName + "=" + replaceVal;
            }
        } else {
            var re = eval('/(' + paramName + '=)([^&]*)/gi');
            nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
        }
    } else {
        if (oUrl.indexOf("?") != -1) {
            nUrl = oUrl + "&" + paramName + "=" + replaceVal;
        } else {
            nUrl = oUrl + "?" + paramName + "=" + replaceVal;
        }
    }
    return nUrl;
};

05、排序


  • html
<!--综合排序-->
<div class="filter_top">
    <div class="filter_top_left" th:with="p = ${param.sort}"> <!--不能直接用 param.sort去判断,所以用p来做中间替换,将param.sort 赋值给p,p当作text类型来用-->
        <!--判断当param.sort不为空,并且是以自己的param.sort开始的,并且是以desc结尾的 然后3元运算 更改值-->
        <!--判断param.sort是否为空,或者是否是以自己的param.sort开始的 然后3元运算拼接高亮样式和默认样式-->
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="hotScore" href="/static/es/#">综合排序 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a><!--判断3元运算,是否需要加上↓ 和 ↑-->
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'saleCount')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="saleCount" href="/static/es/#">销量 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
        <a  th:class="${(! #strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc':'sort_a'}"
            th:attr="style = ${#strings.isEmpty(p) || #strings.startsWith(p,'skuPrice')} ? 'color: #FFF;border-color:#e4393c;background: #e4393c' : 'color: #333;border-color:#ccc;background: #FFF'"
            sort="skuPrice" href="/static/es/#">价格 [[${(! #strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
        <a href="/static/es/#">评论分</a>
        <a href="/static/es/#">上架时间</a>
    </div>
    <div class="filter_top_right">
                            <span class="fp-text">
                               <b>1</b><em>/</em><i>169</i>
                           </span>
        <a href="/static/es/#" class="prev"></a>
        <a href="/static/es/#" class="next"></a>
    </div>
</div>

  • js
$(".sort_a").click(function () {
    //调用设置好的样式
    // changStyle(this); //传入 this:当前元素  方法用ele接收了这个this
    //跳转到指定位置
    $(this).toggleClass("desc"); //被点击自动加上 desc的样式 再次点击就会取消
    var sort = $(this).attr("sort"); //得到当前点击元素的sort里的值
    console.log("sort = "+sort)
    sort = $(this).hasClass("desc") ? sort+"_desc":sort+"_asc"; //判断是否包含desc 是否进行拼串
    location.href = replaceParamVal(location.href,"sort",sort)
    //禁用默认行为,禁止绑定标签的href跳转等
    return false;
});

  • 样式参考
function changStyle(ele) {
    /***
     *  1、改变当前元素和兄弟元素的样式(当前被点击的元素变为选中状态)
     */
    //1.清空之前元素的样式
    //默认样式 color: #333;border-color:#ccc;background: #FFF
    $(".sort_a").css({"color":"#333","border-color":"#ccc","background":"#FFF"})
    //2.改变当前被点击的元素变成被选中状态
    //高亮样式 color: #FFF;border-color:#e4393c;background: #e4393c
    $(ele).css({"color":"#FFF","border-color":"#e4393c","background":"#e4393c"}) //( ${this}就是当前元素 )
    //3.去掉兄弟元素的 ↑ ↓ 符号
    $(".sort_a").each(function () { //使用each遍历兄弟元素
        var text = $(this).text().replace("↓","").replace("↑",""); //去掉全部的 ↑ ↓
        $(ele).text(text); //设置进原始文本内容(不带拼接的)
    });
    /***
     *  2、改变升序降序
     */
    //被点击自动加上 desc的样式 再次点击就会取消
    $(ele).toggleClass("desc"); //加上就是降序,不加就是升序
    if ($(ele).hasClass("desc")){ //检查被选元素是否包含指定的class内容
        //包含 -> 降序
        var text = $(ele).text().replace("↓","").replace("↑",""); //得到当前的文本值 例如:综合排序。然后将 ↓ ↑ 两个符号的文本清空
        console.log("text1" + text)
        text = text+"↓"; //在文本值的基础上拼接 ↓
        console.log("text2" + text)
        $(ele).text(text) //将拼接好的文本值 设置到当前元素的文本上用于显示
    }else {
        //不包含 -> 升序
        var text = $(ele).text().replace("↓","").replace("↑","");
        text = text+"↑";
        $(ele).text(text)
    }
}

06、价格区间


  • html
<!--不能直接用 param.sort去判断,所以用p来做中间替换,将param.sort 赋值给p,p当作text类型来用。priceRange也是替换param.skuPrics的值-->
<div class="filter_top_left" th:with="p = ${param.sort},priceRange=${param.skuPrice}"> 
    <!--上架时间-->
    <a href="/static/es/#">上架时间</a>
    <!--价格区间-->
    <!-- th:value="${#strings.isEmpty(priceRange)}" 用于input框的回显。且把param.skuPrics的值赋值给pricsRange 并且判断是否为空 -->
    <!-- #strings.substringBefore(priceRange,'_') 截取出指定字符串的 _ 前面的内容  substringAfter为截取指定字符串的 _ 后面的内容-->
    <input id="skuPriceFrom" type="number" style="width: 100px; margin-left: 20px;"
           th:value="${#strings.isEmpty(priceRange) ? '' :  #strings.substringBefore(priceRange,'_')}"> -
    <input id="skuPriceTo" type="number" style="width: 100px;"
           th:value="${#strings.isEmpty(priceRange) ? '' :  #strings.substringAfter(priceRange,'_')}">
    <button id="skuPriceSearchBth">确定</button>
</div>
  • js
$("#skuPriceSearchBth").click(function () {
	//1、拼接上价格区间的查询条件
    var from = $("#skuPriceFrom").val(); //价格开始
    var to = $("#skuPriceTo").val(); //价格结束
    var query = from + "_" + to; //拼接字符串
    location.href = replaceParamVal(location.href,"skuPrice",query); //拼接字符串后调用方法替换url
});

07、仅显示有货


  • html
<li>
    <a href="#" th:with="check = ${param.hasStock}">
        <input id="showHasStock" type="checkbox"
               th:checked="${#strings.equals(param.hasStock,'1')}"><!--使用equals()判断param.hasStock里是否是1-->
        仅显示有货
    </a>
</li>
  • js
//仅显示有货 -> 选择框
$("#showHasStock").change(function () {
    //prop可以获取调用此方法的checked类型的值为true或false(checked返回true或false)
    if ($(this).prop('checked')) {
        //true -> 有库存
        location.href = replaceParamVal(location.href, "hasStock", 1);
    } else {
        //false -> 没选中 (有库存+无库存)
        var re = eval('/(hasStock=)([^&]*)/gi'); //正则表达式匹配hasStock的值
        location.href = (location.href + "").replace(re, ''); //将hasStock替换成空串
    }
    return false;
});

08、面包屑导航 + 条件筛选联动


  • EsResult
/***
*  面包屑导航
*/
private List<NavVo> navs = new ArrayList<>();

@Data
public static class NavVo{
	private String navName; //导航的名字
    private String navValue; //导航的值
    private String link; //取消一个导航后要跳转的位置
}

/***
*  判断哪些属性被筛选了。页面就不需要展示了,点击x才显示
*/
private List<Long> attrIds = new ArrayList<>();
  • Impl
/***   8、构建面包屑导航功能   */
//属性
if (param.getAttrs() != null && param.getAttrs().size()>0){
    List<EsResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
        //1、分析出每个attr传过来的查询参数值
        EsResult.NavVo navVo = new EsResult.NavVo();
        //示例:attrs=2_5寸:6寸
        String[] s = attr.split("_"); //分割
        navVo.setNavValue(s[1]); //值
        // id需要查询出对应的名字
        //远程调用
        R r = productFeignService.attrInfo(Long.parseLong(s[0]));
        result.getAttrIds().add(Long.parseLong(s[0]));
        if (r.getCode() == 0){ //正常返回
            AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
            });
            navVo.setNavName(data.getAttrName());
        }else {
            navVo.setNavName(s[0]);
        }

        //2、取消了面包屑以后,跳转到哪个地方。将请求地址的URL里面的当前条件置空
        //拿到所有的查询条件,去掉当前
        String replace = replaceQueryString(param, attr,"attrs");
        navVo.setLink("http://es.wulawula.com/list.html?" + replace);
        return navVo;
    }).collect(Collectors.toList());
    result.setNavs(collect);
}

//品牌、分类
if (param.getBrandId() != null && param.getBrandId().size()>0){
    List<EsResult.NavVo> navs = result.getNavs();
    EsResult.NavVo navVo = new EsResult.NavVo();
    navVo.setNavName("品牌");
    //远程调用
    R r = productFeignService.brandsInfo(param.getBrandId());
    if (r.getCode() == 0) {
        List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
        });
        StringBuffer buffer = new StringBuffer();
        String replace = "";
        for (BrandVo brandVo:brand){
            buffer.append(brandVo.getName());
            replace = replaceQueryString(param, brandVo.getBrandId()+"","brandId");
        }
        navVo.setNavValue(buffer.toString());
        navVo.setLink("http://es.wulawula.com/list.html?" + replace);
    }
    navs.add(navVo);
}

  • R代码
public <T> T getData(String key,TypeReference<T> typeReference){
	Object data = get(key);
	String s = JSON.toJSONString(data); //对象 --> JSON字符串
	//转换对象,可以将字符串类型的对象转换成我们指定类型的对象
	T t = JSON.parseObject(s, typeReference); //JSON字符串 --> 相应的对象
	return t;
}
@Data
public class BrandVo {
    private Long brandId; //品牌id
    private String name; //品牌名字
}
  • feign
@FeignClient("gulimall-product")
public interface ProductFeignService {

    //查询属性的信息
    @GetMapping("/product/attr/info/{attrId}")
    public R attrInfo(@PathVariable("attrId") Long attrId);

    //返回所有的品牌数据
    @GetMapping("/product/brand/infos")
    public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
    
}
  • html
<!--遍历面包屑功能-->
<div class="JD_ipone_one c">
    <!--获取点击x后跳转的地址-->
    <a th:href="${nav.link}"
       th:each="nav:${result.navs}">
        <!--获取跳转的属性名和值-->
        <span th:text="${nav.navName}"></span>
        <span th:text="${nav.navValue}"></span> x </a>
</div>
  • html
  • 使用 #lists.contains() 方法获取list中的返回的值
<!--其他的所有需要展示的属性-->
<div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
    <div class="sl_key">
        <span th:text="${attr.attrName}">属性名字</span>
    </div>
    <div class="sl_value">
        <ul>
            <!--此处的value也是一个list,也需要遍历-->
            <li th:each="val:${attr.attrValue}"><a
                                                   th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
                                                   th:text="${val}">显示所有属性值</a>
            </li>
        </ul>
    </div>
</div>


11、异步

异步 & CompletableFuture异步编排


12、CompletableFuture异步编排

异步 & CompletableFuture异步编排


13、商品详情


01、初步


1、修改hosts的http规则

# gulimall
192.168.56.10 wulawula.com
192.168.56.10 es.wulawula.com
192.168.56.10 item.wulawula.com

2、修改 Nginx 配置文件

/mydata/nginx/conf/conf.d/mydata/nginx/conf/conf.d

商品详情1

3、配置 GateWay

### 搜索页面
        - id: gulimall_es_route
          # lb 代表的就是负载均衡
          uri: lb://gulimall-es
          predicates:
            - Host=es.wulawula.com

4、修改商品列表的跳转路径,点击商品图片即可跳转到商品详情页

<!--图片 -->
<a th:href="|http://item.wulawula.com/${product.skuId}.html|">
	<img th:src="${product.skuImg}" class="dim">
</a>

使用 |${}| 的方式动态拼接url路径:|http://item.wulawula.com/${product.skuId}.html|

5、封装数据Vo

  • 总封装vo
/***
 *  封装前台商品详情页信息
 */
@Data
public class SkuItemVo {

    //1、sku基本信息获取   pms_sku_info
    SkuInfoEntity info;

    //2、sku的图片信息    pms_sku_images
    List<SkuImagesEntity> images;

    //3、获取spu的销售属性组合 -> 组合有多种 -> 将单个的销售属性组合起来
    List<SkuItemSaleAttrVo> saleAttr;

    //4、获取spu的介绍    pms_spu_info_desc
    SpuInfoDescEntity desc;

    //5、获取spu的规格参数信息
    SpuItemAttrGroup groupAttrs;
}
  • 下属vo 2.1
//2.1、获取spu的销售属性(单个)
@Data
public class SkuItemSaleAttrVo{
    private Long skuId; //id
    private String attrName; //名称
    private List<String> attrValues; //值
}
  • 下属vo 5.1
//5.1、获取spu的基本属性分组(分组下有多个属性)
@ToString
@Data
public class SpuItemAttrGroupVo{
    private String groupName; //分组的名字
    private List<SpuBaseAttrVo> attrs; //分组下对应的属性
}
  • 下属vo 5.1.1
//5.1.1、spu的基本属性(单个)
@ToString
@Data
public class SpuBaseAttrVo{
    private String attrName; //属性的名字
    private String attrValue; //属性的值
}
  • controller
@Controller
public class ItemController {
    @Autowired
    SkuInfoService skuInfoService;
    /***
     * 展示当前sku的详情
     */
    @GetMapping("/{skuId}.html")
    public String skuItem(@PathVariable Long skuId, Model model){
        System.out.println("准备查询" + skuId + "详情");
        //前台:查询出sku的详情内容,用于展示
        SkuItemVo vo = skuInfoService.item(skuId);
        model.addAttribute("item",vo);
        return "item";
    }
}
  • SkuInfoServiceImpl
    //前台:查询出sku的详情内容,用于展示
    @Override
    public SkuItemVo item(Long skuId) {

        SkuItemVo skuItemVo = new SkuItemVo();

        //1、sku基本信息获取    pms_sku_info
        SkuInfoEntity info = getById(skuId);
        skuItemVo.setInfo(info);

        Long catalogId = info.getCatalogId();
        Long spuId = info.getSpuId();

        //2、sku的图片信息      pms_sku_images
        //前台:通过skuid查询sku的图片信息
        List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);

        //3、获取spu的销售属性组合 -> 组合有多种
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
        skuItemVo.setSaleAttr(saleAttrVos);

        //4、获取spu的介绍 pms_spu_info_desc
        SpuInfoDescEntity desc = spuInfoDescService.getById(spuId);
        skuItemVo.setDesc(desc);

        //5、获取spu的规格参数信息 -> 组合
        //获取spu的销售属性(单个)
        //通过spuid查询出属性分组
        List<SpuItemAttrGroupVo>attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        skuItemVo.setGroupAttrs(attrGroupVos);

        return skuItemVo;
    }

针对 3、获取spu的销售属性组合 -> 组合有多种 写sql

  • SkuSaleAttrValueServiceImpl
    @Override
    public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {
        SkuSaleAttrValueDao dao = this.baseMapper;
        //sql
        List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
        return saleAttrVos;
    }
  • SkuSaleAttrValueDao
    //3、获取spu的销售属性组合 -> 组合有多种
    List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);
  • SkuSaleAttrValueDao.xml
<select id="getSaleAttrsBySpuId" resultType="com.wulawula.gulimall.product.vo.web.SkuItemSaleAttrVo">
SELECT
	ssav.attr_id attr_id,
	ssav.attr_name attr_name,
	GROUP_CONCAT(DISTINCT ssav.attr_value) attrValues
FROM 
	pms_sku_info info
LEFT JOIN 
	pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE 
	info.spu_id=#{spuId}
GROUP BY 
	ssav.attr_id,ssav.attr_name
	
</select>

针对 5、获取spu的规格参数信息 -> 组合 写sql

    @Override
    public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {

        //1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
        AttrGroupDao baseMapper = this.getBaseMapper();
        //sql
        List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        return vos;
    }

  • AttrGroupDao
    List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
  • AttrGroupDao.xml
    <!-- resultType:返回集合里面元素的类型,只要有嵌套属性就要封装自定义结果集 -->
    <resultMap id="spuItemAttrGroupVo" type="com.wulawula.gulimall.product.vo.web.SpuItemAttrGroupVo">
        <result property="groupName" column="attr_group_name"/>
        <collection property="attrs" ofType="com.wulawula.gulimall.product.vo.web.SpuBaseAttrVo">
            <result property="attrName" column="attr_name"/>
            <result property="attrValue" column="attr_value"/>
        </collection>
    </resultMap>

    <select id="getAttrGroupWithAttrsBySpuId"resultMap="spuItemAttrGroupVo">

        SELECT pav.spu_id, ag.attr_group_name, ag.attr_group_id, aar.attr_id, attr.attr_name, pav.attr_value
        FROM pms_attr_group ag
        LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
		LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
        LEFT JOIN pms_product_attr_value pav ON pav.attr_id = attr.attr_id
        WHERE  ag.catelog_id = #{catalogId} AND pav.spu_id = #{spuId}
    </select>

商品详情2


02、sku组合切换


  • 修改 SkuItemSaleAttrVo
//2.1、获取spu的销售属性(单个)
@ToString
@Data
public class SkuItemSaleAttrVo{
    private Long attrId; //id
    private String attrName; //名称
    private List<AttrValueWithSkuIdVo> attrValues; //值(修改为有多种)
}
  • 新增 AttrValueWithSkuIdVo
//2.1.1、组合多种值
@Data
public class AttrValueWithSkuIdVo {
    private String attrValue;
    private String skuIds;
}
  • 修改 SkuSaleAttrValueDao.xml
    <resultMap id="skuItemSaleAttrVo" type="com.wulawula.gulimall.product.vo.web.SkuItemSaleAttrVo">
        <result property="attrId" column="attr_id"/>
        <result property="attrName" column="attr_name"/>
        <collection property="attrValues" ofType="com.wulawula.gulimall.product.vo.web.AttrValueWithSkuIdVo">
            <result property="attrValue" column="attr_value"/>
            <result property="skuIds" column="sku_ids"/>
        </collection>
    </resultMap>

    <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
        SELECT
            ssav.attr_id attr_id,
            ssav.attr_name attr_name,
            ssav.attr_value attr_value,
            GROUP_CONCAT(DISTINCT info.sku_id) sku_ids
        FROM
            pms_sku_info info
        LEFT JOIN
            pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
        WHERE
            info.spu_id = #{spuId}
        GROUP BY
            ssav.attr_id,ssav.attr_name, ssav.attr_value
    </select>

商品详情3

  • html
<div class="box-attr-3">
					<div class="box-attr clear" th:each="attr:${item.saleAttr}">
						<dl>
							<!--遍历属性-->
							<dt>选择[[${attr.attrName}]]</dt>
							<!--遍历下面的数组用逗号分隔开  val就是每一个值-->
							<dd th:each="vals:${attr.attrValues}">
								<!--用逗号把vals.skuIds分隔开。再判断这俩门面是否包含item.info.skuId-->
								<!--然后自定义方法判断是否包含?包含的话加上checked   示例:商品为:黑色8GB+128GB的手机 查看它的属性。只有黑色和8GB+128GB两个属性会有checked-->
								<a
									th:attr="skus=${vals.skuIds} , class=${#strings.contains(#strings.listSplit(vals.skuIds,','),item.info.skuId.toString()) ? 'sku_attr_value checked' : 'sku_attr_value'}" >
									[[${vals.attrValue}]]
<!--											<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
								</a>
							</dd>
						</dl>
					</div>
				</div>
  • js1
$(".sku_attr_value").click(function () {
		var skus = new Array();
		//1、点击的元素加上clicked自定义属性值,代表这个是我们刚刚点击的
		$(this).addClass("clicked");
		//2、获得当前元素的sku集合 字符串 并用逗号分隔开转为数组
		var curr = $(this).attr("skus").split(",");
		skus.push(curr); //当前被点击的所有sku组合的数组放到集合里面
		//3、去掉原来的checked属性,在当前标签用parent()往上找两级父标签,再用find()去找后代标签,用removeClass()移除指定的属性
		$(this).parent().parent().find(".sku_attr_value").removeClass("checked")
		//其他属性也遍历放进集合里面
		$("a[class='sku_attr_value checked']").each(function () {
			skus.push($(this).attr("skus").split(","));
		});
		console.log(skus);
		//2、取出它们的交集,得到skuId
		// console.log($(skus[0]).filter(skus[1])[0]);
		var filterEle = skus[0];
		for (var i=1;i<skus.length;i++){
			filterEle = $(filterEle).filter(skus[1])
		}
		//跳转到指定的颜色界面
		location.href = "http://item.wulawula.com/"+ filterEle[0] +".html";
		console.log(filterEle[0]);
	});
  • js2
//判断是否选中标签,去改变它的css样式
	$(function () {
		//因为方法加再了dd标签 用parent()方法去找它的父标签
		$("a[class='sku_attr_value']").parent().css({"border":"solid 1px #CCC"}) //先清除
		$("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"}) //再加色
		// $(".sku_attr_value.checked").parent().css({"border":"solid 1px red"})
		// $(".sku_attr_value").parent().css({"border":"solid 1px #CCC"})
	})

03、异步编排优化


1、自定义线程池

  • config
//如果指定的 ThreadPoolConfigProperties类 没有用Component加到容器中,那么我们要在需要此配置的类里开启属性配置,
//来使用此class的配置内容。示例:都不加的话,此类的方法上不能传入 ThreadPoolConfigProperties类
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class) //开启属性配置
@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor executor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(pool.getCoreSize(), /*核心线程大小*/
                pool.getMaxSize(), /*最大线程大小*/
                pool.getKeepAliveTime(), /*空闲线程多久关闭*/
                TimeUnit.SECONDS, /*时间单位 -> 秒*/
                new LinkedBlockingQueue<>(100000), /*阻塞队列长度*/
                Executors.defaultThreadFactory(), /*线程工厂 -> 此处使用默认的*/
                new ThreadPoolExecutor.AbortPolicy() /*拒绝策略 -> 抛弃*/

        );
    }
}

2、设置 线程池属性配置类 让其可以在yml里作为可配置的

  • ConfigProperties
@ConfigurationProperties(prefix = "wulawula.thread" ) //生成配置元数据,可在yml里面配置
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

3、在yml里设置 配置类 的参数

  • yml
wulawula:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

4、修改程序 -> 异步编排

在这次查询中任务2、3、4、5、6都需要spuId,因此需要等待任务1执行完毕,得到任务1的执行结果。因此任务1采用supplyAsync,需要其有返回值。任务2、3、4、5、6调用thenAcceptAsync()可以接受上一步的结果且没有返回值。任务1和任务7谁也不依赖谁,平级的。都需要传入一个skuId,因此可以创建两个异步对象。 记得注入线程池

最后,调用 allOf().get() 方法使得所有方法都已经执行完成且是按照顺序执行

//注入线程池
    @Autowired
    ThreadPoolExecutor executor;


//前台:查询出sku的详情内容,用于展示
    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {

        SkuItemVo skuItemVo = new SkuItemVo();

        /***
         * 异步编排
         */
        /* 1.创建异步任务 -> 有返回值 */
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1、sku基本信息获取    pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        /* 2.接收上一步结果,并消费处理该结果,无返回值 */
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(result -> {
            //3、获取spu的销售属性组合 -> 组合有多种
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(result.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);

        /* 3.接收上一步结果,并消费处理该结果,无返回值 */
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(result -> {
            //4、获取spu的介绍 pms_spu_info_desc
            SpuInfoDescEntity desc = spuInfoDescService.getById(result.getSpuId());
            skuItemVo.setDesc(desc);
        }, executor);

        /* 4.接收上一步结果,并消费处理该结果,无返回值 */
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(result -> {
            //5、获取spu的规格参数信息 -> 组合
            //获取spu的销售属性(单个)
            //通过spuid查询出属性分组
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(result.getSpuId(), result.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        /* 5.接收上一步结果,并消费处理该结果,无返回值 */
        CompletableFuture<Void> spuInfoFuture = infoFuture.thenAcceptAsync(result -> {
            //商品描述
            SpuInfoEntity spuInfo = spuInfoService.getById(result.getSpuId());
            skuItemVo.setSpuInfo(spuInfo);
        }, executor);

        /* 6.接收上一步结果,并消费处理该结果,无返回值 */
        CompletableFuture<Void> brandFuture = infoFuture.thenAcceptAsync(result -> {
            //品牌名
            Long brandId = result.getBrandId();
            BrandEntity byId = brandService.getById(brandId);
            skuItemVo.setBrand(byId);
        }, executor);

        /* 7.新开一个异步任务,无返回值 */
        CompletableFuture<Void> imagesFuture = CompletableFuture.runAsync(() -> {
            //2、sku的图片信息      pms_sku_images
            //前台:通过skuid查询sku的图片信息
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        /* 8.等待所有vo都完成 */ CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,spuInfoFuture,brandFuture,imagesFuture).get();

        return skuItemVo;
    }

14、登录 注册


1、修改hosts的http规则

# gulimall
192.168.56.10 wulawula.com
192.168.56.10 es.wulawula.com
192.168.56.10 item.wulawula.com
192.168.56.10 auth.wulawula.com

2、修改 Nginx 配置文件

/mydata/nginx/conf/conf.d/mydata/nginx/conf/conf.d

商品详情1

3、配置 GateWay

### 登陆页面注册页面
        - id: gulimall_auth_route
          # lb 代表的就是负载均衡
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.wulawula.com

4、


01、倒计时效果


1、给a标签绑定一个sendCode

<a id="sendCode">发送验证码</a>

2、自定义的倒计时方法

//发送验证码。倒计时
$(function f() {
	$("#sendCode").click(function () {
		//1、倒计时效果
		//判断class里是否有disavled。如果有的话代表正在倒计时。不能重复点击自定义的倒计时方法
		if ($(this).hasClass("disavled")){
			//正在倒计时
		}else {
			//2、给指定手机号发送验证码
			timeoutChangeStyle();
		}
	})
})

//自定义的倒计时方法
var num = 60; //设置初始时间
function timeoutChangeStyle() {
	$("#sendCode").attr("class","disavled"); //给a标签的class设置一个class值,防止点击完后还可以重复点击
	if (num == 0){
		//发送验证码倒计时完成,重新发送
		$("#sendCode").text("发送验证码"); //重新修改文本内容
		num = 60;
		$("#sendCode").attr("class",""); //此时移除class的值,让其可以再次点击
	}else {
		//验证码倒计时
		var str = num+"s 后再次发送";
		$("#sendCode").text(str); //文本内容
		setTimeout("timeoutChangeStyle()",1000); //计时器方法。不断的调用此大方法,时间间隔为1s
		num --;
	}
}

在这里插入图片描述


02、视图映射跳转


  • controller (以前的方法。直接在controller里跳转页面)
@Controller
public class LoginController {
    //跳转登录页
    @GetMapping("/login.html")
    public String loginPage(){
        return "login";
    }
    //跳转注册页
    @GetMapping("/reg.html")
    public String regPage(){
        return "reg";
    }
}

发送一个请求直接跳转到一个页面,此请求不传递数据。只是单纯的跳转方法就可以使用视图映射

SpringMVC viewcontroller:将请求和页面映射过来

  • config (视图映射 实现WebMvcConfigurer接口 参数可以参考之前的controller方法)
@Configuration
public class GulimallConfig implements WebMvcConfigurer {
    /***
     *  视图映射
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    /*  addViewController:添加视图配置器  参数1:url路径       参数2:视图名 */
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

03、整合阿里短信服务


第三方调用模块

  • 定义方法类 短信服务的具体方法
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data //为这些方法生成getter,setter
@Component
public class SmsComponent {

    //aliyuncs的参数
    private String accessKeyID;
    private String accessKeySecret;
    private String signName;//您的申请签名
    private String templateCode; //你的模板

    //param里面包含验证码,phone里面是手机号
    public void sendSmsCode(String phone, String param) {
        //判断手机号是否为空
        if (StringUtils.isEmpty(phone)) {
            System.out.println("手机号为空");
        }else {
            DefaultProfile profile = DefaultProfile.getProfile("default", accessKeyID, accessKeySecret);
            IAcsClient client = new DefaultAcsClient(profile);

            //设置相关固定的参数
            CommonRequest request = new CommonRequest();
            request.setMethod(MethodType.POST); //提交方式
            request.setDomain("dysmsapi.aliyuncs.com");
            request.setVersion("2017-05-25");
            request.setAction("SendSms");

            //设置发送相关的参数
            request.putQueryParameter("PhoneNumbers", phone);   //手机号
            request.putQueryParameter("SignName", signName);    //申请的阿里云的 签名名称
            request.putQueryParameter("TemplateCode", templateCode);   //申请的阿里云的 模板code

            HashMap<String, Object> params = new HashMap<>();
            params.put("code",param);
            request.putQueryParameter("TemplateParam", JSONObject.toJSONString(params)); //验证码数据,转换json数据传递

            try {
                //最终发送
                CommonResponse response = client.getCommonResponse(request);
                System.out.println("发送成功");
                //判断成功还是失败
            }catch (ServerException e) {
                e.printStackTrace();
                System.out.println("发送失败1");
            } catch (ClientException e) {
                e.printStackTrace();
                System.out.println("发送失败2");
            }
        }
    }
}
  • application.yml 配置自定义的配置文件参数
spring:
  cloud:
    alicloud:
      ### 自定义短信配置文件
      sms:
        access-key-i-d: 自己的AccessKeyId
        access-key-secret: 自己的AccessKeySecret
        sign-name: 申请的阿里云的 签名名称
        template-code: 申请的阿里云的 模板code

主程序模块

  • 错误状态码枚举类
public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }
    public String getMsg(){
        return msg;
    }
}
  • openfeign 远程调用第三方模块发送验证码
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    //调用第三方模块发送验证码
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
  • controller
  • 验证码的再次校验:存进redis
  • 防止同一个手机号在60s内再次发送验证码:存的时候加上当前的时间戳。并在每一次调用发送验证码的远程接口时先取出来redis里存的验证码。并以 _ 切割字符串。判断redis里存取的时间和当前时间的时间差是否在60000毫秒以内,在的话抛出枚举类里定义的异常
@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService; //注入远程调用
    @Autowired
    StringRedisTemplate stringRedisTemplate; //注入redis

    //调用短信发送功能
    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        
        //每次执行先读取是否由此验证码
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE + phone);

        //TODO 1、接口防刷
      
      
      	//第一次发送验证码肯定是空的。所以此处不为空时,才判断是否在60s之内
        if (!StringUtils.isEmpty(redisCode)){ 
            long s = Long.parseLong(redisCode.split("_")[1]); //截取存进redis的时间
            if (System.currentTimeMillis() - s < 60000){
                //防止同一个手机号在60s内再次发送验证码
                System.out.println("60s内不能再次发送");
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        String code = UUID.randomUUID().toString().substring(0, 5) + "_" + System.currentTimeMillis(); //给redis寸数据加时间戳。
        
        //2、验证码的再次校验 -> redis  key对应phone value对应code   sms:code:18346779985 -> 123456
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE+phone,code,10, TimeUnit.MINUTES); //指定过期时间为10分钟
        
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }
}
  • js 给指定手机号发送完验证码后,调用回调方法。判断 主程序的code的值
//发送验证码。倒计时
$(function f() {
	$("#sendCode").click(function () {
		//1、倒计时效果
		//判断class里是否有disavled。如果有的话代表正在倒计时。不能重复点击自定义的倒计时方法
		if ($(this).hasClass("disavled")){
			//正在倒计时
		}else {
			//2、给指定手机号发送验证码
			phoneNumber = $("#phoneNum").val(),//得到手机号
			$.get("/sms/sendcode?phone="+ phoneNumber,function (data) {
				if (data.code != 0){
					alert(data.msg);
				}
			}); //给LoginController发送请求。拼接phone参数
			timeoutChangeStyle();
		}
	})
})

在这里插入图片描述


04、注册【异常机制、加密】


被调用服务 -> 会员服务

  • 错误状态码枚举类 定义枚举 用于判断手机号或者验证码是否重复
    //  错误状态码枚举类

public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    USER_EXIST_EXCEPTION(15001,"用户名已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode(){
        return code;
    }

    public String getMsg(){
        return msg;
    }
}
  • vo 【接收注册页面传过来的注册信息的vo】
//接收注册页面传过来的注册信息的vo
@Data
public class MemberRegistVo {
    private String username;
    private String password;
    private String phone;
}
  • controller 需要捕获异常 -> 判断手机号已存在 用户名已存在。然后返回给调用服务
  • 通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
  • 如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
/***
     *      会员的注册功能 -> 远程调用
     */
    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){

        try{
            memberService.regist(vo);
            //捕获异常 -> 手机号已存在 用户名已存在
        }catch (PhoneExistException e){
            R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
        }catch (UserNameExistException e){
            R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
        }

        return R.ok();
    }
  • server 接口
public interface MemberService extends IService<MemberEntity> {
    //会员的注册功能
    void regist(MemberRegistVo vo);

    //检查用户名和手机号是否唯一
    void checkPhoneUnique(String phone) throws PhoneExistException;
    void checkUseNameUnique(String username) throws UserNameExistException;
}
  • serviceImpl 异常机制
@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Autowired
    MemberLevelDao memberLevelDao;

    //会员的注册功能
    @Override
    public void regist(MemberRegistVo vo) {
        MemberDao memberDao = this.baseMapper;
        MemberEntity entity = new MemberEntity();

        //1、注册时设置默认等级:普通会员。
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        entity.setLevelId(levelEntity.getId());

        //2、检查用户名和手机号是否唯一,为了让controller感知到异常,需要使用异常机制
        checkPhoneUnique(vo.getPhone());
        checkUseNameUnique(vo.getUsername());

        entity.setMobile(vo.getPhone());
        entity.setUsername(vo.getUsername());

        //密码要进行加密存储 -> 不可逆 -> MD5 -> MD5盐值 -> BCryptPasswordEncoder
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        //其他的默认信息

        //保存
        memberDao.insert(entity);
    }
	
  	//-------------------------------------异常机制----------------------------------------
    //检查用户名和手机号是否唯一   
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException{
        MemberDao memberDao = this.baseMapper;
        Integer count1 = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (count1 > 0){
            throw new PhoneExistException();
        }
    }
    @Override
    public void checkUseNameUnique(String username) throws UserNameExistException{
        MemberDao memberDao = this.baseMapper;
        Integer count2 = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (count2 > 0){
            throw new UserNameExistException();
        }
    }
}
  • 设置用户的默认等级
  • MemberLevelDao
    //1、注册时设置默认等级:普通会员 -> default_status = 1
    MemberLevelEntity getDefaultLevel();
  • MemberLevelDao.xml
    <select id="getDefaultLevel" resultType="com.wulawula.gulimall.member.entity.MemberLevelEntity">
        SELECT * FROM ums_member_level WHERE default_status = 1
    </select>

加密

1、 MD5加密

  • MD5不可直接进行加密存储:
  • 抗修改性 -> MD5暴力破解网站每天大量搜索MD5的值。 -> 制作成彩虹表 每个数段的MD5值都是一样的,就会存储起来,等待查询
@Test
void contextLoads() {
  	
  	//e10adc3949ba59abbe56e057f20f883e
	String s = DigestUtils.md5Hex("123456");
	
	//7e8feb2276322ecddd4423b649dfd4d9
	String s = DigestUtils.md5Hex("123456 ");
  
  	System.out.println(s)
}

2、 MD5盐值加密

  • 盐值加密 加盐(随机盐): $1$ + 8位字符
  • 且两次的运行结果是一样的
  • 验证密码进行登陆:再次将用户输入的 123456 进行盐值加密 【盐值要去数据库查,数据库需要保存盐值字段】
@Test
void contextLoads() {
  	
  	//$1$YdkJTmB1$jsWeFyCOFNJ1jXRp3rHJe1
	String s = Md5Crypt.md5Crypt("123456".getBytes());
  
    //第一次:$1$88888888$.04CpISZfzlbsDnC6Fjr11
    //第二次:$1$88888888$.04CpISZfzlbsDnC6Fjr11
	String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$88888888");

  	System.out.println(s)
}

3、 BCryptPasswordEncoder推荐

  • Spring的密码加密器(基于盐值加密)

  • 也是拼接了随机字符串 即使要加密的数据一样,得到的加密字符串却不一样

  • 加密使用 encode 解密使用 matches

  • 一个字符串加密两次得到两个不同的结果,将两个结果解密,发现两个都是123456的加密字符,返回true

@Test
void contextLoads() {
  	
	BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    //第一次:$2a$10$6VBbkOIjUzoYsA/Wo/glsui6uZ1kJ8lMaygh6YjdhY2tMC3c4kPf6
    //第二次:$2a$10$ezArPw7xXBsZ.DMFLFEh1u4VnOccNSDCwHnnih3mGVRc5ewzpVV9q
    String s = passwordEncoder.encode("123456");

    //true
    boolean matches = passwordEncoder.matches("123456", "$2a$10$6VBbkOIjUzoYsA/Wo/glsui6uZ1kJ8lMaygh6YjdhY2tMC3c4kPf6");
    //true
    boolean matches = passwordEncoder.matches("123456", "$2a$10$ezArPw7xXBsZ.DMFLFEh1u4VnOccNSDCwHnnih3mGVRc5ewzpVV9q");

	System.out.println(s + " -> " + matches);
}

调用服务 -> 登录注册服务

  • feign 【会员的注册功能 -> 远程调用】
@FeignClient("gulimall-member")
public interface MemberFeignService {

    //会员的注册功能 -> 远程调用
    @PostMapping("/member/member/regist")
    R regist(@RequestBody UserRegistVo vo);
}
  • VO 【此处使用JSR303校验vo字段】
@Data
public class UserRegistVo {

    @NotEmpty(message = "用户名不能为空") //不能为空
    @Length(min = 6,max = 18,message = "用户名必须是6-18位字符") //用户名长度限制
    private String username;

    @NotEmpty(message = "密码不能为空") //不能为空
    @Length(min = 6,max = 18,message = "用户名必须是6-18位字符") //密码长度限制
    private String password;

    @NotEmpty(message = "手机号不能为空") //不能为空
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确") //正则表达式 第一个数字是1,第2个数字是3-9内的,后面的9个数字是0-9之内的
    private String phone;

    @NotEmpty(message = "验证码不能为空") //不能为空
    private String code;
}
  • controller 【此处的注册功能是根据是根据另一个服务调用的】
  • 若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
  • 若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册
  • 会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
  • RedirectAttributes可以通过session保存信息并在重定向的时候携带过去

	@Autowired
    StringRedisTemplate stringRedisTemplate; //Redis
    @Autowired
    MemberFeignService memberFeignService; //远程调用

	//注册

    @PostMapping("/regist")
    //@Valid开启校验功能。错误信息都会在BindingResult中
    //RedirectAttributes 重定向携带数据,不要用Model了
    // TODO 重定向携带数据,利用session原理,将数据放在session中。只要跳转到下一个页面且去除这个数据以后,session里面的数据就会被删除
    // TODO 分布式下session会出现问题

    public String regist(@Valid UserRegistVo vo, BindingResult result, /*Model model*/ RedirectAttributes redirectAttributes){
        /***
         *  1、使用JSR303校验填写的内容是否出错。出错则重定向到注册页面。且将错误信息返回给前台。
         */
        //1、如果BindingResult里有校验错误
        if (result.hasErrors()){

            //将收集的错误信息手机成一个map集合。传给前台页面。用于展示
            //方法 1
//            HashMap<String, String> errors = new HashMap<>();
//            result.getFieldErrors().stream().map(fieldError -> {
//                String field = fieldError.getField(); //哪个字段出现了错误
//                String defaultMessage = fieldError.getDefaultMessage(); //错误的消息
//                errors.put(field,defaultMessage);
//                return errors;
//            });

            //方法 2
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));

            //            model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors); //重定向的写法

            //2、校验出错。重定向到注册页
            /**
             * 使用 return "forward:/reg.html"; 会出现
             * 问题:Request method 'POST' not supported的问题
             * 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
             */
//            return "foeward:/reg.html"; //转发
//            return "reg";    //转发会出现重复提交的问题,不要以转发的方式
            //使用重定向  解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributes
            return "redirect:http://auth.wulawula.com/reg.html";
        }

        /***
         *  2、校验验证码。调用远程服务,真正的进行注册
         */
        //1、校验验证码
        String code = vo.getCode();
        String rediscode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE + vo.getPhone());
        //判断redis里是否有验证码
        if (! StringUtils.isEmpty(rediscode)){
            String s = rediscode.split("_")[0];
            //判断验证码是否正确
            if (code.equals(s)){
                //校验成功。删除验证码(下次再用旧的验证码就不能通过了) --> 令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE + vo.getPhone());
                //2、验证码通过  真正的注册,调用远程读取进行注册
                R r = memberFeignService.regist(vo);
                //判断状态码
                if (r.getCode() == 0){
                    //成功
                    return "redirect:http:/login.html";
                }else {
                    //失败 异常
                    HashMap<String, String> errors = new HashMap<>();
                    errors.put("msg",r.getData(new TypeReference<String>(){}));
                    redirectAttributes.addFlashAttribute("errors",errors);

                    return "redirect:http://auth.wulawula.com/reg.html";
                }

            }else {
                //验证码出错
                Map<String, String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                redirectAttributes.addFlashAttribute("errors",errors); //重定向的写法
                return "redirect:http://auth.wulawula.com/reg.html";
            }
        }else {
            //redis里没有验证码
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            redirectAttributes.addFlashAttribute("errors",errors); //重定向的写法
            return "redirect:http://auth.wulawula.com/reg.html";
        }

        //3、注册成功,回到登录页
//        return "redirect:http://auth.wulawula.com/login.html";
//        return "redirect:http:/login.html"; //重定向
    }

转发和重定向的区别

forward 【转发】redirect 【重定向】
地址栏服务器的直接跳转,客户端浏览器并不知道,地址栏内容不变(服务器内部的动作)为客户端浏览器根据url地址重新向服务器请求,地址栏变(有可能是请求的URI地址发生变化)
数据共享共享浏览器传来的request全新的request
运用的地方用户登录后根据角色跳转页面在用户注销后跳转主页或其他页面
效率较高(比重定向少了一次服务器请求)较低

05、简单登录


被调用的服务 -> 会员服务

  • 错误状态码枚举类
public enum BizCodeEnume {

    UNKNOWN_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    USER_EXIST_EXCEPTION(15001,"用户名已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }
    public String getMsg(){
        return msg;
    }
}
  • vo 【封装登陆传过来的数据】
@Data
public class MemberLoginVo {
    private String loginacct;
    private String password;
}
  • controller 【会员的登录功能 -> 远程调用】
@PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){

        MemberEntity entity = memberService.login(vo);
        if (entity != null){
            return R.ok();
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
  • MemberServiceImpl 【会员的登录功能】
  • 因为是使用的 BCryptPasswordEncoder 加密。必须去数据库查询,然后解密比对
@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Override
    public MemberEntity login(MemberLoginVo vo) {

        String loginacct = vo.getLoginacct();
        String password = vo.getPassword();

        //1、去数据库查询 SELECT * FROM ums_member WHERE username = ? OR mobile = ?
        MemberDao memberDao = this.baseMapper;
        //用户名和手机号都可以当作登录名使用
        MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile",loginacct));

        if (entity == null){
            //登陆失败 手机号或用户名匹配不上
            return null;
        }else {
            //判断密码是否匹配  需要解密
            String passwordDb = entity.getPassword();
            //解密
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            //2、进行密码匹配  						参数1:明文密码  参数2:数据库查到的加密密码
            boolean matches = passwordEncoder.matches(password, passwordDb); 
            if (matches){
                return entity;
            }else {
                return null;
            }
        }
    }
}

调用的服务 -> 登录注册服务

通过会员服务远程调用登录接口

  • 如果调用成功,重定向至首页
  • 如果调用失败,则封装错误信息并携带错误信息重定向至登录页
  • vo 【封装前台传来的数据】
//登录数据封装
@Data
public class UserLoginVo {
    private String loginacct;
    private String password;
}
  • feign
@FeignClient("gulimall-member")
public interface MemberFeignService {
    //会员的登录功能 -> 远程调用
    @PostMapping("/member/member/login")
    R login(@RequestBody UserLoginVo vo);
}
  • controller 【登录】
@Controller
public class LoginController {

    @Autowired
    MemberFeignService memberFeignService; //调用远程服务

    @PostMapping("/login")
    public String login(UserLoginVo vo,RedirectAttributes result){

        //远程登陆
        R r = memberFeignService.login(vo);
        if (r.getCode() == 0){
            //成功 -> 首页
            return "redirect:http://wulawula.com"; //重定向
        }else {
            //失败 -> 登录页
            HashMap<String, String> errors = new HashMap<>();
            errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
            result.addFlashAttribute("errors",errors);
            System.out.println(errors);
            return "redirect:http://auth.wulawula.com/login.html"; //重定向
        }
    }
} 
  • html
<div class="si_bom1 tab" style="display: none;">
	<div class="error">
		<div></div>
		请输入账户名和密码
	</div>

	<form action="/login" method="post">
<!--						<div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:''):''}"></div>-->
		<ul>
			<li class="top_1">
				<img src="/static/login/JD_img/user_03.png" class="err_img1"/>
				<input type="text" name="loginacct" placeholder=" 邮箱/用户名/已验证手机" class="user"/>
			</li>
			<li>
				<img src="/static/login/JD_img/user_06.png" class="err_img2"/>
				<input type="password" name="password" placeholder=" 密码" class="password"/>
			</li>
			<li class="bri">
				<a href="">忘记密码</a>
				<div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:''):''}"></div>
			</li>
			<li class="ent">
				<button class="btn2" type="submit"><a>&nbsp; &nbsp;</a></button>
			</li>
		</ul>
	</form>
</div>

06、社交登录 OAuth2.0


注册登录3


微博作为社交账号进行社交登录为例

1、进入微博开放平台且登陆自己的微博账号

https://open.weibo.com/

2、点击 微连接 -> 网站接入 -> 立即接入

在这里插入图片描述

3、设置回调页面路径

在这里插入图片描述

4、点击 文档 -> OAuth2.0授权认证 【里面有需要的资源】

在这里插入图片描述

5、引导需要授权的用户到如下地址:URL GET请求

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

  • YOUR_CLIENT_ID:你申请的应用的AppKey
  • YOUR_REGISTERED_REDIRECT_URI:登陆之后重定向的跳转URI(授权回调页)

HTML

<li>
	<a href="https://api.weibo.com/oauth2/authorize?client_id=2416521972&response_type=code&redirect_uri=http://wulawula.com/success">
	<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png" />
	</a>
</li>

点击登陆之后。跳转成功

在这里插入图片描述

6、如果用户同意授权,页面跳转至 授权回调页/?code=CODE

code是我们用来换取令牌的参数

http://wulawula.com/success?code=f53d29f843b681a28d1fe6321f00476e

7、 换取Access Token(访问令牌)URL Code只能换取一次Access Token

https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE

  • YOUR_CLIENT_ID: 你申请的应用的AppKey
  • YOUR_CLIENT_SECRET: 创建网站应用时的App Secret
  • YOUR_REGISTERED_REDIRECT_URI: 登陆之后重定向的跳转URI(授权回调页)
  • CODE:换取令牌的认证码
https://api.weibo.com/oauth2/access_token?client_id=2416521972&client_secret=bb9dd96f8c51c01799ab5915672b6cdb&grant_type=authorization_code&redirect_uri=http://wulawula.com/success&code=f53d29f843b681a28d1fe6321f00476e

使用PostMan测试得到以下JSON POST请求 可以发现得到了 Access Token

{
    "access_token": "2.00iOICcGqkTXdC0ca7b7d7e5moPOSC",
    "remind_in": "157679999",
    "expires_in": 157679999,
    "uid": "6058806080",
    "isRealName": "true"
}

8、使用获得的Access Token调用API

点击接口管理 -> 根据用户ID获取用户信息 -> 测试接口示例:

https://api.weibo.com/2/users/show.json?access_token=2.00iOICcGqkTXdC0ca7b7d7e5moPOSC&uid=6058806080

使用PostMan测试得到以下JSON POST请求

{
    "id": 6058806080,
    "idstr":  "个人信息......",
    "class":  个人信息......,
    "screen_name":  "个人信息......",
    "name": "个人信息......",
    "province":  "个人信息......",
    "city":  "个人信息......",
    "location":  "个人信息......",
    "description":  "个人信息......",
    ......
}

在这里插入图片描述

1、如果用户同意授权,页面跳转至 授权回调页/?code=CODE code是我们用来换取令牌的参数

http://wulawula.com/success?code=f53d29f843b681a28d1fe6321f00476e

此处也应该屏蔽掉

2、应用的 client_idclient_secret 应该都是保密的,不应该由页面处理,应该由服务器后台处理

修改授权回调页 http://auth.wulawula.com/oauth2.0/weibo/success

登陆之后重定向的跳转URI

在这里插入图片描述

流程图

在这里插入图片描述


被调用的服务

  • SocialUser 【社交登录封装远程调用的数据。注册进会员表】
@Data
public class SocialUser {
    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid; //社交id
    private String isRealName;
}
  • MemberOAuth2Entity 【封装数据的实体类】
@Data
@TableName("ums_member_oauth2")
public class MemberOAuth2Entity implements Serializable {
	private static final long serialVersionUID = 1L;
	   // id
	private Long id;
	   // 会员等级id
	private Long levelId;
	   // 用户名
	private String username;
	   // 密码
	private String password;
	   // 昵称
	private String nickname;
	   // 手机号码
	private String mobile;
	   // 邮箱
	private String email;
	   // 头像
	private String header;
	   // 性别
	private Integer gender;
	   // 生日
	private Date birth;
	   // 所在城市
	private String city;
	   // 职业
	private String job;
	   // 个性签名
	private String sign;
	   // 用户来源
	private Integer sourceType;
	   // 积分
	private Integer integration;
	   // 成长值
	private Integer growth;
	   // 启用状态
	private Integer status;
	   // 注册时间
	private Date createTime;
	   //	社交帐号id
	private String socialUid;
	   //	社交帐号访问令牌
	private String accessToken;
	   //	社交帐号访问令牌的过期时间
	private Long expiresIn;
}
  • MemberOAuth2Controller
  • 登录包含两种流程,实际上包括了注册和登录
  • 如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息,注册并将结果返回
  • 如果之前已经使用该社交账号登录,则更新token并将结果返回
@RestController
@RequestMapping("/member/oauth")
public class MemberOAuth2Controller {

    @Autowired
    MemberOAuth2Service memberOAuth2Service;

    /***
     *      社交用户(登录+注册 合并) -> 远程调用
     */
    @PostMapping("/oauth2/login")
    public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception {
        MemberOAuth2Entity entity = memberOAuth2Service.oauthlogin(socialUser);
        if (entity != null){
            //TODO 1、登陆成功处理
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
}
  • MemberOAuth2ServiceImpl
@Service("memberOAuth2Service")
public class MemberOAuth2ServiceImpl extends ServiceImpl<MemberOAuth2Dao, MemberOAuth2Entity>  implements MemberOAuth2Service {

    @Autowired
    MemberOAuth2Dao memberOAuth2Dao;


    //社交用户(登录+注册 合并) -> 远程调用
    @Override
    public MemberOAuth2Entity oauthlogin(SocialUser socialUser) throws Exception {
        String uid = socialUser.getUid();
        //1、判断当前社交用户是否已经登陆过系统
        MemberOAuth2Dao memberDao = this.baseMapper;

        MemberOAuth2Entity memberOAuth2Entity = memberDao.selectOne(new QueryWrapper<MemberOAuth2Entity>().eq("social_uid", uid));
        if (memberOAuth2Entity != null){
//------------------登录------------------
            //2、这个用户已经注册过了
            MemberOAuth2Entity update = new MemberOAuth2Entity();

            update.setId(memberOAuth2Entity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());

            memberDao.updateById(update);

            memberOAuth2Entity.setAccessToken(socialUser.getAccess_token());
            memberOAuth2Entity.setExpiresIn(socialUser.getExpires_in());
            return memberOAuth2Entity;
        }else {
//------------------注册------------------
            //3、没有查到当前社交用户对应的记录,注册
            MemberOAuth2Entity regist = new MemberOAuth2Entity();
            //4、查询当前社交用户的社交登录账号(昵称、性别等)
            try {
                HashMap<String, String> query = new HashMap<>();
                query.put("access_token",socialUser.getAccess_token());
                query.put("uid",socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String,String>(), query);
                if (response.getStatusLine().getStatusCode() == 200){ //返回状态码 200
                    //查询成功
                    String json = EntityUtils.toString(response.getEntity()); //得到请求体对象转为JSON
                    JSONObject jsonObject = JSON.parseObject(json); //直接从json里取值
                    String name = jsonObject.getString("name"); //昵称
                    String gender = jsonObject.getString("gender"); //性别

                    //---将查询到的信息写入实体类---
                    regist.setNickname(name);
                    regist.setGender("m".equals(gender)? 1 : 0);
                }
            }catch (Exception e){
                e.printStackTrace();
            }

            regist.setSocialUid(socialUser.getUid());
            regist.setAccessToken(socialUser.getAccess_token());
            regist.setExpiresIn(socialUser.getExpires_in());
            System.out.println("regist =" + regist);
            memberDao.insert(regist);
            return regist;
        }
    }
}

调用的服务

SocialUserMemberOAuth2ResponseVo 分别对应被调用程序的 SocialUser 和 **MemberOAuth2Entity **

  • feign
@FeignClient("gulimall-member")
public interface MemberFeignService {

    //社交用户(登录+注册 合并) -> 远程调用
    @PostMapping("/member/oauth/oauth2/login")
    R oauthlogin(@RequestBody SocialUser socialUser);
}
  • OAuth2Controller 【处理社交登录请求】
  • 通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
  • 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
@Slf4j
@Controller
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;

    /***
     * 根据用户登录得到的code换取Access Token
     */
    @RequestMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code) throws Exception {

        //1、根据code换取Access Token  ->  说明登陆成功
        HashMap<String, String> map = new HashMap<>();
        map.put("client_id","24******72");
        map.put("client_secret","bb9dd96f8c************15672b6cdb");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.wulawula.com/oauth2.0/weibo/success");
        map.put("code",code);
        /***
         *  参数1:换取Access Token的主机地址
         *  参数2:path给哪里发请求
         *  参数3:请求方式
         *  参数4:请求头
         *  参数5:查询参数
         *  参数6:请求体
         */
        Map<String, String> headers = new HashMap<>();
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);

        //2、处理
        //获取响应状态码
        if (response.getStatusLine().getStatusCode() == 200){
            //成功 -> 获取到了 Access Token
                // response.getEntity() 获取到响应体内容
                // EntityUtils.toString() 可以将响应体内容转换为JSON
            String json = EntityUtils.toString(response.getEntity());
            // JSON -> 实体类对象
            SocialUser socialUser = JSON.parseObject(json, SocialUser.class);

            //知道是哪个社交用户了
            // 3.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
            //登陆或者注册这个社交用户
            R login = memberFeignService.oauthlogin(socialUser);
            if (login.getCode() == 0){
                MemberOAuth2ResponseVo data = login.getData("data", new TypeReference<MemberOAuth2ResponseVo>() {
                });
                System.out.println("登陆成功...... 用户信息: " + data);
                log.info("登陆成功,用户信息:{}");
                //登陆成功,跳回首页
                return "redirect:http://wulawula.com";
            }else {
                //失败 -> 重定向到登陆页
                return "redirect:http://auth.wulawula.com/login.html";
            }
        }else {
            //失败 -> 重定向到登陆页
            return "redirect:http://auth.wulawula.com/login.html";
        }
    }
}

07、Session


注册登录11

jsessionid相当于银行卡卡号,存在服务器的session相当于存储的现金,每次都能通过jsessionid取出现金


分布式下session的共享问题

在这里插入图片描述

  • 问题1、同一服务的集群,session不同步问题

如果第一次访问1号服务器,且保存了cookie在1号服务器的内存空间中,因为是分布式集群环境,第2次可能访问2号服务器,虽然请求也携带了cookie,但是之前cookie存的是在1号服务器的内存中,2号服务器并没有,因此也会出现问题

  • 问题2、:不同服务、不同域名的session跨域问题

正常情况下session不可跨域,它有自己的作用范围,session只能在当前域名(Domain)下生效,域名一换,session就找不到了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbZNR4Ji-1608222854287)(D:\Java项目\00_A_Java笔记\小技术点笔记配套图片\注册登录12.png)]


问题1解决方式:同一服务的集群,session不同步问题

1、session复制 不推荐

注册登录14

2、客户端存储 不推荐

在这里插入图片描述

3、hash一致性

在这里插入图片描述

4、统一存储 整合 Redis + SpringSession 推荐

在这里插入图片描述


问题2解决方式:不同服务、不同域名的session跨域问题

示例:父域名:wulawula.com 子域名:auth.wulawula.com order.wulawula.com

示例:第一次浏览器在会员服务里登陆成功,会员服务会将数据存到session,且session不在自己服务的内存里存储,让其在redis里存储,然后给浏览器返回cookie,此处让这个cookie的JSESSIONID对应的的作用域不能只是自己的服务,此处应该放大作用域,由子域名放大到父域名,使得以后的访问都由父域名进行访问,且数据都统一存储在redis中,就算要使用JSESSIONID取出对应的数据,都要去Redis里去查询

在这里插入图片描述


08、整合SpringSession


因为是分布式环境。登录模块为 gulimall-auth-server。 首页模块为 gulimall-product。公共模块为 gulimall-common

gulimall-common

  • MemberOAuth2ResponseVo 【序列化】
@ToString
@Data
//由于SpringSession默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
public class MemberOAuth2ResponseVo implements Serializable {
	   // id
	private Long id;
	   // 会员等级id
	private Long levelId;
	   // 用户名
	private String username;
	   // 密码
	private String password;
	   // 昵称
	private String nickname;
	   // 手机号码
	private String mobile;
	   // 邮箱
	private String email;
	   // 头像
	private String header;
	   // 性别
	private Integer gender;
	   // 生日
	private Date birth;
	   // 所在城市
	private String city;
	   // 职业
	private String job;
	   // 个性签名
	private String sign;
	   // 用户来源
	private Integer sourceType;
	   // 积分
	private Integer integration;
	   // 成长值
	private Integer growth;
	   // 启用状态
	private Integer status;
	   // 注册时间
	private Date createTime;
	   //	社交帐号id
	private String socialUid;
	   //	社交帐号访问令牌
	private String accessToken;
	   //	社交帐号访问令牌的过期时间
	private Long expiresIn;
}

gulimall-auth-server

  • pom
<dependency>
	<groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • properties
###配置SpringSession
#SpringSession的保存类型
spring.session.store-type=redis
#session的过期时间 (默认30分钟)
server.servlet.session.timeout=30m

启动类添加注解 整合Redis作为session的存储 @EnableRedisHttpSession

  • 自定义配置 config 【两个模块都需要加】
/***
 * 需要解决子域共享问题
 */
@Configuration
public class GulimallSessionConfig {

    //需要解决子域共享问题 -> 默认是子域,需要改为父域
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("wulawula.com"); //指定作用域为父域 而不是子域
        cookieSerializer.setCookieName("WULA_SESSION"); //修改名字
        return cookieSerializer;
    }

    //使用JSON的序列化方式来序列化对象数据到redis中 -> redis中看到的就不是二进制字符了
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}
  • controller
@Slf4j
@Controller
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;

    /***
     * 根据用户登录得到的code换取Access Token
     * @param code
     * @return
     * @throws Exception
     */
    @RequestMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {

        //1、根据code换取Access Token  ->  说明登陆成功
        HashMap<String, String> map = new HashMap<>();
        map.put("client_id","2416521972");
        map.put("client_secret","bb9dd96f8c51c01799ab5915672b6cdb");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.wulawula.com/oauth2.0/weibo/success");
        map.put("code",code);
        /***
         *  参数1:换取Access Token的主机地址
         *  参数2:path给哪里发请求
         *  参数3:请求方式
         *  参数4:请求头
         *  参数5:查询参数
         *  参数6:请求体
         */
        Map<String, String> headers = new HashMap<>();
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);

        //2、处理
        //获取响应状态码
        if (response.getStatusLine().getStatusCode() == 200){
            System.out.println("获取token成功");
            //成功 -> 获取到了 Access Token
                // response.getEntity() 获取到响应体内容
                // EntityUtils.toString() 可以将响应体内容转换为JSON
            String json = EntityUtils.toString(response.getEntity());
            // JSON -> 实体类对象
            SocialUser socialUser = JSON.parseObject(json, SocialUser.class);

            //知道是哪个社交用户了
            // 3.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
            //登陆或者注册这个社交用户
            R login = memberFeignService.oauthlogin(socialUser);
            if (login.getCode() == 0){
                System.out.println("登陆成功");
                MemberOAuth2ResponseVo data = login.getData("data", new TypeReference<MemberOAuth2ResponseVo>() {
                });
                System.out.println("登陆成功...... 用户信息: " + data);
                log.info("登陆成功,用户信息:{}");
/*session---------------------------------------------*/
                //TODO 1、默认发的令牌.session=唯一的字符串。作用域为当前域   (需要解决子域共享问题)
                //TODO 2、使用JSON的序列化方式来序列化对象数据到redis中
                session.setAttribute("loginUser",data); //直接加入
/*--------------------------------------------------*/
                //登陆成功,跳回首页
                return "redirect:http://wulawula.com";
            }else {
                System.out.println("登陆失败");
                //失败 -> 重定向到登陆页
                return "redirect:http://auth.wulawula.com/login.html";
            }
        }else {
            //失败 -> 重定向到登陆页
            System.out.println("获取token失败");
            return "redirect:http://auth.wulawula.com/login.html";
        }
    }
}

gulimall-product

  • pom
<dependency>
	<groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • properties
###配置SpringSession
#SpringSession的保存类型
spring.session.store-type=redis

启动类添加注解 整合Redis作为session的存储 @EnableRedisHttpSession

  • 自定义配置 config 【两个模块都需要加】
/***
 * 需要解决子域共享问题
 */
@Configuration
public class GulimallSessionConfig {

    //需要解决子域共享问题 -> 默认是子域,需要改为父域
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("wulawula.com"); //指定作用域为父域 而不是子域
        cookieSerializer.setCookieName("WULA_SESSION"); //修改名字
        return cookieSerializer;
    }

    //使用JSON的序列化方式来序列化对象数据到redis中 -> redis中看到的就不是二进制字符了
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}
  • js 此处使用三元判断判断显示昵称问题
<li>
	<a href="http://auth.wulawula.com/login.html">你好,请登录 [[${session.loginUser==null ? '' : session.loginUser.nickname}]]</a>
</li>
<li>
	 <a href="http://auth.wulawula.com/reg.html" class="li_2">免费注册</a>
</li>

注册登录20

自动延期:redis中的数据也是有过期时间的


SpringSession核心原理 - 装饰者模式

  • 原生的获取session时是通过HttpServletRequest获取的
  • SpringSession是包装过的

@EnableRedisHttpSession导入RedisHttpSessionConfiguration.class配置

  • 1、给容器中添加了一个组件

​ SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增删改查的封装类

  • 2、SessionRepositoryFilter=》Filter: session存储过滤器,每个请求过来都必须经过filter
    • 1、创建的时候,就自动从容器中获取到了SessionRepository:
    • 2、原生的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
    • 3、以前获取session。 request.getSession()
    • 4、以后获取session。wrapperedRequest.getSession();===>SressionRepository中获取到
@Override
protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
   request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
 
   //包装原始的请求对象
   SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
         request, response, this.servletContext);
   //包装原始的响应对象
   SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
         wrappedRequest, response);
 
   try {
      //此执行链使用的是包装后的对象
      filterChain.doFilter(wrappedRequest, wrappedResponse); //使得我们获取的session都是包装过的
   }
   finally {
      wrappedRequest.commitSession();
   }
}

09、细节修改


  • gulimall-common 的 AuthServerConstant
  • 存redis时定义的key
public class AuthServerConstant {
    public static final String SMS_CODE_CACHE= "sms:code:"; //redis存取验证码的前缀
    public static final String LOGIN_USER= "loginUser"; //redis存取登陆的key的前缀
}
  • gulimall-auth-server 的 LoginController
  • 尝试获取session。有session就是登陆了 -> 首页 。 没有session就是没登陆 -> 登录页
    @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute == null){
            //没登陆
            return "login";
        }else {
            //登陆了
            return "redirect:http://wulawula.com"; //重定向
        }
    }
  • GulimallConfig 【视图映射】
  • 需要判断是否有session。视图映射不能直接跳转。
@Configuration
public class GulimallConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
                              /*  参数1:url路径       参数2:视图名 */
//        registry.addViewController("/login.html").setViewName("login"); 
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

es服务也需要加入 08整合SpringSession中的 pom、properties、自定义配置 config、以及redis的pom

只要session没过期,让其在首页、搜索页、商品页都显示登陆的会员的昵称

  • 示例 html
<li>
	<a href="http://auth.wulawula.com/login.html" class="li_2" th:if="${session.loginUser == null}">你好,请登录</a>
    <a class="li_2" th:else style="width: 100px">欢迎:[[${session.loginUser.nickname}]]</a>
</li>
<li>
	<a href="http://auth.wulawula.com/reg.html" th:if="${session.loginUser == null}">免费注册</a>
</li>

010、单点登录


希望一个账号登陆旗下多个不同应用。一处登录处处登录。一处退出处处退出

单点登录2


1、配置XXL-SSO

XXL-SSO 是三个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。

拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持""等特性。现已开放源代码,开箱即用。

编排:

  • ssoserver.com 登录认证服务器
  • client1.com 客户端1
  • client2.com 客户端2

1、修改SwitchHosts

# XLL-OOS
127.0.0.1 ssoserver.com
127.0.0.1 client1.com 
127.0.0.1 client2.com 

修改 xxl-sso配置文件 application.properties 【且知道不同的端口号以及访问路径】

8080/xxl-sso-server

8081/xxl-sso-web-sample-springboot

2、打包gitee项目

删除 .git文件 --> 在xxl-sso页 cmd --> mvn clean package -Dmaven.skip.test=true

D:\Java项目\单点登录demo\xxl-sso>mvn clean package -Dmaven.skip.test=true

3、启动 在target页 cmd --> java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar

D:\Java项目\单点登录demo\xxl-sso\xxl-sso-server\target>java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar

访问 http://ssoserver.com:8080/xxl-sso-server/login

4、启动客户端 方法同3 【一个客户端启动两次,修改端口号即可】

第一个加上server.port=8081

D:\Java项目\单点登录demo\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8081

访问 http://client1.com:8081/xxl-sso-web-sample-springboot/

第二个加上server.port=8082

D:\Java项目\单点登录demo\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8082

访问 http://client1.com:8082/xxl-sso-web-sample-springboot/


2、测试

略…

未完 接下一章.

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值