谷粒商城详细笔记

前言

mysql安装在腾讯云

redis安装在本地虚拟机master上

运行时,renren-fast这个项目要到单独开个idea窗口打开。

一、项目简介

1、项目微服务架构图

谷粒商城详细笔记_第1张图片微服务:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
各个服务都注册到注册中心,别的服务可以通过注册中心发现我们的服务。

2、微服务划分图

谷粒商城详细笔记_第2张图片

二、开发

1、配置虚拟机

  • 安装vmware(阿里云盘:IT技术学习 - gulimall-soft)

    • 网址:https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html
    • 下载地址:https://www.vmware.com/go/getworkstation-win
    • vmware16pro许可证密钥最新
      • ZF3R0-FHED2-M80TY-8QYGC-NPKYF
      • YF390-0HF8P-M81RQ-2DXQE-M2UT6
      • ZF71R-DMX85-08DQY-8YMNC-PPHV8
  • 本机搭建Linux (centos)

    • 点击 创建新的虚拟机
      谷粒商城详细笔记_第3张图片

    • 点击 自定义 下一步
      谷粒商城详细笔记_第4张图片

    • 点击 下一步
      谷粒商城详细笔记_第5张图片

    • 点击 稍后安装操作系统 下一步
      谷粒商城详细笔记_第6张图片

    • 点击 Linux CentOS 7 64位 下一步
      谷粒商城详细笔记_第7张图片

    • 修改虚拟机名称slave 修改位置 下一步
      谷粒商城详细笔记_第8张图片

    • 处理器配置设置 下一步
      谷粒商城详细笔记_第9张图片

    • 内存3072 下一步
      谷粒商城详细笔记_第10张图片

    • 网络类型
      谷粒商城详细笔记_第11张图片

    • 虚拟机向导
      谷粒商城详细笔记_第12张图片

    • 磁盘类型
      谷粒商城详细笔记_第13张图片

    • 选择磁盘
      谷粒商城详细笔记_第14张图片

    • 指定磁盘容量50G
      谷粒商城详细笔记_第15张图片

    • 指定磁盘文件(直接下一步)
      谷粒商城详细笔记_第16张图片

    • 点击完成
      谷粒商城详细笔记_第17张图片

    • 点击 编辑虚拟机设置
      谷粒商城详细笔记_第18张图片

    • 点击 CD/DVD 使用ISO映像文件 浏览
      谷粒商城详细笔记_第19张图片

    • 选择镜像文件所在位置(下载地址:http://ftp.sjtu.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso)
      谷粒商城详细笔记_第20张图片

    • 点击 确定
      谷粒商城详细笔记_第21张图片

    • 点击 开启此虚拟机
      谷粒商城详细笔记_第22张图片

    • 鼠标点进去 点回车
      谷粒商城详细笔记_第23张图片

    • 继续点 回车
      谷粒商城详细笔记_第24张图片

    • 点击 ESC (停止检测)
      谷粒商城详细笔记_第25张图片

    • 选择中文简体 继续
      谷粒商城详细笔记_第26张图片

    • 稍等片刻,待出现如下时,点击软件选择
      谷粒商城详细笔记_第27张图片

    • 选择 基础设施服务器 完成
      谷粒商城详细笔记_第28张图片

    • 稍等片刻,待出现如下时,点击 安装位置
      谷粒商城详细笔记_第29张图片

    • 点击 完成
      谷粒商城详细笔记_第30张图片

    • 点击 网络和主机名
      谷粒商城详细笔记_第31张图片

    • 点击 配置
      谷粒商城详细笔记_第32张图片

    • 点击 常规 选中-可用时自动链接到这个网络
      谷粒商城详细笔记_第33张图片

    • 查看网段

      • 点击 编辑 虚拟网络编辑
        谷粒商城详细笔记_第34张图片

      • 复制好这个网段 192.168.91.0

    • 按如下配置,完了点保存
      谷粒商城详细笔记_第35张图片

    • 设置主机名
      谷粒商城详细笔记_第36张图片

    • 点击 开始安装
      谷粒商城详细笔记_第37张图片

    • 设置 root密码
      谷粒商城详细笔记_第38张图片
      谷粒商城详细笔记_第39张图片

    • 稍等片刻,安装完成!!!
      谷粒商城详细笔记_第40张图片

    • 视频地址:https://www.bilibili.com/video/BV1Qv41167ck?p=6

    • centos镜像下载地址:http://ftp.sjtu.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso

2、linux连接工具安装

  • 位置:阿里云盘(IT技术学习 - gulimall-soft - Xmanager-7)
    谷粒商城详细笔记_第41张图片

  • 解压安装包运行安装程序,点击下一步,选择安装位置,建议安D盘(英文路径),记住自己的安装位置,后面要用到

  • 解压插件包
    谷粒商城详细笔记_第42张图片

    解压后如下图所示
    谷粒商城详细笔记_第43张图片

  • 全选复制解压后的文件,到安装包安装的路径,例如我安装的路径 D:\ruanjian\Xmanager-7,粘贴->替换目标中的文件
    谷粒商城详细笔记_第44张图片

  • 破解完毕!!!

  • 原版地址:https://www.yuque.com/yinghuashuxia-cohok/ahov4c/eipegl

3、虚拟机安装docker

1、创建/etc/docker文件夹
sudo mkdir -p /etc/docker
2、配置镜像加速
// 阿里云 https://cr.console.aliyun.com/cn-shanghai/instances/mirrors
// 从上面地址里的 镜像工具 中找到 镜像加速器
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://lcfrsqb4.mirror.aliyuncs.com"]
}
EOF

//腾讯云
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://mirror.ccs.tencentyun.com"]
}
EOF

// 设置完后重启下daemon
sudo systemctl daemon-reload

//重启docker
sudo systemctl restart docker
3、卸载旧版本
sudo yum remove docker \
                docker-client \
                docker-client-latest \
                docker-common \
                docker-latest \
                docker-latest-logrotate \
                docker-logrotate \
                docker-engine
4、设置Docker仓库源地址
// 阿里云的源地址
$ sudo yum-config-manager \
    --add-repo \
    http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

// 清华大学源地址
$ sudo yum-config-manager \
    --add-repo \
    https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
5、安装 Docker
sudo yum install docker-ce docker-ce-cli containerd.io
6、启动Docker
sudo systemctl start docker
7、设置为开机自启
sudo systemctl enable docker

3、docker安装mysql

1、拉取mysql镜像
docker pull mysql:5.7
2、运行mysql
# --name指定容器名字 -v目录挂载 -p指定端口映射  -e设置mysql参数 -d后台运行
sudo docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
####
-v 将对应文件挂载到主机
-e 初始化对应
-p 容器端口映射到主机的端口
3、创建&修改配置文件
vi /mydata/mysql/conf/my.cnf

里面的具体内容

[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve
4、重启mysql
docker restart mysql
5、设置自动启动
sudo docker update mysql --restart=always   //docker中开机自启

4、安装Redis

1、下载镜像文件
docker pull redis
2、创建本地映射文件
mkdir -p /mydata/redis/conf

touch /mydata/redis/conf/redis.conf
3、修改映射配置文件内容
vim /mydata/redis/conf/redis.conf

具体内容

appendonly yes

表示数据持久化,不会重启就丢。

4、启动 同时 映射到对应文件夹
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
5、使用 redis 镜像执行 redis-cli 命令连接
docker exec -it redis redis-cli
6、重启Redis
docker restart redis
7、设置为开机自启
sudo docker update redis --restart=always
8、安装redis-desktop-manager

安装包在阿里云盘。(IT技术学习 - gulimall-soft)
谷粒商城详细笔记_第45张图片谷粒商城详细笔记_第46张图片

8、统一开发环境
1、jdk至少是1.8

1、查看云端目前支持安装的jdk版本

[root@localhost ~]# yum search java|grep jdk
ldapjdk-javadoc.noarch : Javadoc for ldapjdk
java-1.6.0-openjdk.x86_64 : OpenJDK Runtime Environment
java-1.6.0-openjdk-demo.x86_64 : OpenJDK Demos

2、安装

[root@localhost ~]#  yum install -y java-1.8.0-openjdk

3、验证

[root@localhost ~]# java -version
openjdk version "1.8.0_151"

4、安装openjdk-devel(jps)

sudo yum install java-1.8.0-openjdk-devel.x86_64
2、安装配置linux maven
  • 下载maven

    • 下载地址:https://maven.apache.org/download.cgi (或者直接从阿里网盘取)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFKIB7Ex-1684464779513)(images/谷粒商城项目笔记/image-20220502155305378.png)]

  • 放到/usr/local/目录下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-60LOfMgw-1684464779513)(images/谷粒商城项目笔记/image-20220502155403659.png)]

  • 解压

    tar -zxvf apache-maven-3.8.5-bin.tar.gz
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tPUA9ZZ-1684464779514)(images/谷粒商城项目笔记/image-20220502155506298.png)]

  • 配置maven仓库

    • 进入cd apache-maven-3.6.3目录

      cd apache-maven-3.8.5   #进入apache-maven-3.8.5目录
      
    • 创建ck目录

      mkdir ck    #创建ck目录
      
    • 编辑settings.xml文件

      cd conf            # 进入conf目录
      
      vi settings.xm # settings.xm文件
      
    • 找到localRepository下面加上如下

      <localRepository>/usr/local/apache-maven-3.8.5/ck</localRepository>
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5wFTKqqt-1684464779514)(images/谷粒商城项目笔记/image-20220502160004889.png)]

    • 找到mirror 加上阿里的仓库配置

      <mirror>
            <id>alimaven</id>
            <name>aliyun maven</name>
             <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>central</mirrorOf>
      </mirror>
      
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2t0CsTBF-1684464779514)(images/谷粒商城项目笔记/image-20220502160037809.png)]

    • 编辑:vi /etc/profile 文件,翻到最后加上如下

      export MAVEN_HOME=/usr/local/apache-maven-3.8.5
      export PATH=$PATH:$MAVEN_HOME/bin
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bz7rUllG-1684464779514)(images/谷粒商城项目笔记/image-20220502160217274.png)]

    • 重新加载一下,使新增配置生效

      source /etc/profile
      
  • 安装完成,测试一下

    mvn -v
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tMOXM6Pd-1684464779515)(images/谷粒商城项目笔记/image-20220502160403364.png)]

3、配置win10 maven
  • 配置阿里云镜像

    <mirrors>
    		<mirror>
    		<id>nexus-aliyun</id>
    		<mirrorOf>central</mirrorOf>
    		<name>Nexus aliyun</name>
    		<url>http://maven.aliyun.com/nexus/content/groups/public</url>
    		</mirror>
    	</mirrors>
    
  • 配置 jdk 1.8 编译项目

    <profiles>
    		<profile>
    			<id>jdk-1.8</id>
    			<activation>
    				<activeByDefault>true</activeByDefault>
    				<jdk>1.8</jdk>
    			</activation>
    			<properties>
    				<maven.compiler.source>1.8</maven.compiler.source>
    				<maven.compiler.target>1.8</maven.compiler.target>
    				<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
    			</properties>
    		</profile>
    	</profiles>
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noRl1Do1-1684464779515)(images/谷粒商城项目笔记/image-20220502161154177.png)]

4、安装vscode
  • 下载地址:https://code.visualstudio.com/Download (阿里网盘也有备份)

    • 安装插件

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FEKrngd-1684464779515)(images/谷粒商城项目笔记/image-20220502162914480.png)]

5、配置git信息
  • 安装git (https://git-scm.com/)(阿里网盘有备份:IT技术学习 - gulimall-soft)

  • 配置用户名

    git config --global user.name "liuhandong" #这个不需要和注册时一样
    
  • 配置邮箱

    git config --global user.email "[email protected]" # 注册账号使用的邮箱
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htmFg8L2-1684464779515)(images/谷粒商城项目笔记/image-20220502163225891.png)]

  • 配置 ssh 免密登录

    ssh-keygen -t rsa -C "[email protected]"
    
  • 连点三次回车

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-niFrhzpR-1684464779515)(images/谷粒商城项目笔记/image-20220502164158401.png)]

  • 查看密钥

    cat ~/.ssh/id_rsa.pub
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErhKAlNh-1684464779516)(images/谷粒商城项目笔记/image-20220502164238936.png)]

  • 复制密钥

  • 进入gitee的安全设置,把刚才复制的密钥粘贴到 公钥 框中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KSvm3zbv-1684464779516)(images/谷粒商城项目笔记/image-20220502164358929.png)]

  • 测试该密钥

    ssh -T [email protected]
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ws89M2Lh-1684464779516)(images/谷粒商城项目笔记/image-20220502164714488.png)]

6、初始化gitee

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjs2cjVK-1684464779516)(images/谷粒商城项目笔记/image-20220502164939532.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNphaNbB-1684464779516)(images/谷粒商城项目笔记/image-20220502165236550.png)]

7、填充几大核心服务骨架

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UDBslGaR-1684464779517)(images/谷粒商城项目笔记/image-20220502175101221.png)]

8、初始化数据库
  • 创建数据库(服务器中的数据库,每个服务分别对应一个数据库)

    gulimall_oms   //订单系统
    
    gulimall_pms   //商品系统
    
    gulimall_sms   //sell营销系统
    
    gulimall_ums   //用户系统
    
    gulimall_wms   //库存系统
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJHj9CNc-1684464779517)(images/谷粒商城项目笔记/image-20220502174800320.png)]

  • 分别运行下面gitee中的sql语句

    https://gitee.com/dongHangDongHang/gulimall/tree/master/sql
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3KxOH4SC-1684464779517)(images/谷粒商城项目笔记/image-20220502174252737.png)]

9、逆向工程搭建
  • 打开renren-generator项目

    • 下载地址:https://gitee.com/renrenio/renren-generator.git
  • 修改application.yml中的数据库配置

    • 数据库指向我们想要生成实体类,controller代码的哪个数据库。
  • 修改generator.properties配置文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W84R96N5-1684464779517)(images/谷粒商城项目笔记/image-20220506101644326.png)]

  • 启动该项目

    • 访问localhost:80

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTK2YXy9-1684464779518)(images/谷粒商城项目笔记/image-20220506102339977.png)]

10、MybatisPlus整合
导入依赖
<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.2.0version>
dependency>
配置数据源

配置数据源

  1. 导入数据库驱动

    https://mvnrepository.com/artifact/mysql/mysql-connector-java

     
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>8.0.17version>
            dependency>
    
  2. 在application.yml配置数据源相关信息
    spring:
      datasource:
        username: root
        password: root
        url: jdbc:mysql://192.168.56.10:3306/gulimall_pms
        driver-class-name: com.mysql.jdbc.Driver
    mybatis-plus:
    	# mapper文件扫描
      mapper-locations: classpath*:/mapper/**/*.xml
      global-config:
        db-config:
          id-type: auto # 数据库主键自增
    

配置MyBatis-Plus包扫描:

  1. 使用@MapperScanner

  2. 告诉MyBatis-Plus,Sql映射文件位置

    @MapperScan("com.atguigu.gulimall.product.dao")
    @SpringBootApplication
    public class GulimallProductApplication {
        public static void main(String[] args) {
            SpringApplication.run(GulimallProductApplication.class, args);
        }
    }
    

具体过程参考官网: https://baomidou.com/guide/install.html#release

分页配置
@Configuration // 声明配置类
@EnableTransactionManagement // 开启注解
@MapperScan("com.atguigu.gulimall.product.dao") // 指定扫描包
public class MyBatisConfig {


    /**
     * 引入分页插件 拦截器
     * @return
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
         paginationInterceptor.setLimit(1000);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}
逻辑删除

说明:

只对自动注入的sql起效:

  • 插入: 不作限制
  • 查找: 追加where条件过滤掉已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
  • 更新: 追加where条件防止更新到已删除数据,且使用 wrapper.entity 生成的where条件会忽略该字段
  • 删除: 转变为 更新

例如:

  • 删除: update user set deleted=1 where id = 1 and deleted=0
  • 查找: select id,name,deleted from user where deleted=0

步骤1:配置 application.yml

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto # 数据库主键自增
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

步骤2:实体类字段上加上@TableLogic注解

/**
 * 是否显示[0-不显示,1显示]
 */
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
11、安装postman
  • 下载安装包(阿里云盘:IT技术学习 - gulimall-soft)
  • 安装后账号登陆
9、SpringCloudAlibaba搭建
1、中文文档地址

https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/README-zh.md

nacos标准注册流程:

https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/readme-zh.md

2、版本选择
<spring-boot.version>2.1.8.RELEASEspring-boot.version>
<spring-cloud.version>Greenwich.SR3spring-cloud.version>
3、在common的pom.xml中加入
        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>

# 下面是依赖管理,相当于以后再dependencies里引spring cloud alibaba就不用写版本号, 全用dependencyManagement进行管理
<dependencyManagement>
     <dependencies>
         <dependency>
             <groupId>com.alibaba.cloudgroupId>
             <artifactId>spring-cloud-alibaba-dependenciesartifactId>
             <version>2.1.0.RELEASEversion>
             <type>pomtype>
             <scope>importscope>
         dependency>
     dependencies>
dependencyManagement>
4、下载nacos

地址:https://github.com/alibaba/nacos/releases/tag/1.1.3 (阿里网盘有备份)

5、启动nacos

双击nacos/bin目录下的startup.cmd

5、在服务配置文件中指定注册中心地址
spring:
  application:
    name: gulimall-coupon  # 注意这里的name一定要有,不然注册不进去
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
6、使用 @EnableDiscoveryClient 注解开启服务注册与发现功能
 @SpringBootApplication
 @EnableDiscoveryClient
 public class ProviderApplication {

 	public static void main(String[] args) {
 		SpringApplication.run(ProviderApplication.class, args);
 	}
 }
7、查看注册结果
  • 启动刚才使用了@EnableDiscoveryClient注解的服务

  • 访问:http://localhost:8848/nacos/#/serviceManagement?dataId=&group=&appName=&namespace=

  • 点击 服务列表 ,看到该服务已注册进去。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fvaFRdoZ-1684464779518)(images/谷粒商城项目笔记/image-20220503115337555.png)]

8、使用Feign进行远程调用
  • 给调用者导包(此时他就有了远程调用其他服务的能力)

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
    
  • 在该服务中创建远程调用的哪个服务的接口

    public interface CouponFeignService {
    
    }
    
  • 在该接口前加注解

    @FeignClient("gulimall-coupon")  //里面就是被调用的服务的类目,和nacos上显示的一致
    public interface CouponFeignService {
    
    }
    
  • 把要调用的哪个服务的方法粘过来

    @FeignClient("gulimall-coupon")
    public interface CouponFeignService {
    
        @RequestMapping("/coupon/coupon/member/list")  //这里的路径要写全
        public R membercoupons();
    
    }
    
  • 在该服务中开启远程调用的功能(主启动类前加注解@EnableFeignClients)

    @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")
    @EnableDiscoveryClient
    @SpringBootApplication
    public class GulimallMemberApplication {
        public static void main(String[] args) {
            SpringApplication.run(GulimallMemberApplication.class, args);
        }
    }
    
  • 在controller中注入该类,,并使用

    @Autowired
    private CouponFeignService couponFeignService;
    
    @RequestMapping("/coupons")
    public R test(){
        MemberEntity member = new MemberEntity();
        member.setNickname("张三");
        R membercoupons = couponFeignService.membercoupons();
        return new R().put("member",member).put("coupons",membercoupons.get("coupons"));
    }
    
  • 然后启动这个服务和被调用的服务,并查看nacos上是否注册成功(启动时坑:若IDEA是2021.3版本,则lombok需是最新版)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxEPxHLf-1684464779518)(images/谷粒商城项目笔记/image-20220503145255418.png)]

  • 发起url调用,测试调用结果:http://localhost:8000/member/member/coupons

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BvWtsuPv-1684464779518)(images/谷粒商城项目笔记/image-20220503145533880.png)]

9、使用nacos做分布式配置中心
  • 中文文档:github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md

  • gitee:https://gitee.com/gaoziteng/spring-cloud-alibaba-0221.git

  • 导包(直接放在common的pom中)

    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    dependency>
    
  • 在要使用配置中心的服务中创建bootstrap.properties,配置Nacos Config元数据(该文件优先与application.yml,先被加载)

    spring.application.name=gulimail-coupon
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    
  • 测试读取application.properties中的值

    • application.properties加入如下

      coupon.user.name=jiangsan
      coupon.user.age=15
      
    • CouponController加入如下

      @Value("${coupon.user.name}")
      private String name;
      
      @Value("${coupon.user.age}")
      private Integer age;
      
      @RequestMapping("/test")
      public R test(){
          return R.ok().put("name",name).put("age",age);
      }
      
    • 测试路径:http://localhost:7000/coupon/coupon/test

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6UOwFOuo-1684464779519)(images/谷粒商城项目笔记/image-20220503151651919.png)]

  • 在nacos中添加配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dLUQtRV5-1684464779519)(images/谷粒商城项目笔记/image-20220506124044548.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tVi1Jk9I-1684464779519)(images/谷粒商城项目笔记/image-20220506124201393.png)]

  • 此时重启服务,加载到nacos配置

    http://localhost:7000/coupon/coupon/test

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fGXq5Q4p-1684464779520)(images/谷粒商城项目笔记/image-20220506131542325.png)]

  • 但是此时没有动态刷新

  • 在启动类加@RefreshScope

    @RefreshScope
    @EnableDiscoveryClient
    @SpringBootApplication
    public class Coupon7000 {
        public static void main(String[] args) {
            SpringApplication.run(Coupon7000.class, args);
        }
    }
    
  • 这里保留一个问题,就是我加了注解,但是任然没有动态刷新

  • 后来发现@RefreshScope注解应加在controller上面

    @RefreshScope
    @RestController
    @RequestMapping("coupon/coupon")
    public class CouponController {
    
  • 测试application.yml和application.properties的加载优先级

    • 此时在application.yml加入如下

      coupon:
        user:
          name: 战狼
          age: 27
      
    • 此时application.yml和application.properties中都有,运行http://localhost:7000/coupon/coupon/test

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JwIOqkJA-1684464779520)(images/谷粒商城项目笔记/image-20220503151651919.png)]

    • 发现此时还是application.properties生效

    • 将application.properties中的内容删除,再次运行

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKaYB3PQ-1684464779520)(images/谷粒商城项目笔记/image-20220503152258290.png)]

    • 此时application.yml生效

  • 删除本地的这俩位置配置,并去nacos新建配置(名字是服务名+.properties)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R78UfpXn-1684464779520)(images/谷粒商城项目笔记/image-20220503152649781.png)]

  • 去主启动类,加注解 @RefreshScope

    @RefreshScope   //动态刷新
    @EnableDiscoveryClient
    @SpringBootApplication
    public class GulimallCouponApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GulimallCouponApplication.class, args);
        }
    
    }
    
10、nacos配置中心进阶
10.1、命名空间
  • 命名空间:配置集隔离

    默认public。默认新增的配置都在public空间下
        
    用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 DatalD 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
    
  • 创建几个命名空间

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5FAhtsx-1684464779521)(images/谷粒商城项目笔记/image-20220506135227732.png)]

  • 在prop下也创建gulimall-coupon.properties

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9W14qoCu-1684464779521)(images/谷粒商城项目笔记/image-20220506135607821.png)]

  • 如果希望使用prop下的配置

    • 在bootstrop.properties下

      spring.application.name=gulimail-coupon   
      spring.cloud.nacos.config.server-addr=127.0.0.1:8848
      spring.cloud.nacos.config.namespace=6a6e47e3-f4e3-4605-ad67-b7759e3c1c5f
      
    • namespace的值从如下位置获取

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LmZ1eg24-1684464779521)(images/谷粒商城项目笔记/image-20220506135930145.png)]

    • 此时就使用的是prop下的配置文件了

10.2、配置集
  • 一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。
10.3、配置集ID

类似文件名

10.4、配置分组

如果不写就默认DEFAULT_GROUP

Group

  • 切换配置组的操作

    • 新建配置

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4fb8JUTe-1684464779521)(images/谷粒商城项目笔记/image-20220506141033940.png)]

    • bootstrap.properties加如下配置

      spring.application.name=gulimail-coupon   
      spring.cloud.nacos.config.server-addr=127.0.0.1:8848
      spring.cloud.nacos.config.namespace=6a6e47e3-f4e3-4605-ad67-b7759e3c1c5f
      spring.cloud.nacos.config.group=1111
      
    • 重启服务后生效的就是1111这个组的配置

每个微服务创建自己的命名空间,使用配置分组来区分环境

11.5、同时加载多个配置集
  • 在coupon命名空间下新建配置(存储数据库加载相关配置)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-abqGVqmr-1684464779522)(images/谷粒商城项目笔记/image-20220506142047462.png)]

  • 在coupon命名空间下新建配置(mybatis相关配置)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kjzXcWRh-1684464779522)(images/谷粒商城项目笔记/image-20220506142331778.png)]

  • 其他配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eYPTbey0-1684464779522)(images/谷粒商城项目笔记/image-20220506142553381.png)]

  • 修改bootstrap.properties

    spring.application.name=gulimail-coupon
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=70a3355c-30cf-45c9-8a29-7ebc568cf41a
    #spring.cloud.nacos.config.group=dev
    
    spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
    spring.cloud.nacos.config.ext-config[0].group=dev
    spring.cloud.nacos.config.ext-config[0].refresh=true
    
    spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
    spring.cloud.nacos.config.ext-config[1].group=dev
    spring.cloud.nacos.config.ext-config[1].refresh=true
    
    spring.cloud.nacos.config.ext-config[2].data-id=other.yml
    spring.cloud.nacos.config.ext-config[2].group=dev
    spring.cloud.nacos.config.ext-config[2].refresh=true
    #
    spring.cloud.nacos.config.ext-config[3].data-id=gulimall-coupon.properties
    spring.cloud.nacos.config.ext-config[3].group=dev
    spring.cloud.nacos.config.ext-config[3].refresh=true
    
  • 重启后配置生效

11、Gateway网关
  • 官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

  • 开启网关服务注册发现(注册到nacos)

    @EnableDiscoveryClient
    public class Gateway88 {
        public static void main(String[] args) {
            SpringApplication.run(Gateway88.class, args);
        }
    }
    
  • 配置文件中注册中心和配置中心nacos地址

    spring:
      application:
        name: gulimall-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
          config:
            server-addr: localhost:8848
    
  • 设置配置中心名称空间

    
    
  • 由于网关暂时没有引入数据库的配置,就拍掉相关依赖

    @EnableDiscoveryClient
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class Gateway88 {
        public static void main(String[] args) {
            SpringApplication.run(Gateway88.class, args);
        }
    }
    
  • 设置网关路由

    spring:
      cloud:
        gateway:
          routes:
            - id: test_route
              uri: https://www.baidu.com
              predicates:
                - Query=url,baidu   #url=baidu则进去百度页面
            - id: test_qq
              uri: https://www.qq.com
              predicates:
                - Query=url,qq      #url=qq则进去qq页面
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaV6QONl-1684464779522)(images/谷粒商城项目笔记/image-20220503194841442.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X89A3xFP-1684464779523)(images/谷粒商城项目笔记/image-20220503194900304.png)]

10、运行gateway服务
11、运行renren-fast服务
12、运行renren-fast-vue服务
  • 安装nodejs 10.16.3

    • 下载地址:https://nodejs.org/download/release/v10.16.3/node-v10.16.3-x64.msi

    • 安装完成后检查一下

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2TuImPp-1684464779523)(images\谷粒商城项目笔记\image-20220611174029302.png)]

  • 设置npm的镜像仓库位置

    npm config set registry http://registry.npm.taobao.org/  # 设置node仓库。提高下载速度
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-32ZqihlR-1684464779523)(images\谷粒商城项目笔记\image-20220611174325843.png)]

  • 下载组件

    npm install
    
  • 此时如果出现了异常,可以尝试安装不同版本的nodejs试试(这里使用14.13.0版本的好像比较好使)

  • 运行程序

    npm run dev
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bieH5B0g-1684464779524)(images\谷粒商城项目笔记\image-20220612094318245.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2bjQNQ8-1684464779524)(images\谷粒商城项目笔记\image-20220612094330215.png)]

    • 如果出现了如下异常

      Module build failed: Error: Missing binding D:\Idea_WorkSpace\gulimall\renren-fast-vue\node_modules\node-sass\vendor\win32-x64-83\binding.node Node Sass could not find a binding for your current environment: Windows 64-bit with Node.js 14.x
      
    • 就执行如下代码

      npm i node-sass
      
    • 然后再执行

      npm run dev
      
  • 登录进去(账号密码:admin/admin)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWaTqPTg-1684464779524)(images\谷粒商城项目笔记\image-20220612094432674.png)]

这里使用vscode

启动虚拟机
  • 启动虚拟机
  • 启动里面的redis
7、运行thirdparty服务
8、运行product服务
9、运行ware服务
10、安装elasticsearch、kibana、ik分词器
11、配置nginx

开发

商品服务-三级分类

三级分类添加
  • 新增一级目录

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BT0jwEWb-1684464779525)(images\谷粒商城项目笔记\image-20220612103734072.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYt2vcPm-1684464779526)(images\谷粒商城项目笔记\image-20220612103836424.png)]

    此时刷新页面看到如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNI7jTfY-1684464779526)(images\谷粒商城项目笔记\image-20220612104038960.png)]

    具体添加的系统数据本身在gulimall_admin库里的sys_menu表中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ISfh3VyI-1684464779526)(images\谷粒商城项目笔记\image-20220612104202570.png)]

  • 新增分类维护

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T72muG2l-1684464779527)(images\谷粒商城项目笔记\image-20220612104246007.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-87DcmmaF-1684464779527)(images\谷粒商城项目笔记\image-20220612104410785.png)]

解决跨域问题

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

下图详细说明了 URL 的改变导致是否允许通信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TtiyG380-1684464779527)(images\谷粒商城项目笔记\image-20201017090210286.png)]

跨域流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cdZto4Ng-1684464779527)(images\谷粒商城项目笔记\image-20201017090318165.png)]

浏览器发请求都要实现发送一个请求询问是否可以进行通信 ,我直接给你返回可以通信不就可以了吗?[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGRAjZc6-1684464779528)(/image-20201017090546193.png)]

相关资料参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

解决跨越( 一 ) 使用nginx部署为同一域

开发过于麻烦,上线在使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LN1uPQiy-1684464779528)(images\谷粒商城项目笔记\image-20201017090434369.png)]

解决跨域 ( 二 )配置当次请求允许跨域

1、添加响应头

  • Access-Control-Allow-Origin: 支持哪些来源的请求跨域

  • Access-Control-Allow-Methods: 支持哪些方法跨域

  • Access-Control-Allow-Credentials: 跨域请求默认不包含cookie,设置为true可以包含cookie

  • Access-Control-Expose-Headers: 跨域请求暴露的字段

    ​ CORS请求时, XML .HttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: CacheControl、Content-L anguage、Content Type、Expires、

    Last-Modified、 Pragma。 如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

  • Access-Control-Max- Age: 表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一-请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

网关配置文件

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?>/?.*),/$\{segment}

        - id: admin_route
          uri: lb://renren-fast  # lb负载均衡 到指定的服务
          predicates:
            - Path=/api/**  # path指定对应路径
          filters: # 重写路径
            - RewritePath=/api/(?>/?.*), /renren-fast/$\{segment}

跨越设置

请求先发送到网关,网关在转发给其他服务 事先都要注册到注册中心

@Configuration
public class GulimallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();

        // 配置跨越
        corsConfiguration.addAllowedHeader("*"); // 允许那些头
        corsConfiguration.addAllowedMethod("*"); // 允许那些请求方式
        corsConfiguration.addAllowedOrigin("*"); //  允许请求来源
        corsConfiguration.setAllowCredentials(true); // 是否允许携带cookie跨越
        // 注册跨越配置
        source.registerCorsConfiguration("/**",corsConfiguration);

        return new CorsWebFilter(source);
    }

}
配置中心
  • 新建命名空间

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qQDKeWi4-1684464779528)(images\谷粒商城项目笔记\image-20220613132743147.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sE8s8Gq7-1684464779528)(images\谷粒商城项目笔记\image-20220613132810649.png)]

  • 复制 命名空间ID

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0DUw2zB-1684464779528)(images\谷粒商城项目笔记\image-20220613133056738.png)]

  • 新建 bootstrap.properties

    spring.application.name=gulimall-product
    
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=41bfdf41-f5db-4487-a2a8-b0a3b7c89c26   //刚才新建的命名空间的命名空间ID
    

商品服务&品牌管理

新建品牌管理页

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1M5ccVCn-1684464779529)(images\谷粒商城项目笔记\image-20220613200550385.png)]

阿里云上传
上传模型
  • 上传的账号信息存储在应用服务器
  • 上传先找应用服务器要一个policy上传策略,生成防伪签名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhDiTclS-1684464779529)(images\谷粒商城项目笔记\image-20220613204431958.png)]

和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。

这里我们选择将图片放置到阿里云上,使用对象存储。

阿里云上使使用对象存储方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9dtFQnme-1684464779529)(images/谷粒商城项目笔记/image-20220507213107240.png)]

创建Bucket

地址:https://oss.console.aliyun.com/bucket

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-skpsqLrz-1684464779529)(images/谷粒商城项目笔记/image-20220507213248903.png)]

创建Bucket(作为项目)

上传文件:上传成功后,取得图片的URL

这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。

使用代码上传

查看阿里云关于文件上传的帮助:

https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ

1)添加依赖包

在Maven项目中加入依赖项(推荐方式)

在 Maven 工程中使用 OSS Java SDK,只需在 pom.xml 中加入相应依赖即可。以 3.8.0 版本为例,在 pom内加入如下内容:

<dependency>
    <groupId>com.aliyun.ossgroupId>
    <artifactId>aliyun-sdk-ossartifactId>
    <version>3.8.0version>
dependency>
2)上传文件流

以下代码用于上传文件流:

// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "";
String accessKeySecret = "";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

// 上传文件流。
InputStream inputStream = new FileInputStream("");
ossClient.putObject("", "", inputStream);

// 关闭OSSClient。
ossClient.shutdown();

上面代码的信息可以通过如下查找:

  • endpoint

    • 点击概览:(https://oss.console.aliyun.com/bucket/oss-cn-shanghai/gulimall-handong/overview)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIzXP2Jh-1684464779530)(images\谷粒商城项目笔记\image-20220614092823502.png)]

  • accessKey的获取

    • 点击AccessKey管理:(https://ram.console.aliyun.com/manage/ak?spm=5176.8466032.top-nav.dak.4c861450qNVy5P)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-skanunlj-1684464779530)(images\谷粒商城项目笔记\image-20220614093010116.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QUsangUL-1684464779530)(images\谷粒商城项目笔记\image-20220614093053334.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbZahInm-1684464779530)(images\谷粒商城项目笔记\image-20220614093126163.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oj9Jd4AK-1684464779531)(images\谷粒商城项目笔记\image-20220614093202853.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-McWZjker-1684464779531)(images\谷粒商城项目笔记\image-20220614093221895.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BALnNB3Q-1684464779531)(images\谷粒商城项目笔记\image-20220614093240612.png)]

      [外链图片转存中…(img-oZcI88SQ-1684464779531)]

      accessKeyId accessKeySecret参数上面就是。

  • 添加权限

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jSntkYPw-1684464779531)(images\谷粒商城项目笔记\image-20220614093533817.png)]
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GwlwXkVX-1684464779532)(images\谷粒商城项目笔记\image-20220614093629762.png)]
  • 图片的路径:比如,“D:\User\zzpic15479.jpg”

    • https://oss.console.aliyun.com/bucket
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oAMNaYFZ-1684464779532)(images\谷粒商城项目笔记\image-20220614094517968.png)]
  • 上传后的文件名,自己取,比如:15479.jpg

  • 运行此程序

  • 查看上传的图片

    • https://oss.console.aliyun.com/bucket
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MD8Ct8d9-1684464779532)(images\谷粒商城项目笔记\image-20220614095418490.png)]
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEQj4nFE-1684464779532)(images\谷粒商城项目笔记\image-20220614095432736.png)]
3)使用SpringCloud Alibaba管理oss
  • 接入OSS

    • 导包(把上面导的那个包先注掉)(这个包放在common里面)

      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-starter-alicloud-ossartifactId>
      dependency>
      
    • 配置文件

      access-key: LTAI4G4W1RA4JXz2QhoDwHhi
        secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw
        oss:
          endpoint: oss-cn-shanghai.aliyuncs.com
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YomwQPDA-1684464779533)(images\谷粒商城项目笔记\image-20220614101038976.png)]

    • 注入如下

      @Autowired
      OSSClient ossClient;
      
    • 完整代码

      @Autowired
      OSSClient ossClient;
      
      @Test
      public void upload() throws FileNotFoundException {
          // 上传文件流。
          InputStream inputStream = new FileInputStream("D:\\User\\dahai.jpg");
          ossClient.putObject("gulimall-handong", "dahai.jpg", inputStream);
          // 关闭OSSClient。
          ossClient.shutdown();
          System.out.println("上传成功...");
      }
      
服务端签名后直传实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QgT8gyI-1684464779533)(images\谷粒商城项目笔记\image-20220614101941089.png)]

创建第三方服务模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fsN0vjpG-1684464779533)(images\谷粒商城项目笔记\image-20220614102058519.png)]

  • 导包

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    </dependency>
    
  • 配置文件

    alicloud:
        access-key: LTAI5tSGcmqPbjBm7nXWtwuQ
        secret-key: 0et8rK9M4M6gPzIht34TZKIawBpQ9k
        oss:
            endpoint: oss-cn-shanghai.aliyuncs.com
            bucket: gulimall-handong
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JheiXJGV-1684464779534)(images\谷粒商城项目笔记\image-20220614102509609.png)]

  • 创建新的名称空间

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ETpH0me-1684464779534)(images\谷粒商城项目笔记\image-20220614102710350.png)]

  • 新建配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVI2QIAK-1684464779534)(images\谷粒商城项目笔记\image-20220614103039069.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vfiulFOX-1684464779534)(images\谷粒商城项目笔记\image-20220614103149785.png)]

    spring:
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
        alicloud:
          access-key: LTAI5tSGcmqPbjBm7nXWtwuQ
          secret-key: 0et8rK9M4M6gPzIht34TZKIawBpQ9k
          oss:
            endpoint: oss-cn-shanghai.aliyuncs.com
            bucket: gulimall-handong
    
  • bootstrap.properties

    spring.application.name=gulimall-third-party
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=796ec540-3ef8-472b-927f-dc8bff7f60cf
    
    spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
    spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
    spring.cloud.nacos.config.ext-config[0].refresh=true
    
  • 启动该服务

官方文档:

前面后直传
  • 官方文档:https://help.aliyun.com/document_detail/31926.html?spm=5176.12818093.help.dexternal.4d1d16d0KcuZ0x
  • 配置跨域
    • https://oss.console.aliyun.com/bucket/oss-cn-shanghai/gulimall-handong/overview
    • [外链图片转存中…(img-wUHclvBt-1684464779535)]
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wNESapcz-1684464779535)(images\谷粒商城项目笔记\image-20220614110538671.png)]
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iieWszFC-1684464779535)(images\谷粒商城项目笔记\image-20220614110609815.png)]
JSR303校验

问题引入:填写form时应该有前端校验,后端也应该有校验
前端
前端的校验是element-ui表单验证
Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。

后端

@NotNull等

步骤1:使用校验注解

在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>

里面依赖了hibernate-validator
在非空处理方式上提供了@NotNull,@NotBlank和@NotEmpty
1 @NotNull

The annotated element must not be null. Accepts any type.
注解元素禁止为null,能够接收任何类型

2 @NotEmpty

the annotated element must not be null nor empty.

该注解修饰的字段不能为null或""

Supported types are:

支持以下几种类型

CharSequence (length of character sequence is evaluated)字符序列(字符序列长度的计算)
Collection (collection size is evaluated)
集合长度的计算
Map (map size is evaluated)
map长度的计算
Array (array length is evaluated)
数组长度的计算
3 @NotBlank

The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
该注解不能为null,并且至少包含一个非空格字符。接收字符序列。

@Valid

步骤2:controller中加校验注解@Valid,开启校验
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
    brandService.save(brand);
    return R.ok();
}

测试: http://localhost:88/api/product/brand/save

在postman种发送上面的请求

{
    "timestamp": "2020-04-29T09:20:46.383+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.brandEntity.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "brandEntity.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "不能为空",
            "objectName": "brandEntity",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='brandEntity'. Error count: 1",
    "path": "/product/brand/save"
}

能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:

javax.validation.constraints.AssertFalse.message     = 只能为false
javax.validation.constraints.AssertTrue.message      = 只能为true
javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}
javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message          = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message             = 最大不能超过{value}
javax.validation.constraints.Min.message             = 最小不能小于{value}
javax.validation.constraints.Negative.message        = 必须是负数
javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零
javax.validation.constraints.NotBlank.message        = 不能为空
javax.validation.constraints.NotEmpty.message        = 不能为空
javax.validation.constraints.NotNull.message         = 不能为null
javax.validation.constraints.Null.message            = 必须为null
javax.validation.constraints.Past.message            = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message        = 必须是正数
javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零
javax.validation.constraints.Size.message            = 个数必须在{min}和{max}之间

org.hibernate.validator.constraints.CreditCardNumber.message        = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message                = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message                     = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message                   = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message                  = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message         = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message                = 不能为空
org.hibernate.validator.constraints.NotEmpty.message                = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message  = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message                   = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message                = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message            = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message                     = 需要是一个合法的URL

org.hibernate.validator.constraints.time.DurationMax.message        = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message        = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是

public @interface NotBlank {
	String message() default "{javax.validation.constraints.NotBlank.message}";
}

可以在添加注解的时候,修改message:

@NotBlank(message = "品牌名必须非空")
private String name;

当再次发送请求时,得到的错误提示信息:

{
    "timestamp": "2020-04-29T09:36:04.125+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.brandEntity.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "brandEntity.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "品牌名必须非空",
            "objectName": "brandEntity",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='brandEntity'. Error count: 1",
    "path": "/product/brand/save"
}

但是这种返回的错误结果并不符合我们的业务需要。

BindResult

步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
    if( result.hasErrors()){
        Map<String,String> map=new HashMap<>();
        //1.获取错误的校验结果
        result.getFieldErrors().forEach((item)->{
            //获取发生错误时的message
            String message = item.getDefaultMessage();
            //获取发生错误的字段
            String field = item.getField();
            map.put(field,message);
        });
        return R.error(400,"提交的数据不合法").put("data",map);
    }else {
        
    }
    brandService.save(brand);
    return R.ok();
}

这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。

统一异常处理@ControllerAdvice

步骤4:统一异常处理

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
1 抽取一个异常处理类

@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
    public R handleValidException(MethodArgumentNotValidException exception){
    
        Map<String,String> map=new HashMap<>();
        // 获取数据校验的错误结果
        BindingResult bindingResult = exception.getBindingResult();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            String message = fieldError.getDefaultMessage();
            String field = fieldError.getField();
            map.put(field,message);
        });
    
        log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
    
        return R.error(400,"数据校验出现问题").put("data",map);
    }

}

2 测试: http://localhost:88/api/product/brand/save

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hud4EvGM-1684464779536)(images/谷粒商城项目笔记/image-20220508124513443.png)]

在这里插入图片描述

3 默认异常处理

@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
    log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
    return R.error(400,"数据校验出现问题");
}

4 错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程
中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

package com.atguigu.common.exception;

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnum {

    UNKNOW_EXEPTION(10000,"系统未知异常"),

    VALID_EXCEPTION( 10001,"参数格式校验失败");

    private int code;
    private String msg;

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

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vfgN0PDi-1684464779536)(images/谷粒商城项目笔记/image-20220508124544452.png)]

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

5 测试: http://localhost:88/api/product/brand/save

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeyuLXga-1684464779536)(images/谷粒商城项目笔记/image-20220508124558551.png)]

在这里插入图片描述
1.5 分组校验功能(完成多场景的复杂校验)

1 groups
1 给校验注解,标注上groups,指定什么情况下才需要进行校验
groups里面的内容要以接口的形式显示出来
如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id

@NotNull(message = "修改必须定制品牌id", groups = {UpdateGroup.class})
@Null(message = "新增不能指定id", groups = {AddGroup.class})
@TableId
private Long brandId;

在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

2 @Validated
2 业务方法参数上使用@Validated注解

@Validated的value方法:

Specify one or more validation groups to apply to the validation step kicked off by this annotation.
指定一个或多个验证组以应用于此注释启动的验证步骤。

JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using
them as type-safe group arguments, as implemented in SpringValidatorAdapter.

JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。

Other SmartValidator implementations may support class arguments in other ways as well.

其他SmartValidator 实现也可以以其他方式支持类参数。

@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
    brandService.save(brand);

    return R.ok();

}
@RequestMapping("/delete")
//@RequiresPermissions("${moduleNamez}:brand:delete")
public R delete(@RequestBody Long[] brandIds) {
    brandService.removeByIds(Arrays.asList(brandIds));

    return R.ok();

}

3 分组情况下,校验注解生效问题
3 默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

1.6 自定义校验功能

场景:要校验showStatus的01状态,可以用正则,但我们可以利用其他方式解决
复杂场景。比如我们想要下面的场景

/**

  * 显示状态[0-不显示;1-显示]
    */
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals = {0,1}, groups = {AddGroup.class, UpdateGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;

如何做:

添加依赖

<dependency>
    <groupId>javax.validationgroupId>
    <artifactId>validation-apiartifactId>
    <version>2.0.1.Finalversion>
dependency>

1 编写自定义的校验注解
必须有3个属性

message()错误信息
groups()分组校验
payload()自定义负载信息

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 使用该属性去Validation.properties中取
    String message() default "{com.atguigu.common.valid.ListValue.message}";

    Class<?>[] groups() default { };
    
    Class<? extends Payload>[] payload() default { };
    
    int[] value() default {};

}

该属性值取哪里取呢?
common创建文件ValidationMessages.properties
里面写上com.atguigu.common.valid.ListValue.message=必须提交指定的值 [0,1]

2 编写自定义的校验器

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set=new HashSet<>();
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.value();
        for (int i : value) {
            set.add(i);
        }

    }
    
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {


        return  set.contains(value);
    }

}

3 关联校验器和校验注解

@Constraint(validatedBy = { ListValueConstraintValidator.class})

一个校验注解可以匹配多个校验器

4 使用实例

/**

  * 显示状态[0-不显示;1-显示]
    */
    @ListValue(value = {0,1},groups ={AddGroup.class})
    private Integer showStatus;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LvkZ8gN-1684464779537)(images/谷粒商城项目笔记/image-20220508115031123.png)]

商品基本概念
SPU 和 SKU

SPU:Standard Product Unit (标准化产品单元)

是商品信息聚合的最小单位,是一组可复用,易检索的标准化信息的组合,该集合描述了一个产品的特性

IPhoneX 是 SPU,MI8 是 SPU

IPhoneX 64G 黑曜石 是 SKU

MIX8 + 64G 是 SKU

SKU: Stock KeepingUnit (库存量单位)

基本属性 【规格参数】与 销售属性

每个分共下的商共享规格参数、与销售属性,只是有些商品不一定更用这个分类下全部的属性:

属性是以三级分类组织起来的

规格参数中有些是可以提供检索的

规格参数也是基本属性,他们具有自己的分组

属性的分组也是以三级分类组织起来的

属性名确定的,但是值是每一个商品不同来决定的

【属性分组-规格参数-销售属性-三级分类】关联关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ksOi5AXe-1684464779537)(images/谷粒商城项目笔记/image-20220614174042452.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWxbSi0c-1684464779537)(images/谷粒商城项目笔记/image-20220614174057934.png)]

属性分组

接口文档:https://easydoc.net/s/78237135/ZUqEdvA4/HqQGp9TI

设置隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

ElasticSearch

索引:库

类型:表

数据是json格式

文档:就相当于表中一列列数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYTfQvho-1684464779537)(images/谷粒商城项目笔记/image-20220510123223174.png)]

倒排索引

1、安装ElasticSearch
  • 下载镜像文件

    docker pull elasticsearch:7.4.2
    docker pull kibana:7.4.2
    
  • 本地创建两个文件

    mkdir -p /mydata/elasticsearch/config    //配置文件信息挂载到这个文件夹下
    mkdir -p /mydata/elasticsearch/data      //
    
  • 修改配置文件

    echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
    
  • 给文件夹添加权限

    chmod -R 777 /mydata/elasticsearch/
    
  • 运行镜像

    docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
    -e "discovery.type=single-node" \
    -e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
    -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
    -v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
    -v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
    -d elasticsearch:7.4.2
    
  • 设置开机自启

    docker update elasticsearch --restart=always
    
  • 启动起来后查看该容器的日志

    docker logs elasticsearch
    或者
    docker logs [容器id]
    或者
    docker logs [容器id前三位]
    
  • 测试

    • 地址:124.222.248.51:9200

    • 出现如下界面表示安装成功

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YyaaO0vy-1684464779538)(images/谷粒商城项目笔记/image-20220510134053961.png)]

  • 特别注意:

    -e ES_JAVA_OPTS="-Xms64m -Xmx256m" \ 测试环境下,设置 ES 的初始内存和最大内存,否则导致过大启动不了 ES
    
2、启动Kibana
  • 运行镜像

    docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.91.100:9200 -p 5601:5601 \
    -d kibana:7.4.2
    
    // 注意如果用的是云服务器,并且es和kibana在一台机器,则使用如下命令找到ip地址
    docker inspect elasticsearch | grep IPAddress
    // 并把ip地址放在host后面
    // 一般是172.17.0.3
    
    // http://192.168.91.100:9200 一定改为自己虚拟机的地址
    
  • 启动完成后等待一会儿,或许几秒,或许几分钟

  • 设置开机自启

    docker update kibana --restart=always
    
  • 测试

    • 测试地址:124.222.248.51:5601

    • 出现如下则成功

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbRFit6I-1684464779538)(images/谷粒商城项目笔记/image-20220510141748528.png)]

  • 初始化es

    PUT gulimall_product
    {
      "mappings":{
        "properties":{
          "skuId":{
            "type":"long"
          },
           "spuId":{
            "type":"keyword"
          },
           "skuTitle":{
            "type":"text",
            "analyzer": "ik_smart"
          },
           "skuPrice":{
            "type":"keyword"
          },
           "skuImg":{
            "type":"text",
            "analyzer": "ik_smart"
          },
           "saleCount":{
            "type":"long"
          },
           "hasStock":{
            "type":"boolean"
          },
          "hotScore":{
            "type":"long"
          },
          "brandId":{
            "type":"long"
          },
          "catelogId":{
            "type":"long"
          },
          "brandName":{
            "type":"keyword",
            "index": false,
            "doc_values": false
          },
          "brandImg":{
            "type":"keyword",
             "index": false,
            "doc_values": false
          },
          "catalogName":{
            "type":"keyword",
             "index": false,
             "doc_values": false
          },
          "attrs":{
            "type":"nested",
            "properties": {
              "attrId":{
                "type":"long"
              },
              "attrName":{
                "type":"keyword",
                "index":false,
                "doc_values":false
              },
              "attrValue": {
                "type":"keyword"
              }
            }
          }
        }
      }
    }
    
3、初步检索
1、_cat
GET /_cat/nodes:查看所有节点  实例:http://124.222.248.51:9200/_cat/nodes
GET /_cat/health:查看 es 健康状况  http://124.222.248.51:9200/_cat/health
GET /_cat/master:查看主节点 
GET /_cat/indices:查看所有索引  相当于show databases;
2、索引一个文档

即保存一条数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识。

1. PUT 请求

接口:PUT http://192.168.163.131:9200/customer/external/1

参数解释:192.168.131:9200/索引名(库名)/类型名(表名)/唯一标识id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgA5udXF-1684464779538)(images/谷粒商城项目笔记/1615298924150-ef6c809b-eda8-41cc-8eba-2156ec376cb5.png)]

2. POST 请求

接口:POST http://192.168.163.131:9200/customer/external/

如果不带id则是新增,带了Id,如果有就是更新,没有就是新增。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6Sdxa8m-1684464779538)(images/谷粒商城项目笔记/1615299124434-93f69d46-00f1-423e-b795-313bb1d2f3c9.png)]

PUT和POST都可以

  • POST新增,如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号;
  • PUT可以新增也可以修改。PUT必须指定id;由于PUT需要指定id,我们一般用来做修改操作,不指定id会报错。
3、查看文档

/index/type/id

接口:GET http://192.168.163.131:9200/customer/external/1

[外链图片转存中…(img-D8uK1b4u-1684464779539)]

{
    "_index": "customer",  # 在哪个索引()
    "_type": "external",   # 在哪个类型()
    "_id": "1",						 # 文档id(记录)
    "_version": 5,				 # 版本号
    "_seq_no": 4,					 # 并发控制字段,每次更新都会+1,用来做乐观锁
    "_primary_term": 1,		 # 同上,主分片重新分配,如重启,就会变化
    "found": true,
    "_source": {					 # 数据
        "name": "zhangsan"
    }
}

# 乐观锁更新时携带 ?_seq_no=0&_primary_term=1  当携带数据与实际值不匹配时更新失败
更新文档
/index/type/id/_update

接口:POST http://192.168.163.131:9200/customer/external/1/_update

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DO8VvAab-1684464779539)(images/谷粒商城项目笔记/1615345823323-a4869fd9-d54d-461a-bcf2-77046a9f1970.png)]

几种更新文档的区别

在上面索引文档即保存文档的时候介绍,还有两种更新文档的方式:

  • 当PUT请求带id,且有该id数据存在时,会更新文档;
  • 当POST请求带id,与PUT相同,该id数据已经存在时,会更新文档;

这两种请求类似,即带id,且数据存在,就会执行更新操作。

类比:

  • 请求体的报文格式不同,_update方式要修改的数据要包裹在 doc 键下
  • _update方式不会重复更新,数据已存在不会更新,版本号不会改变,另两种方式会重复更新(覆盖原来数据),版本号会改变
  • 这几种方式在更新时都可以增加属性,PUT请求带id更新和POST请求带id更新,会直接覆盖原来的数据,不会在原来的属性里面新增属性
删除文档&索引
删除文档

接口:DELETE http://192.168.163.131:9200/customer/external/1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J2rZPb0l-1684464779539)(images/谷粒商城项目笔记/1615355451681-013af65f-aa49-43bb-94e9-1e6fdad42791.png)]

删除索引

接口:DELETE http://192.168.163.131:9200/customer

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P4klT6GB-1684464779540)(images/谷粒商城项目笔记/1615355541689-70478b27-b1ec-4c06-a16c-f23b5b6552f1.png)]

bulk-批量操作数据

语法格式:

{action:{metadata}}\n   // 例如index保存记录,update更新
{request body  }\n

{action:{metadata}}\n
{request body  }\n
1. 指定索引和类型的批量操作

接口:POST /customer/external/_bulk

参数:

{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}

在Kibana中使用dev-tools测试批量:

[外链图片转存中…(img-6mFM23Z5-1684464779540)]

2. 对所有索引执行批量操作

接口:POST /_bulk

参数:

{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}
{"title":"my first blog post"}
{"index":{"_index":"website","_type":"blog"}}
{"title":"my second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"my updated blog post"}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFGHG052-1684464779540)(images/谷粒商城项目笔记/1615356270583-8b578bba-5ffb-4e69-85b9-8d083c1958a2.png)]

  • 这里的批量操作,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。
  • bulk api以此按顺序执行所有的action(动作)。如果一个单个的动作因任何原因失败,它将继续处理它后面剩余的动作。
  • 当bulk api返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查是否一个指定的动作是否失败了。
批量创建数据

官方测试数据地址:https://gitee.com/xlh_blog/common_content/blob/master/es%E6%B5%8B%E8%AF%95%E6%95%B0%E6%8D%AE.json

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upmqozj7-1684464779540)(images/谷粒商城项目笔记/image-20220510151413430.png)]

4、进阶检索
检索示例介绍

下面的请求都是在Kibana dev-tools 操作

请求接口
GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "account_number": "asc"
    }
  ]
}
# query 查询条件
# sort 排序条件
结果
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "0",
        "_score" : null,
        "_source" : {
          "account_number" : 0,
          "balance" : 16623,
          "firstname" : "Bradshaw",
          "lastname" : "Mckenzie",
          "age" : 29,
          "gender" : "F",
          "address" : "244 Columbus Place",
          "employer" : "Euron",
          "email" : "[email protected]",
          "city" : "Hobucken",
          "state" : "CO"
        },
        "sort" : [
          0
        ]
      },
      ...
    ]
  }
}
响应字段解释
  • took – how long it took Elasticsearch to run the query, in milliseconds
  • timed_out – whether or not the search request timed out
  • _shards – how many shards were searched and a breakdown of how many shards succeeded, failed, or were skipped.
  • max_score – the score of the most relevant document found
  • hits.total.value - how many matching documents were found
  • hits.sort - the document’s sort position (when not sorting by relevance score)
  • hits._score - the document’s relevance score (not applicable when using match_all)
响应结果说明

Elasticsearch 默认会分页返回10条数据,不会一下返回所有数据。

请求方式说明

ES支持两种基本方式检索;

  • 通过REST request uri 发送搜索参数 (uri +检索参数);
  • 通过REST request body 来发送它们(uri+请求体);

也就是说除了上面示例的请求接口,根据请求体进行检索外;

还可以用GET请求参数的方式检索:

GET bank/_search?q=*&sort=account_number:asc
# q=* 查询所有
# sort=account_number:asc 按照account_number进行升序排列
Query DSL

本小节参考官方文档:Query DSL

Elasticsearch提供了一个可以执行查询的Json风格的DSL。这个被称为Query DSL,该查询语言非常全面。

1. 基本语法格式

一个查询语句的典型结构:

QUERY_NAME:{
   ARGUMENT:VALUE,
   ARGUMENT:VALUE,...
}

如果针对于某个字段,那么它的结构如下:

{
  QUERY_NAME:{
     FIELD_NAME:{
       ARGUMENT:VALUE,
       ARGUMENT:VALUE,...
      }   
   }
}

请求示例:

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "account_number": {
        "order": "desc"
      },
      "balance": {
      	"order": "asc"
      }
    }
  ]
}

# match_all 查询类型【代表查询所有的所有】,es中可以在query中组合非常多的查询类型完成复杂查询;
# from+size 限定,完成分页功能;从第几条数据开始,每页有多少数据
# sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;
2. 返回部分字段

请求示例:

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "account_number": {
        "order": "desc"
      }
    }
  ],
  "_source": ["balance","firstname"]
}

# _source 指定返回结果中包含的字段名

结果示例:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "bank",
        "_type" : "account",
        "_id" : "999",
        "_score" : null,
        "_source" : {
          "firstname" : "Dorothy",
          "balance" : 6087
        },
        "sort" : [
          999
        ]
      },
    	...
    ]
 	}
}
3. match-匹配查询

精确查询-基本数据类型(非文本)

GET bank/_search
{
  "query": {
    "match": {
      "account_number": 20
    }
  }
}
# 查找匹配 account_number 为 20 的数据 非文本推荐使用 term

模糊查询-文本字符串

GET bank/_search
{
  "query": {
    "match": {
      "address": "mill lane"
    }
  }
}
# 查找匹配 address 包含 mill 或 lane 的数据

match即全文检索,对检索字段进行分词匹配,会按照响应的评分 _score 排序,原理是倒排索引。

精确匹配-文本字符串

GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "288 Mill Street"
    }
  }
}
# 查找 address 为 288 Mill Street 的数据。
# 这里的查找是精确查找,只有完全匹配时才会查找出存在的记录,
# 如果想模糊查询应该使用match_phrase 短语匹配
4. match_phrase-短语匹配

将需要匹配的值当成一整个单词(不分词)进行检索

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill lane"
    }
  }
}
# 这里会检索 address 匹配包含短语 mill lane 的数据
5. multi_math-多字段匹配
GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "mill",
      "fields": [
        "city",
        "address"
      ]
    }
  }
}
# 检索 city 或 address 匹配包含 mill 的数据,会对查询条件分词
6. bool-复合查询

复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间

可以互相嵌套,可以表达非常复杂的逻辑。

  • must:必须达到must所列举的所有条件
  • must_not,必须不匹配must_not所列举的所有条件。
  • should,应该满足should所列举的条件。
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ]
    }
  }
}
# 查询 gender 为 M 且 address 包含 mill 的数据
7. filter-结果过滤

并不是所有的查询都需要产生分数,特别是哪些仅用于filtering过滤的文档。为了不计算分数,elasticsearch会自动检查场景并且优化查询的执行。

filter 对结果进行过滤,且不计算相关性得分。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "filter": {
        "range": {
          "balance": {
            "gte": "10000",
            "lte": "20000"
          }
        }
      }
    }
  }
}
# 这里先是查询所有匹配 address 包含 mill 的文档,
# 然后再根据 10000<=balance<=20000 进行过滤查询结果

Each must, should, and must_not element in a Boolean query is referred to as a query clause. How well a document meets the criteria in each must or should clause contributes to the document’s relevance score. The higher the score, the better the document matches your search criteria. By default, Elasticsearch returns documents ranked by these relevance scores.

在boolean查询中,must, shouldmust_not 元素都被称为查询子句 。 文档是否符合每个“must”或“should”子句中的标准,决定了文档的“相关性得分”。 得分越高,文档越符合您的搜索条件。 默认情况下,Elasticsearch 返回根据这些相关性得分排序的文档。

The criteria in a must_not clause is treated as a filter. It affects whether or not the document is included in the results, but does not contribute to how documents are scored. You can also explicitly specify arbitrary filters to include or exclude documents based on structured data.

“must_not”子句中的条件被视为“过滤器”。 它影响文档是否包含在结果中,但不影响文档的评分方式。还可以显式地指定任意过滤器来包含或排除基于结构化数据的文档。

8. term-精确检索

Avoid using the term query for text fields.

避免使用 term 查询文本字段

By default, Elasticsearch changes the values of text fields as part of analysis. This can make finding exact matches for text field values difficult.

默认情况下,Elasticsearch 会通过analysis分词将文本字段的值拆分为一部分,这使精确匹配文本字段的值变得困难。

To search text field values, use the match query instead.

如果要查询文本字段值,请使用 match 查询代替。

https://www.elastic.co/guide/en/elasticsearch/reference/7.11/query-dsl-term-query.html

在上面3.match-匹配查询中有介绍对于非文本字段的精确查询,Elasticsearch 官方对于这种非文本字段,使用 term来精确检索是一个推荐的选择。

GET bank/_search
{
  "query": {	
    "term": {
      "age": "28"
    }
  }
}
# 查找 age 为 28 的数据
9. Aggregation-执行聚合

https://www.elastic.co/guide/en/elasticsearch/reference/7.11/search-aggregations.html

聚合语法

GET /my-index-000001/_search
{
  "aggs":{
    "aggs_name":{ # 这次聚合的名字,方便展示在结果集中
        "AGG_TYPE":{ # 聚合的类型(avg,term,terms)
        }	
     }
	}
}
示例1-搜索address中包含mill的所有人的年龄分布以及平均年龄
GET bank/_search
{
  "query": {
    "match": {
      "address": "Mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    },
    "balanceAvg": {
      "avg": {
        "field": "balance"
      }
    }
  },
  "size": 0
}
# "ageAgg": {   				  --- 聚合名为 ageAgg
#   "terms": {				    --- 聚合类型为 term
#     "field": "age",     --- 聚合字段为 age
#     "size": 10			    --- 取聚合后前十个数据
#   }
# },
# ------------------------
# "ageAvg": {   				  --- 聚合名为 ageAvg
#   "avg": {				      --- 聚合类型为 avg 求平均值
#     "field": "age"	    --- 聚合字段为 age
#   }
# },
# ------------------------
# "balanceAvg": {				  --- 聚合名为 balanceAvg
#   "avg": {				      --- 聚合类型为 avg 求平均值
#     "field": "balance"  --- 聚合字段为 balance
#   }
# }
# ------------------------
# "size": 0               --- 不显示命中结果,只看聚合信息

结果:

{
  "took" : 10,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "ageAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 38,
          "doc_count" : 2
        },
        {
          "key" : 28,
          "doc_count" : 1
        },
        {
          "key" : 32,
          "doc_count" : 1
        }
      ]
    },
    "ageAvg" : {
      "value" : 34.0
    },
    "balanceAvg" : {
      "value" : 25208.0
    }
  }
}
示例2-按照年龄聚合,并且求这些年龄段的这些人的平均薪资
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "ageAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

结果:

{
  "took" : 12,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "ageAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 31,
          "doc_count" : 61,
          "ageAvg" : {
            "value" : 28312.918032786885
          }
        },
        {
          "key" : 39,
          "doc_count" : 60,
          "ageAvg" : {
            "value" : 25269.583333333332
          }
        },
        {
          "key" : 26,
          "doc_count" : 59,
          "ageAvg" : {
            "value" : 23194.813559322032
          }
        },
        {
          "key" : 32,
          "doc_count" : 52,
          "ageAvg" : {
            "value" : 23951.346153846152
          }
        },
        {
          "key" : 35,
          "doc_count" : 52,
          "ageAvg" : {
            "value" : 22136.69230769231
          }
        },
        {
          "key" : 36,
          "doc_count" : 52,
          "ageAvg" : {
            "value" : 22174.71153846154
          }
        },
        {
          "key" : 22,
          "doc_count" : 51,
          "ageAvg" : {
            "value" : 24731.07843137255
          }
        },
        {
          "key" : 28,
          "doc_count" : 51,
          "ageAvg" : {
            "value" : 28273.882352941175
          }
        },
        {
          "key" : 33,
          "doc_count" : 50,
          "ageAvg" : {
            "value" : 25093.94
          }
        },
        {
          "key" : 34,
          "doc_count" : 49,
          "ageAvg" : {
            "value" : 26809.95918367347
          }
        },
        {
          "key" : 30,
          "doc_count" : 47,
          "ageAvg" : {
            "value" : 22841.106382978724
          }
        },
        {
          "key" : 21,
          "doc_count" : 46,
          "ageAvg" : {
            "value" : 26981.434782608696
          }
        },
        {
          "key" : 40,
          "doc_count" : 45,
          "ageAvg" : {
            "value" : 27183.17777777778
          }
        },
        {
          "key" : 20,
          "doc_count" : 44,
          "ageAvg" : {
            "value" : 27741.227272727272
          }
        },
        {
          "key" : 23,
          "doc_count" : 42,
          "ageAvg" : {
            "value" : 27314.214285714286
          }
        },
        {
          "key" : 24,
          "doc_count" : 42,
          "ageAvg" : {
            "value" : 28519.04761904762
          }
        },
        {
          "key" : 25,
          "doc_count" : 42,
          "ageAvg" : {
            "value" : 27445.214285714286
          }
        },
        {
          "key" : 37,
          "doc_count" : 42,
          "ageAvg" : {
            "value" : 27022.261904761905
          }
        },
        {
          "key" : 27,
          "doc_count" : 39,
          "ageAvg" : {
            "value" : 21471.871794871793
          }
        },
        {
          "key" : 38,
          "doc_count" : 39,
          "ageAvg" : {
            "value" : 26187.17948717949
          }
        },
        {
          "key" : 29,
          "doc_count" : 35,
          "ageAvg" : {
            "value" : 29483.14285714286
          }
        }
      ]
    }
  }
}
示例3-查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword"
          },
          "aggs": {
            "balanceAvg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBalanceAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}
# "field": "gender.keyword" gender是txt没法聚合 必须加.keyword精确替代

结果:

{
  "took" : 17,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "ageAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 31,
          "doc_count" : 61,
          "genderAgg" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "M",
                "doc_count" : 35,
                "balanceAvg" : {
                  "value" : 29565.628571428573
                }
              },
              {
                "key" : "F",
                "doc_count" : 26,
                "balanceAvg" : {
                  "value" : 26626.576923076922
                }
              }
            ]
          },
          "ageBalanceAvg" : {
            "value" : 28312.918032786885
          }
        },
        {
          "key" : 39,
          "doc_count" : 60,
          "genderAgg" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "F",
                "doc_count" : 38,
                "balanceAvg" : {
                  "value" : 26348.684210526317
                }
              },
              {
                "key" : "M",
                "doc_count" : 22,
                "balanceAvg" : {
                  "value" : 23405.68181818182
                }
              }
            ]
          },
          "ageBalanceAvg" : {
            "value" : 25269.583333333332
          }
        },
        ...
      ]
    }
  }
}
Mapping
1、字段类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DAaHEs74-1684464779541)(images/谷粒商城项目笔记/image-20201026074813810.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4l1gMLil-1684464779541)(images/谷粒商城项目笔记/image-20201026074841875.png)]

2、映射

Mapping(映射)

Mapping 是用来定义一个文档(document),以及他所包含的属性(field)是如何存储索引的,比如使用 mapping来定义的:

  • 哪些字符串属性应该被看做全文本属性(full text fields)
  • 那些属性包含数字,日期或者地理位置
  • 文档中的所有属性是能被索引(_all 配置)
  • 日期的格式
  • 自定义映射规则来执行动态添加属性

查看 mapping 信息

GET bank/_mapping

修改 mapping 信息

https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html

自动猜测的映射类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ozAZ1WVd-1684464779542)(images/谷粒商城项目笔记/image-20201026075424198.png)]

3、新版本改变
  • 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch 是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed 最终在Lucene,中的处理方式是一样的。
  • 两个不同 type下的两个user_ name, 在ES同-个索引下其实被认为是同一一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同typpe中的相同字段称就会在处理中出现神突的情况,导致Lucene处理效率下降。
  • 去掉type就是为了提高ES处理数据的效率。

ES 7.x

URL 中的 type 参数 可选,比如索引一个文档不再要求提供文档类型

ES 8.X

不在支持 URL 中的 type 参数

解决:

1、将索引从多类型迁移到单类型,每种类型文档一个独立的索引

2、将已存在的索引下的类型数据,全部迁移到指定位置即可,详见数据迁移

1、创建映射

PUT /my_index
{
  "mappings":{
    "properties": {
      "age":{"type":"integer"},
      "email":{"type":"keyword"}
    }
  }
}

2、添加新的字段映射

PUT /my_index/_mapping
{
  "properties":{
    "employeeid":{
      "type":"keyword",
      "index":false
    }
  }
}

3、更新映射

对于已经存在的映射字段,我们不能更新,更新必须创建新的索引进行数据迁移

4、数据迁移

先创 new_twitter 的正确映射,然乎使用如下方式进行数据迁移

POST _reindex [固定写法]
{
  "source":{
    "index":"twitter"
  },
  "dest":{
    "index":"new_twitter"
  }
}
## 将旧索引的 type 下的数据进行迁移
POST _reindex
{
  "source": {
    "index":"twitter",
    "type":"tweet"
  },
  "dest":{
    "index":"twweets"
  }
}

参考官网:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html

参数映射规则:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-params.html#mapping-params

分词

一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 token

列如,witespace tokenizer 遇到的空白字符时分割文本,它会将文本 “Quick brown fox” 分割为 【Quick brown fox】

tokenizer (分词器)还负责记录各个term (词条)的顺序或 position 位置(用于phrase短语和word proximity词近邻查询),以及

term (词条)所代表的原始 word (单词)的start(起始)和end (结束)的 character offsets (字符偏移量) (用于 高亮显示搜索的内容)。

Elasticsearch 提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)

1、安装 ik 分词器

注意:不能用默认的 elasticsearch-plugin.install xxx.zip 进行自动安装

https://github.com/medcl/elasticsearch-analysis-ik/releases 下载与 es对应的版本

安装后拷贝到 plugins 目录下

  • 下载安装包:(阿里云盘有备份:IT技术学习 - gulimall-s)

    • 下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
  • 在/mydata/elasticsearch/plugins/目录下新建文件夹ik

    cd /mydata/elasticsearch/plugins/
    
    mkdir ik
    
  • 将下好的压缩包拷贝到ik文件夹里

  • 解压缩

    unzip elasticsearch-analysis-ik-7.4.2.zip
    
  • 删除zip文件

    rm -rf elasticsearch-analysis-ik-7.4.2.zip
    
  • 进入elasticsearch中查看安装情况

    [root@master ik]# docker ps
    CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS          PORTS                                                                                  NAMES
    c23a3a8c65b3   kibana:7.4.2          "/usr/local/bin/dumb…"   23 minutes ago   Up 23 minutes   0.0.0.0:5601->5601/tcp, :::5601->5601/tcp                                              kibana
    545c3c3a9ff9   elasticsearch:7.4.2   "/usr/local/bin/dock…"   24 minutes ago   Up 14 minutes   0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 0.0.0.0:9300->9300/tcp, :::9300->9300/tcp   elasticsearch
    
    [root@master ik]# docker exec -it 545 /bin/bash
    
    [root@545c3c3a9ff9 elasticsearch]# ls
    LICENSE.txt  NOTICE.txt  README.textile  bin  config  data  jdk  lib  logs  modules  plugins
    
    [root@545c3c3a9ff9 elasticsearch]# cd bin
    
    [root@545c3c3a9ff9 bin]# ls
    elasticsearch           elasticsearch-cli       elasticsearch-enve      elasticsearch-node           elasticsearch-setup-passwords  elasticsearch-sql-cli-7.4.2.jar  x-pack-env
    elasticsearch-certgen   elasticsearch-croneval  elasticsearch-keystore  elasticsearch-plugin         elasticsearch-shard            elasticsearch-syskeygen          x-pack-security-env
    elasticsearch-certutil  elasticsearch-env       elasticsearch-migrate   elasticsearch-saml-metadata  elasticsearch-sql-cli          elasticsearch-users              x-pack-watcher-env
    
    [root@545c3c3a9ff9 bin]# elasticsearch-plugin list
    ik
    
2、测试
  • 使用前记得重启elasticsearch

    docker restart elasticsearch
    

分词器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kd0cFEUQ-1684464779542)(images/谷粒商城项目笔记/image-20201026092255250.png)]

docker run -p 80:80 --name nginx -d nginx:1.10

3、自定义词库
  • Docker 安装 Nginx

    • 创建要挂载的配置目录

      mkdir -p /mydata/nginx/conf
      
    • 启动临时nginx容器

      docker run -p 80:80 --name nginx -d nginx:1.10
      
    • 拷贝出 Nginx 容器的配置

      # 将nginx容器中的nginx目录复制到本机的/mydata/nginx/conf目录
      docker container cp nginx:/etc/nginx /mydata/nginx/conf
      
      # 复制的是nginx目录,将该目录的所有文件移动到 conf 目录
      mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/
      
      # 删除多余的 /mydata/nginx/conf/nginx目录
      rm -rf /mydata/nginx/conf/nginx
      
    • 删除临时nginx容器

      # 停止运行 nginx 容器
      docker stop nginx
      
      # 删除 nginx 容器
      docker rm nginx
      
    • 启动 nginx 容器

      docker run -p 80:80 --name nginx \
      -v /mydata/nginx/html:/usr/share/nginx/html \
      -v /mydata/nginx/logs:/var/log/nginx \
      -v /mydata/nginx/conf/:/etc/nginx \
      -d nginx:1.10
      
    • 设置 nginx 随 Docker 启动

      docker update nginx --restart=always
      
    • 测试 nginx

      • echo '

        谷粒商城源码

        '
        \ >/mydata/nginx/html/index.html
      • 打开:http://192.168.163.131/ 可以看到下面内容说明安装成功

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zt6nim4y-1684464779543)(images/谷粒商城项目笔记/image-20220510180925529.png)]

  • nginx 中自定义分词文件

    mkdir /mydata/nginx/html/es
    
    echo "蔡徐坤" > /mydata/nginx/html/es/fenci.txt
    

    nginx 默认请求地址为 ip:port/es/fenci.txt;本机为:192.168.163.131/es/fenci.txt

    如果想要增加新的词语,只需要在该文件追加新的行并保存新的词语即可。

  • 给 es 配置自定义词库

    # 1. 打开并编辑 ik 插件配置文件
    vim /mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
    

    修改为以下内容:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 -->
            <entry key="ext_dict"></entry>
             <!--用户可以在这里配置自己的扩展停止词字典-->
            <entry key="ext_stopwords"></entry>
            <!--用户可以在这里配置远程扩展字典 -->
            <!-- <entry key="remote_ext_dict">words_location</entry> -->
            <entry key="remote_ext_dict">http://192.168.91.100/es/fenci.txt</entry>
            <!--用户可以在这里配置远程扩展停止词字典-->
            <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
    </properties>
    
  • 重启 elasticsearch 容器

    docker restart elasticsearch
    
  • 测试自定义词库

    GET my_index/_analyze
    {
       "analyzer": "ik_max_word", 
       "text":"蔡徐坤"
    }
    

    结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XAJImH0j-1684464779543)(images/谷粒商城项目笔记/image-20220510181243145.png)]

5 Elasticsearch - Rest - client

1、9300:TCP

Spring-data-elasticsearch:transport-api.jar

SpringBoot版本不同,transport-api.jar 不同,不能适配 es 版本

7.x 已经不在适合使用,8 以后就要废弃

2、9200:HTTP

JestClient 非官方,更新慢

RestTemplate:默认发送 HTTP 请求,ES很多操作都需要自己封装、麻烦

HttpClient:同上

Elasticsearch - Rest - Client:官方RestClient,封装了 ES 操作,API层次分明

最终选择 Elasticsearch - Rest - Client (elasticsearch - rest - high - level - client)

1、SpringBoot 整合

1、Pom.xml


<dependency>
    <groupId>org.elasticsearch.clientgroupId>
    <artifactId>elasticsearch-rest-high-level-clientartifactId>
    <version>7.4.2version>
dependency>

为什么要导入这个?这个配置那里来的?

官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-maven.html

2、Config配置
/**
 * @author gcq
 * @Create 2020-10-26
 *
 * 1、导入配置
 * 2、编写配置,给容器注入一个RestHighLevelClient
 * 3、参照API 官网进行开发
 */
@Configuration
public class GulimallElasticsearchConfig {


    public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//        builder.addHeader("Authorization", "Bearer " + TOKEN);
//        builder.setHttpAsyncResponseConsumerFactory(
//                new HttpAsyncResponseConsumerFactory
//                        .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
        COMMON_OPTIONS = builder.build();
    }



    @Bean
    public RestHighLevelClient esRestClient() {
        RestClientBuilder builder = null;
        builder = RestClient.builder(new HttpHost("192.168.56.10", 9200, "http"));

        RestHighLevelClient client = new RestHighLevelClient(builder);
//        RestHighLevelClient client = new RestHighLevelClient(
//                RestClient.builder(
//                        new HttpHost("localhost", 9200, "http"),
//                        new HttpHost("localhost", 9201, "http")));
        return client;
    }

}

官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html

3、使用

测试是否注入成功

@Autowired
private RestHighLevelClient client;

@Test
public void contextLoads() {
    System.out.println(client);
}

测试是否能 添加 或更新数据

/**
 * 添加或者更新
 * @throws IOException
 */
@Test
public void indexData() throws IOException {
    IndexRequest indexRequest = new IndexRequest("users");
    User user = new User();
    user.setAge(19);
    user.setGender("男");
    user.setUserName("张三");
    String jsonString = JSON.toJSONString(user);
    indexRequest.source(jsonString,XContentType.JSON);

    // 执行操作
    IndexResponse index = client.index(indexRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);

    // 提取有用的响应数据
    System.out.println(index);
}

测试复杂检索

 @Test
    public void searchTest() throws IOException {
        // 1、创建检索请求
        SearchRequest searchRequest = new SearchRequest();
        // 指定索引
        searchRequest.indices("bank");
        // 指定 DSL,检索条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));

        //1、2 按照年龄值分布进行聚合
        TermsAggregationBuilder aggAvg = AggregationBuilders.terms("ageAgg").field("age").size(10);
        sourceBuilder.aggregation(aggAvg);

        //1、3 计算平均薪资
        AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
        sourceBuilder.aggregation(balanceAvg);


        System.out.println("检索条件" + sourceBuilder.toString());

        searchRequest.source(sourceBuilder);

        // 2、执行检索
        SearchResponse searchResponse = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);

        // 3、分析结果
        System.out.println(searchResponse.toString());

        // 4、拿到命中得结果
        SearchHits hits = searchResponse.getHits();
        // 5、搜索请求的匹配
        SearchHit[] searchHits = hits.getHits();
        // 6、进行遍历
        for (SearchHit hit : searchHits) {
            // 7、拿到完整结果字符串
            String sourceAsString = hit.getSourceAsString();
            // 8、转换成实体类
            Accout accout = JSON.parseObject(sourceAsString, Accout.class);
            System.out.println("account:" + accout );
        }

        // 9、拿到聚合
        Aggregations aggregations = searchResponse.getAggregations();
//        for (Aggregation aggregation : aggregations) {
//
//        }
        // 10、通过先前名字拿到对应聚合
        Terms ageAgg1 = aggregations.get("ageAgg");
        for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
            // 11、拿到结果
            String keyAsString = bucket.getKeyAsString();
            System.out.println("年龄:" + keyAsString);
            long docCount = bucket.getDocCount();
            System.out.println("个数:" + docCount);
        }
        Avg balanceAvg1 = aggregations.get("balanceAvg");
        System.out.println("平均薪资:" + balanceAvg1.getValue());
        System.out.println(searchResponse.toString());
    }

结果:

accout:GulimallSearchApplicationTests.Accout(account_number=970, balance=19648, firstname=Forbes, lastname=Wallace, age=28, gender=M, address=990 Mill Road, employer=Pheast, email=forbeswallace@pheast.com, city=Lopezo, state=AK)accout:GulimallSearchApplicationTests.Accout(account_number=136, balance=45801, firstname=Winnie, lastname=Holland, age=38, gender=M, address=198 Mill Lane, employer=Neteria, email=winnieholland@neteria.com, city=Urie, state=IL)accout:GulimallSearchApplicationTests.Accout(account_number=345, balance=9812, firstname=Parker, lastname=Hines, age=38, gender=M, address=715 Mill Avenue, employer=Baluba, email=parkerhines@baluba.com, city=Blackgum, state=KY)accout:GulimallSearchApplicationTests.Accout(account_number=472, balance=25571, firstname=Lee, lastname=Long, age=32, gender=F, address=288 Mill Street, employer=Comverges, email=leelong@comverges.com, city=Movico, state=MT)年龄:38
个数:2
年龄:28
个数:1
年龄:32
个数:1
平均薪水:25208.0

总结:参考官网的API 和对应在 kibana 中发送的请求,在代码中通过调用对应API实现效果

官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html#java-rest-high-search-request-optional

ELK

Elasticsearch 用于检索数据

logstach:存储数据

Kiban:视图化查看数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rP98RzZT-1684464779543)(images/谷粒商城项目笔记/image-20201027120603889.png)]

Nginx-搭建域名访问环境

  • 修改 Windows hosts 文件

    • 位置:C:\Windows\System32\drivers\etc

    • 后面追加

      # guli mall #  注意这个ip地址是nginx所在服务器主机地址
      192.168.91.100		gulimall.com
      
  • Nginx 配置文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIKbapSk-1684464779544)(images/谷粒商城项目笔记/image-20220511151810799.png)]

  • 分析Nginx配置文件

    • 位置:cat /mydata/nginx/conf/nginx.conf

      user  nginx;
      worker_processes  1;
      
      error_log  /var/log/nginx/error.log warn;
      pid        /var/run/nginx.pid;
      
      events {
          worker_connections  1024;
      }
      
      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;
      }
      
    • 可以看到,在 http 块中最后有 include /etc/nginx/conf.d/*.conf; 这句配置说明在 conf.d 目录下所有 .conf 后缀的文件内容都会作为 nginx 配置文件 http 块中的配置。这是为了防止主配置文件太复杂,也可以对不同的配置进行分类。

      下面我们参考 conf.d 目录下的配置,来配置 gulimall 的 server 块配置

  • 配置gulimall.conf

    • 复制出来

      cd /mydata/nginx/conf/conf.d
      
      cp default.conf gulimall.conf
      
    • 查看Windows ip

      • 打开cmd 输入 ipconfig

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vcv47X3G-1684464779544)(images/谷粒商城项目笔记/image-20220511152556498.png)]

      • 这里的 192.168.1.7 和 192.168.56.1 也是 Windows 的本机地址

        所以我们配置当访问 nginx /请求时代理到 192.168.56.1:10000 商品服务首页

    • 配置代理

      vim gulimall.conf
      
      server {
          listen       80;
          server_name  gulimall.com;
      
          #charset koi8-r;
          #access_log  /var/log/nginx/log/host.access.log  main;
      
          location / {
            proxy_pass http://192.168.91.1:10000;
          }
      
          #error_page  404              /404.html;
      
          # redirect server error pages to the static page /50x.html
          #
          error_page   500 502 503 504  /50x.html;
          location = /50x.html {
              root   /usr/share/nginx/html;
          }
      }
      
    • 图示

      [外链图片转存中…(img-UB12NNaY-1684464779544)]

  • 反向代理:nginx 代理网关由网关进行转发

    • 修改 nginx.conf

      vim /mydata/nginx/conf/nginx.conf
      
    • 修改 http 块,配置上游服务器为网关地址

      user  nginx;
      worker_processes  1;
      
      error_log  /var/log/nginx/error.log warn;
      pid        /var/run/nginx.pid;
      
      events {
          worker_connections  1024;
      }
      
      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 {
              server 192.168.56.1:88;
          }
          include /etc/nginx/conf.d/*.conf;
      }
      

      [外链图片转存中…(img-8SAmwc3X-1684464779544)]

    • 修改 gulimall.conf

      • 配置代理地址为上面配置的上游服务器名

        server {
            listen       80;
            server_name  gulimall.com;
        
            #charset koi8-r;
            #access_log  /var/log/nginx/log/host.access.log  main;
        
            location / {
              proxy_set_header Host $host;
              proxy_pass http://gulimall;
            }
        
            #error_page  404              /404.html;
        
            # redirect server error pages to the static page /50x.html
            #
            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   /usr/share/nginx/html;
            }
        }
        
      • [外链图片转存中…(img-9AhpDrha-1684464779545)]

  • 效果

    [外链图片转存中…(img-fvD3NgtB-1684464779545)]

  • 访问跳转分析

    • 当前通过域名的方式,请求 gulimall.com ;
    • 根据 hosts 文件的配置,请求 gulimall.com 域名时会请求虚拟机 ip
    • 当请求到 192.168.163.131:80 时,会被 nginx 转发到我们配置的 192.168.163.1:10000 路径,该路径为运行商品服务的 windows 主机 ip 地址,至此达到通过域名访问商品服务的目的。
  • 后续网关配置

    • 之后为了统一管理我们的各种服务,我们将通过配置网关作为 nginx 转发的目标。最后通过配置网关根据不同的域名来判断跳转对应的服务。

      [外链图片转存中…(img-zQtzpXIC-1684464779545)]

性能与压力测试

简介

压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统

在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数

使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误,有两种错误类型是:

内存泄漏、并发与同步

有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化

Jvm 内存模型

1、Jvm内存模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGPVndiC-1684464779545)(images/谷粒商城项目笔记/image-20201029112517466.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdDalfLm-1684464779546)(images/谷粒商城项目笔记/image-20201029113956043.png)]

所有的对象实例以及数组都要在堆上分配,堆时垃圾收集器管理的主要区域,也被称为 “GC堆”,也是我们优化最多考虑的地方

堆可以细分为:

  • 新生代

    • Eden空间
    • From Survivor 空间
    • To Survivor 空间
  • 老年代

  • 永久代/原空间

    • Java8 以前永久代、受 JVM 管理、Java8 以后原空间,直接使用物理内存,因此默认情况下,原空间的大小仅受本地内存限制

垃圾回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbZ04yzV-1684464779546)(images/谷粒商城项目笔记/image-20201029114153244.png)]

从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一个新的区域 - 元空间(MetaSpac)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U6s0KZwb-1684464779546)(images/谷粒商城项目笔记/image-20201029114218716.png)]

jconsole 与 jvisualvm

jdk 的两个小工具 jconsole、jvisualvm(升级版本的 jconsole)。通过命令行启动、可监控本地和远程应用、远程应用需要配置

1、jvisualvm 能干什么

监控内存泄漏、跟踪垃圾回收、执行时内存、cpu分析、线程分析…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6SV5caZ-1684464779547)(images/谷粒商城项目笔记/image-20201029120502383.png)]

运行:正在运行的线程

休眠:sleep

等待:wait

驻留:线程池里面的空闲线程

监视:组赛的线程、正在等待锁

2、安装插件方便查看 gc

cmd 启动 jvisualvm

工具->插件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9QoN3Kdr-1684464779547)(images/谷粒商城项目笔记/image-20201029121108492.png)]

如果503 错误解决

打开网址: https://visualvm.github.io/pluginscenters.html

cmd 查看自己的jdk版本,找到对应的

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oTglkSt-1684464779547)(images/谷粒商城项目笔记/image-20220617194246919.png)]

  • 打开https://visualvm.github.io/pluginscenters.html

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NgPAOe6-1684464779547)(images/谷粒商城项目笔记/image-20220617194345745.png)]

  • 复制下面这个链接

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IB3Afz7S-1684464779548)(images/谷粒商城项目笔记/image-20220617194416881.png)]

  • 打开jvisualvm,点击 工具 - 插件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tof7HulC-1684464779548)(images/谷粒商城项目笔记/image-20220617194514322.png)]

  • 点击 设置 - 编辑

    [外链图片转存中…(img-ODx1xX0t-1684464779548)]

  • 刚才复制好的链接复制到如下位置:(注意此时要切换为手机热点网络

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hchTdEeE-1684464779548)(images/谷粒商城项目笔记/image-20220617194636634.png)]

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9COMVAsI-1684464779549)(images/谷粒商城项目笔记/image-20220617194746696.png)]

    • 安装完成后重启jvisualvm(安装完成记得网络切换回去,流量贵
  • 此时打开某个java应用,就发现多了个Visual GC

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIOu6V1a-1684464779549)(images/谷粒商城项目笔记/image-20220617195055516.png)]

docker stats 查看相关命令

JMeter
1、安装

(阿里云盘:IT技术学习 - gulimall-soft)

官网:https://jmeter.apache.org/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MyGqcHC7-1684464779549)(images/谷粒商城项目笔记/image-20220504155752345.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NCLLGLey-1684464779550)(images/谷粒商城项目笔记/image-20220504155833695.png)]

2、启动

解压后打开bin目录

点击jmeter.bat

就开启了jmeter

JMeter 压测示例
1、添加线程组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FK4MQiZ0-1684464779550)(images/谷粒商城项目笔记/image-20201029084634498.png)]

2、添加 HTTP 请求

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCs6ZJ3M-1684464779550)(images/谷粒商城项目笔记/image-20201029085843220.png)]

3、添加监听器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tqhj4CQ6-1684464779550)(images/谷粒商城项目笔记/image-20201029085942442.png)]

4、启动压测&查看

汇总图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxxIKqew-1684464779550)(images/谷粒商城项目笔记/image-20201029092357910.png)]

察看结果树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RewbCIRt-1684464779551)(images/谷粒商城项目笔记/image-20201029092436633.png)]

汇总报告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zjrDWtza-1684464779551)(images/谷粒商城项目笔记/image-20201029092454376.png)]

聚合报告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bl2wq72p-1684464779551)(images/谷粒商城项目笔记/image-20201029092542876.png)]

JMeter Address Already in use 错误解决

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

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

2.在HKEY_ LOCAL MACHINE\SYSTEMCurrentControlSet\Services Tcpip\Parameters下,

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

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

TCPTimedWaitDelay:30

性能优化 - Nginx 动静分离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vAR3SWUO-1684464779551)(images/谷粒商城项目笔记/image-20220511173747297.png)]

  • 新建static目录

    mkdir /mydata/nginx/html/static
    
  • 首先,把商品服务中静态文件夹 index 放到 nginx 下 /mydata/nginx/html/static目录;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7s8oRaVg-1684464779552)(images/谷粒商城项目笔记/image-20220618104825684.png)]

  • 修改 Nginx 配置文件 /mydata/nginx/conf/conf.d/gulimall.conf

vi /mydata/nginx/conf/conf.d/gulimall.conf
# /static/ 下所有的请求都转给 nginx
location /static/ {
	root /usr/share/nginx/html;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3fMZl1cv-1684464779552)(images/谷粒商城项目笔记/image-20220511173837394.png)]

  • 重启nginx

    docker restart nginx
    

缓存和分布式锁

缓存
整合 redis 作为缓存
1、引入依赖

SpringBoot 整合 redis,查看SpringBoot提供的 starts

[外链图片转存中…(img-BDZpIDTr-1684464779552)]

官网:https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/using-boot-build-systems.html#using-boot-starter

pom.xml

 
        
            org.springframework.boot
            spring-boot-starter-data-redis
            
            
                
                    io.lettuce
                    lettuce-core
                
            
        

        
        
            redis.clients
            jedis
        

堆外内存溢出异常:

这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。

下面进行分析:

  • SpringBoot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信;
  • lettuce 的 bug 导致 netty 堆外内存溢出;
  • netty 如果没有指定堆外内存,默认使用 -Xmx 参数指定的内存;
  • 可以通过 -Dio.netty.maxDirectMemory 进行设置;

解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。

  • 升级 lettuce 客户端,或使用 jedis 客户端
2、配置

application.yaml

Spring:
  redis:
    host: 192.168.56.10
    port: 6379

RedisAutoConfig.java

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gziOAEFg-1684464779552)(images/谷粒商城项目笔记/image-20201031154710108.png)]

3、测试
@Autowired
StringRedisTemplate stringRedisTemplate;

@Test
public void testStringRedisTemplate() {

    stringRedisTemplate.opsForValue().set("hello","world_" + UUID.randomUUID().toString());
    String hello = stringRedisTemplate.opsForValue().get("hello");
    System.out.println("之前保存的数据是:" + hello);
}
4、优化三级分类数据获取
/**
 * TODO 产生堆外内存溢出 OutOfDirectMemoryError
 * 1、SpringBoot2.0以后默认使用 Lettuce作为操作redis的客户端,它使用 netty进行网络通信
 * 2、lettuce 的bug导致netty堆外内存溢出,-Xmx300m netty 如果没有指定堆内存移除,默认使用 -Xmx300m
 *      可以通过-Dio.netty.maxDirectMemory 进行设置
 *   解决方案 不能使用 -Dio.netty.maxDirectMemory调大内存
 *   1、升级 lettuce客户端,2、 切换使用jedis
 *   redisTemplate:
 *   lettuce、jedis 操作redis的底层客户端,Spring再次封装
 * @return
 */
@Override
public Map> getCatelogJson() {

    // 给缓存中放 json 字符串、拿出的是 json 字符串,还要逆转为能用的对象类型【序列化和反序列化】

    // 1、加入缓存逻辑,缓存中放的数据是 json 字符串
    // JSON 跨语言,跨平台兼容
    String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
    if (StringUtils.isEmpty(catelogJSON)) {
        // 2、缓存没有,从数据库中查询
        Map> catelogJsonFromDb = getCatelogJsonFromDb();
        // 3、查询到数据,将数据转成 JSON 后放入缓存中
        String s = JSON.toJSONString(catelogJsonFromDb);
        redisTemplate.opsForValue().set("catelogJSON",s);
        return catelogJsonFromDb;
    }
    // 转换为我们指定的对象
    Map> result = JSON.parseObject(catelogJSON, new TypeReference>>() {});

    return result;
}
分布式锁
分布式锁基本原理

[外链图片转存中…(img-fdkVCYEz-1684464779553)]

**理解:**就先当1000个人去占一个厕所,厕所只能有一个人占到这个坑,占到这个坑其他人就只能在外面等待,等待一段时间后可以再次来占坑,业务执行后,释放锁,那么其他人就可以来占这个坑

分布式锁演进 - 阶段一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pmY4YGt-1684464779553)(images/谷粒商城项目笔记/image-20201031123441336.png)]

代码:

 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0");
        if (lock) {
            // 加锁成功..执行业务
            Map> dataFromDb = getDataFromDB();
            redisTemplate.delete("lock"); // 删除锁
            return dataFromDb;
        } else {
            // 加锁失败,重试 synchronized()
            // 休眠100ms重试
            return getCatelogJsonFromDbWithRedisLock();
        }
分布式锁演进 - 阶段二

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHC4ysjT-1684464779553)(images/谷粒商城项目笔记/image-20201031123640746.png)]

代码:

 Boolean lock = redisTemplate.opsForValue().setIfAbsent()
        if (lock) {
            // 加锁成功..执行业务
            // 设置过期时间
            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map> dataFromDb = getDataFromDB();
            redisTemplate.delete("lock"); // 删除锁
            return dataFromDb;
        } else {
            // 加锁失败,重试 synchronized()
            // 休眠100ms重试
            return getCatelogJsonFromDbWithRedisLock();
        }
分布式锁演进 - 阶段三

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tBemFBkT-1684464779554)(images/谷粒商城项目笔记/image-20201031124210112.png)]

代码:

// 设置值同时设置过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111",300,TimeUnit.SECONDS);
if (lock) {
    // 加锁成功..执行业务
    // 设置过期时间,必须和加锁是同步的,原子的
    redisTemplate.expire("lock",30,TimeUnit.SECONDS);
    Map> dataFromDb = getDataFromDB();
    redisTemplate.delete("lock"); // 删除锁
    return dataFromDb;
} else {
    // 加锁失败,重试 synchronized()
    // 休眠100ms重试
    return getCatelogJsonFromDbWithRedisLock();
}
分布式锁演进 - 阶段四

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cTcvtlkS-1684464779554)(images/谷粒商城项目笔记/image-20201031124615670.png)]

图解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfSczvzd-1684464779554)(images/谷粒商城项目笔记/image-20201031130547173.png)]

代码:

 String uuid = UUID.randomUUID().toString();
        // 设置值同时设置过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
        if (lock) {
            // 加锁成功..执行业务
            // 设置过期时间,必须和加锁是同步的,原子的
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map> dataFromDb = getDataFromDB();
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (lockValue.equals(uuid)) {
//                // 删除我自己的锁
//                redisTemplate.delete("lock"); // 删除锁
//            }
// 通过使用lua脚本进行原子性删除
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                //删除锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);

            return dataFromDb;
        } else {
            // 加锁失败,重试 synchronized()
            // 休眠100ms重试
            return getCatelogJsonFromDbWithRedisLock();
        }
分布式锁演进 - 阶段五 最终模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tkhsSQRl-1684464779555)(images/谷粒商城项目笔记/image-20201031130201609.png)]

代码:

 String uuid = UUID.randomUUID().toString();
        // 设置值同时设置过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功");
            // 加锁成功..执行业务
            // 设置过期时间,必须和加锁是同步的,原子的
//            redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map> dataFromDb;
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (lockValue.equals(uuid)) {
//                // 删除我自己的锁
//                redisTemplate.delete("lock"); // 删除锁
//            }
            try {
                dataFromDb = getDataFromDB();
            } finally {
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                //删除锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            // 加锁失败,重试 synchronized()
            // 休眠200ms重试
            System.out.println("获取分布式锁失败,等待重试");
            try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
            return getCatelogJsonFromDbWithRedisLock();
        }

问题:

  • 分布式加锁解锁都是这两套代码,可以封装成工具类
  • 分布式锁有更专业的框架
分布式锁 - Redisson
1、整合
1、引入依赖


    org.redisson
    redisson
    3.12.0

2、配置 redisson
@Configuration
public class MyRedissonConfig {
    /**
     * 所有对 Redisson 的使用都是通过 RedissonClient
     *
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        // Redis url should start with redis:// or rediss://
        config.useSingleServer().setAddress("redis://192.168.163.131:6379");

        // 2、根据 Config 创建出 RedissonClient 实例
        return Redisson.create(config);
    }
}
3、使用
// 1. 获取一把锁
Rlock lock = redisson.getLock("my-lock");

// 2. 加锁, 阻塞式等待
lock.lock();
try {
	System.out.println("加锁成功,执行业务...");
} catch (Exception e) {
} finally {
	// 3. 解锁 假设解锁代码没有运行,Redisson 会出现死锁吗?(不会)
    lock.unlock();
}
2、Redisson - Lock 锁测试 & Redisson - Lock 看门狗原理 - Redisson 如何解决死锁
@RequestMapping("/hello")
@ResponseBody
public String hello(){
    // 1、获取一把锁,只要锁得名字一样,就是同一把锁
    RLock lock = redission.getLock("my-lock");

    // 2、加锁
    lock.lock(); // 阻塞式等待,默认加的锁都是30s时间
    // 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期后被删掉
    // 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s以后自动删除

    lock.lock(10, TimeUnit.SECONDS); //10s 后自动删除
    //问题 lock.lock(10, TimeUnit.SECONDS) 在锁时间到了后,不会自动续期
    // 1、如果我们传递了锁的超时时间,就发送给 redis 执行脚本,进行占锁,默认超时就是我们指定的时间
    // 2、如果我们为指定锁的超时时间,就是用 30 * 1000 LockWatchchdogTimeout看门狗的默认时间、
    //      只要占锁成功,就会启动一个定时任务,【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s就自动续期
    //      internalLockLeaseTime【看门狗时间】 /3,10s

    //最佳实践
    // 1、lock.lock(10, TimeUnit.SECONDS);省掉了整个续期操作,手动解锁

    try {
        System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
        Thread.sleep(3000);
    } catch (Exception e) {

    } finally {
        // 解锁 将设解锁代码没有运行,reidsson会不会出现死锁
        System.out.println("释放锁...." + Thread.currentThread().getId());
        lock.unlock();
    }

    return "hello";
}

进入到 Redisson Lock 源码

1、进入 Lock 的实现 发现 他调用的也是 lock 方法参数 时间为 -1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LB9jyr9Q-1684464779555)(images/谷粒商城项目笔记/image-20201101051659465.png)]

2、再次进入 lock 方法

发现他调用了 tryAcquire

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSNihe65-1684464779555)(images/谷粒商城项目笔记/image-20201101051925487.png)]

3、进入 tryAcquire

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6ry8esG-1684464779556)(images/谷粒商城项目笔记/image-20201101052008724.png)]

4、里头调用了 tryAcquireAsync

这里判断 laseTime != -1 就与刚刚的第一步传入的值有关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWYyU12W-1684464779556)(images/谷粒商城项目笔记/image-20201101052037959.png)]

5、进入到 tryLockInnerAsync 方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zu16j6Fa-1684464779556)(images/谷粒商城项目笔记/image-20201101052158592.png)]

6、internalLockLeaseTime 这个变量是锁的默认时间

这个变量在构造的时候就赋初始值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YJnPEcC1-1684464779557)(images/谷粒商城项目笔记/image-20201101052346059.png)]

7、最后查看 lockWatchdogTimeout 变量

也就是30秒的时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zv2I5wfJ-1684464779557)(images/谷粒商城项目笔记/image-20201101052428198.png)]

3、Reidsson - 读写锁

二话不说,上代码!!!

/**
     * 保证一定能读取到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享锁)读锁是一个共享锁
     * 写锁没释放读锁就必须等待
     * 读 + 读 相当于无锁,并发读,只会在 reids中记录好,所有当前的读锁,他们都会同时加锁成功
     * 写 + 读 等待写锁释放
     * 写 + 写 阻塞方式
     * 读 + 写 有读锁,写也需要等待
     * 只要有写的存在,都必须等待
     * @return String
     */
    @RequestMapping("/write")
    @ResponseBody
    public String writeValue() {

        RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
        String s = "";
        RLock rLock = lock.writeLock();
        try {
            // 1、改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功..." + Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            redisTemplate.opsForValue().set("writeValue",s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("写锁释放..." + Thread.currentThread().getId());
        }
        return s;
    }

    @RequestMapping("/read")
    @ResponseBody
    public String readValue() {
        RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
        RLock rLock = lock.readLock();
        String s = "";
        rLock.lock();
        try {
            System.out.println("读锁加锁成功..." + Thread.currentThread().getId());
            s = (String) redisTemplate.opsForValue().get("writeValue");
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("读锁释放..." + Thread.currentThread().getId());
        }
        return s;
    }

来看下官网的解释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78BNd7tK-1684464779557)(images/谷粒商城项目笔记/image-20201101053042268.png)]

4、Redisson - 闭锁测试

官网!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AH6aLyTW-1684464779557)(images/谷粒商城项目笔记/image-20201101053053554.png)]

上代码

/**
 * 放假锁门
 * 1班没人了
 * 5个班级走完,我们可以锁们了
 * @return
 */
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redission.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();//等待闭锁都完成

    return "放假了....";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redission.getCountDownLatch("door");
    door.countDown();// 计数器减一

    return id + "班的人走完了.....";
}

和 JUC 的 CountDownLatch 一致

await()等待闭锁完成

countDown() 把计数器减掉后 await就会放行

5、Redisson - 信号量测试

官网!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-siFzJljP-1684464779558)(images/谷粒商城项目笔记/image-20201101053450708.png)]

/**
 * 车库停车
 * 3车位
 * @return
 */
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
    RSemaphore park = redission.getSemaphore("park");
    boolean b = park.tryAcquire();//获取一个信号,获取一个值,占用一个车位

    return "ok=" + b;
}

@GetMapping("/go")
@ResponseBody
public String go() {
    RSemaphore park = redission.getSemaphore("park");

    park.release(); //释放一个车位

    return "ok";
}

类似 JUC 中的 Semaphore

缓存数据一致性
缓存数据一致性 - 双写模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SzujftcI-1684464779558)(images/谷粒商城项目笔记/image-20201101053613373.png)]

两个线程写 最终只有一个线程写成功,后写成功的会把之前写的数据给覆盖,这就会造成脏数据

缓存数据一致性 - 失效模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ZpmefIo-1684464779558)(images/谷粒商城项目笔记/image-20201101053834126.png)]

三个连接

一号连接 写数据库 然后删缓存

二号连接 写数据库时网络连接慢,还没有写入成功

三号链接 直接读取数据,读到的是一号连接写入的数据,此时 二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存

缓存数据一致性解决方案

无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实力同时更新会出事,怎么办?

  • 1、如果是用户纯度数据(订单数据、用户数据),这并发几率很小,几乎不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 2、如果是菜单,商品介绍等基础数据,也可以去使用 canal 订阅,binlog 的方式
  • 3、缓存数据 + 过期时间也足够解决大部分业务对缓存的要求
  • 4、通过加锁保证并发读写,写写的时候按照顺序排好队,读读无所谓,所以适合读写锁,(业务不关心脏数据,允许临时脏数据可忽略)

总结:

  • 我们能放入缓存的数据本来就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前的最新值即可
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点

最后符上 三级分类数据 加上分布式锁

Spring Cache
1、简介
  • Spring 从3.1开始定义了 org.springframework.cache.Cacheorg.sprngframework.cache.CacheManager 接口睐统一不同的缓存技术
  • 并支持使用 JCache(JSR-107)注解简化我们的开发
  • Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合 Cache 接口下 Spring 提供了各种 XXXCache的实现,如 RedisCacheEhCache,ConcrrentMapCache等等,
  • 每次调用需要缓存功能实现方法的时候,Spring 会检查检查指定参数的马努表犯法是否已经被嗲用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次直接调用从缓存中获取
  • 使用 Sprng 缓存抽象时我们需要关注的点有以下两点
    • 1、确定方法需要被缓存以及他们的的缓存策略
    • 2、从缓存中读取之前缓存存储的数据

官网地址:https://docs.spring.io/spring-framework/docs/5.2.10.RELEASE/spring-framework-reference/integration.html#cache-strategie

缓存注解配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SudDETQU-1684464779559)(images/谷粒商城项目笔记/image-20201228171806703-16555137421881.png)]

2、基础概念

从3.1版本开始,Spring 框架就支持透明地向现有 Spring 应用程序添加缓存。与事务支持类似,缓存抽象允许在对代码影响最小的情况下一致地使用各种缓存解决方案。从 Spring 4.1 开始,缓存抽象在JSR-107注释和更多定制选项的支持下得到了显著扩展。

 /**
 *  8、整合SpringCache简化缓存开发
 *      1、引入依赖
 *          spring-boot-starter-cache
 *      2、写配置
 *          1、自动配置了那些
 *              CacheAutoConfiguration会导入 RedisCacheConfiguration
 *              自动配置好了缓存管理器,RedisCacheManager
 *          2、配置使用redis作为缓存
 *          Spring.cache.type=redis
 *
 *       4、原理
 *       CacheAutoConfiguration ->RedisCacheConfiguration ->
 *       自动配置了 RedisCacheManager ->初始化所有的缓存 -> 每个缓存决定使用什么配置
 *       ->如果redisCacheConfiguration有就用已有的,没有就用默认的
 *       ->想改缓存的配置,只需要把容器中放一个 RedisCacheConfiguration 即可
 *       ->就会应用到当前 RedisCacheManager管理所有缓存分区中
 */
3、注解

对于缓存声明,Spring的缓存抽象提供了一组Java注解

/**
@Cacheable: Triggers cache population:触发将数据保存到缓存的操作
@CacheEvict: Triggers cache eviction: 触发将数据从缓存删除的操作
@CachePut: Updates the cache without interfering with the method execution:不影响方法执行更新缓存
@Caching: Regroups multiple cache operations to be applied on a method:组合以上多个操作
@CacheConfig: Shares some common cache-related settings at class-level:在类级别共享缓存的相同配置
**/

注解使用

com.atguigu.gulimall.product.service.impl.CategoryServiceImpl#getLevel1Categorys

/**
     * 1、每一个需要缓存的数据我们都需要指定放到那个名字的缓存【缓存分区的划分【按照业务类型划分】】
     * 2、@Cacheable({"category"})
     *      代表当前方法的结果需要缓存,如果缓存中有,方法不调用
     *      如果缓存中没有,调用方法,最后将方法的结果放入缓存
     * 3、默认行为:
     *      1、如果缓存中有,方法不用调用
     *      2、key默自动生成,缓存的名字:SimpleKey[](自动生成的key值)
     *      3、缓存中value的值,默认使用jdk序列化,将序列化后的数据存到redis
     *      3、默认的过期时间,-1
     *
     *    自定义操作
     *      1、指定缓存使用的key     key属性指定,接收一个SpEl
     *      2、指定缓存数据的存活时间  配置文件中修改ttl
     *      3、将数据保存为json格式
     * @return
     */
	//value 缓存的别名
     // key redis中key的名称,默认是方法名称
    @Cacheable(value = {"category"},key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        long l = System.currentTimeMillis();
        // parent_cid为0则是一级目录
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("耗费时间:" + (System.currentTimeMillis() - l));
        return categoryEntities;
    }
4、表达式语法

配置

package com.atguigu.gulimall.product.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author gcq
 * @Create 2020-11-01
 */
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {

    /**
     * 配置文件中的东西没有用上
     * 1、原来的配置吻技安绑定的配置类是这样子的
     *      @ConfigurationProperties(prefix = "Spring.cache")
     * 2、要让他生效
     *      @EnableConfigurationProperties(CacheProperties.class)
     * @param cacheProperties
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置key的序列化
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        // 设置value序列化 ->JackSon
        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;
    }

}

yaml

Spring:
  cache:
    type: redis
    redis:
      time-to-live: 3600000           # 过期时间
      key-prefix: CACHE_              # key前缀
      use-key-prefix: true            # 是否使用写入redis前缀
      cache-null-values: true         # 是否允许缓存空值
5、缓存穿透问题解决
/**
 * 1、每一个需要缓存的数据我们都需要指定放到那个名字的缓存【缓存分区的划分【按照业务类型划分】】
 * 2、@Cacheable({"category"})
 *      代表当前方法的结果需要缓存,如果缓存中有,方法不调用
 *      如果缓存中没有,调用方法,最后将方法的结果放入缓存
 * 3、默认行为:
 *      1、如果缓存中有,方法不用调用
 *      2、key默自动生成,缓存的名字:SimpleKey[](自动生成的key值)
 *      3、缓存中value的值,默认使用jdk序列化,将序列化后的数据存到redis
 *      3、默认的过期时间,-1
 *
 *    自定义操作
 *      1、指定缓存使用的key     key属性指定,接收一个SpEl
 *      2、指定缓存数据的存活时间  配置文件中修改ttl
 *      3、将数据保存为json格式
 * 4、Spring-Cache的不足:
 *      1、读模式:
 *          缓存穿透:查询一个null数据,解决 缓存空数据:ache-null-values=true
 *          缓存击穿:大量并发进来同时查询一个正好过期的数据,解决:加锁 ? 默认是无加锁
 *          缓存雪崩:大量的key同时过期,解决:加上随机时间,Spring-cache-redis-time-to-live
 *       2、写模式:(缓存与数据库库不一致)
 *          1、读写加锁
 *          2、引入canal,感知到MySQL的更新去更新数据库
 *          3、读多写多,直接去数据库查询就行
 *
 *    总结:
 *      常规数据(读多写少,即时性,一致性要求不高的数据)完全可以使用SpringCache 写模式( 只要缓存数据有过期时间就足够了)
 *
 *    特殊数据:特殊设计
 *      原理:
 *          CacheManager(RedisManager) -> Cache(RedisCache) ->Cache负责缓存的读写
 * @return
 */
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
@Override
public List<CategoryEntity> getLevel1Categorys() {
    long l = System.currentTimeMillis();
    // parent_cid为0则是一级目录
    List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    System.out.println("耗费时间:" + (System.currentTimeMillis() - l));
    return categoryEntities;
}
6、缓存更新
com.atguigu.gulimall.product.service.impl.CategoryServiceImpl#updateCascade

/**
     * 级联更新所有的关联数据
     * @CacheEvict 失效模式
     * 1、同时进行多种缓存操作 @Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
     * 3、存储同一类型的数据,都可以指定成同一分区,分区名默认就是缓存的前缀
     *
     * @param category
     */
    @Caching(evict = {
            @CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
            @CacheEvict(value = {"category"},key = "'getCatelogJson'")
    })
//    @CacheEvict(value = {"category"},allEntries = true)
    @Transactional
    @Override
    public void updateCascate(CategoryEntity category) {
        // 更新自己表对象
        this.updateById(category);
        // 更新关联表对象
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

总结业务流程:

如果忘了这个技术点看下做的笔记的例子,然后去官网看下文档,温故而知新

流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fdORvY5H-1684464779559)(images/谷粒商城项目笔记/image-20201228171552816-16555137421892.png)]

检索服务

  • 在虚拟机新建search文件夹

    mkdir /mydata/nginx/html/static/search
    
  • 将 html\搜索页 目录下的所有内容放入该目录下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NaxzENjI-1684464779560)(images/谷粒商城项目笔记/image-20220618101539423.png)]

  • 启动search服务

  • 配置 Windows hosts 文件

    192.168.91.100		search.gulimall.com
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrC33TLt-1684464779560)(images/谷粒商城项目笔记/image-20220618102202062.png)]

  • 进入配置文件

    cd mydata/nginx/conf/conf.d
    
    vi gulimall.conf
    
  • 修改配置文件

    server {
        listen       80;
        server_name  gulimall.com *.gulimall.com;
    		...
     }
    
  • 重启nginx

    docker restart nginx
    
  • 2

  • 2

  • 2

找到 Nginx 的配置文件,编辑 gulimall.conf,将所有 *.gulimall.com 的请求都经由 Nginx 转发给网关;

server {
    listen       80;
    server_name  search.gulimall.com gulimall.com;
		...
 }

然后重启 Nginx

docker restart nginx
配置网关服务转发到 search 服务
- id: mall_search_route
  uri: lb://mall-search
  predicates:
  - Host=search.gulimall.com
配置页面跳转

配置 /list.html 请求转发到 list 模板

/**
 * 自动将页面提交过来的所有请求参数封装成我们指定的对象
 *
 * @param param
 * @return
 */
@GetMapping(value = "/list.html")
public String listPage(SearchParam param, Model model, HttpServletRequest request) {
    return "list";
}
检索功能实现
  • 2
  • 2
  • 2
  • 2
  • 2
添加模板页面

<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-thymeleafartifactId>
dependency>

将资料中的前端页面放到 search 服务模块下的 resource/templates 下;

配置请求跳转
配置 Nginx 转发

配置 Windows hosts 文件:

192.168.56.100		search.gulimall.com

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4P4KFUz-1684464779560)(images/谷粒商城项目笔记/image-20220512132128091.png)]

异步和线程池

初始化线程的 4 种方式

1、继承 Thread

2、实现 Runnable

3、实现 Callable 接口 + FutureTask(可以拿到返回结果,可以处理异常)

4、线程池

方式一和方式二 主进程无法获取线程的运算结果,不适合当前场景

方式三:主进程可以获取当前线程的运算结果,但是不利于控制服务器种的线程资源,可以导致服务器资源耗尽

方式四:通过如下两种方式初始化线程池

Executors.newFixedThreadPool(3);
//或者
new ThreadPollExecutor(corePoolSize,maximumPoolSize,keepAliveTime,TimeUnit,unit,workQueue,threadFactory,handler);

通过线程池性能稳定,也可以获取执行结果,并捕获异常,但是,在业务复杂情况下,一个异步调用可能会依赖另一个异步调用的执行结果

线程池的 7 大参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bVjmoNnV-1684464779561)(images/谷粒商城项目笔记/image-20201105154808826.png)]

运行流程:

1、线程池创建,准备好 core 数量 的核心线程,准备接受任务

2、新的任务进来,用 core 准备好的空闲线程执行

  • core 满了,就将再进来的任务放入阻塞队列中,空闲的 core 就会自己去阻塞队列获取任务执行
  • 阻塞队列也满了,就直接开新线程去执行,最大只能开到 max 指定的数量
  • max 都执行好了,Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁,终保持到 core 大小
  • 如果线程数开到了 max 数量,还有新的任务进来,就会使用 reject 指定的拒绝策略进行处理

3、所有的线程创建都是由指定的 factory 创建的

面试;

一个线程池 core 7、max 20 ,queue 50 100 并发进来怎么分配的 ?

先有 7 个能直接得到运行,接下来 50 个进入队列排队,再多开 13 个继续执行,线程70个被安排上了,剩下30个默认拒绝策略

常见的 4 种线程池
  • newCacheThreadPool
    
    • 创建一个可缓存的线程池,如果线程池长度超过需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newFixedThreadPool
    
    • 创建一个指定长度的线程池,可控制线程最大并发数,超出的线程会再队列中等待
  • newScheduleThreadPool
    
    • 创建一个定长线程池,支持定时及周期性任务执行
  • newSingleThreadExecutor
    
    • 创建一个单线程化的线程池,她只会用唯一的工作线程来执行任务,保证所有任务
开发中为什么使用线程池
  • 降低资源的消耗
    • 通过重复利用已创建好的线程降低线程的创建和销毁带来的损耗
  • 提高响应速度
    • 因为线程池中的线程没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
  • 提高线程的客观理性
    • 线程池会根据当前系统的特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销,无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
CompletableFuture 异步编排

业务场景:

查询商品详情页逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eySYWXvD-1684464779561)(images/谷粒商城项目笔记/image-20201105163535757.png)]

假如商品详情页的每个查询,需要如下标注时间才能完成

那么,用户需要5.5s后才能看到商品相详情页的内容,很显然是不能接受的

如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应

创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iqmpROZc-1684464779561)(images/谷粒商城项目笔记/image-20201105185420349.png)]

1、runXxx 都是没有返回结果的,supplyXxxx都是可以获取返回结果的

2、可以传入自定义的线程池,否则就是用默认的线程池

3、根据方法的返回类型来判断是否该方法是否有返回类型

代码实现:

  public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main....start.....");
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
        }, executor);

        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, executor);
        Integer integer = future.get();

        System.out.println("main....stop....." + integer);
    }
计算完成时回调方法

[外链图片转存中…(img-3QZWzFK7-1684464779561)]

whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况

whenComplete 和 whenCompleteAsync 的区别

​ whenComplete :是执行当前任务的线程继续执行 whencomplete 的任务

​ whenCompleteAsync: 是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行

方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getId());
    int i = 10 / 0;
    System.out.println("运行结果:" + i);
    return i;
}, executor).whenComplete((res,exception) ->{
    // 虽然能得到异常信息,但是没法修改返回的数据
    System.out.println("异步任务成功完成了...结果是:" +res + "异常是:" + exception);
}).exceptionally(throwable -> {
    // 可以感知到异常,同时返回默认值
    return 10;
});
handle 方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HflXsQQl-1684464779562)(images/谷粒商城项目笔记/image-20201105194503175.png)]

和 complete 一样,可以对结果做最后的处理(可处理异常),可改变返回值

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getId());
    int i = 10 / 2;
    System.out.println("运行结果:" + i);
    return i;
}, executor).handle((res,thr) ->{
    if (res != null ) {
        return res * 2;
    }
    if (thr != null) {
        return 0;
    }
    return 0;
});
线程串行方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4o9W7iqH-1684464779562)(images/谷粒商城项目笔记/image-20201105195632819.png)]

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任物的返回值

thenAccept方法:消费处理结果,接受任务处理结果,并消费处理,无返回结果

thenRun 方法:只要上面任务执行完成,就开始执行 thenRun ,只是处理完任务后,执行 thenRun的后续操作

带有 Async 默认是异步执行的,同之前,

以上都要前置任务完成

   /**
         * 线程串行化,
         * 1、thenRun:不能获取到上一步的执行结果,无返回值
         * .thenRunAsync(() ->{
         *             System.out.println("任务2启动了....");
         *         },executor);
         * 2、能接受上一步结果,但是无返回值 thenAcceptAsync
         * 3、thenApplyAsync 能收受上一步结果,有返回值
         *
         */
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, executor).thenApplyAsync(res -> {
            System.out.println("任务2启动了..." + res);
            return "Hello " + res;
        }, executor);
        String s = future.get();

        System.out.println("main....stop....." + s);
两任务组合 - 都要完成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cnDAALxl-1684464779562)(images/谷粒商城项目笔记/image-20210102044028142.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDtM1DNK-1684464779562)(images/谷粒商城项目笔记/image-20210102044044914.png)]

两个任务必须都完成,触发该任务

thenCombine: 组合两个 future,获取两个 future的返回结果,并返回当前任务的返回值

thenAccpetBoth: 组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值

runAfterBoth:组合 两个 future,不需要获取 future 的结果,只需要两个 future处理完成任务后,处理该任务,

   /**
         * 两个都完成
         */
        CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1当前线程:" + Thread.currentThread().getId());
            int i = 10 / 4;
            System.out.println("任务1结束:" + i);
            return i;
        }, executor);

        CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2当前线程:" + Thread.currentThread().getId());
            System.out.println("任务2结束:");
            return "Hello";
        }, executor);

        // f1 和 f2 执行完成后在执行这个
//        future01.runAfterBothAsync(future02,() -> {
//            System.out.println("任务3开始");
//        },executor);

        // 返回f1 和 f2 的运行结果
//        future01.thenAcceptBothAsync(future02,(f1,f2) -> {
//            System.out.println("任务3开始....之前的结果:" + f1 + "==>" + f2);
//        },executor);

        // f1 和 f2 单独定义返回结果
        CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
            return f1 + ":" + f2 + "-> Haha";
        }, executor);

        System.out.println("main....end....." + future.get());
两任务组合 - 一个完成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-54dm2QvE-1684464779563)(images/谷粒商城项目笔记/image-20201106101904880.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lW9C68FO-1684464779563)(images/谷粒商城项目笔记/image-20201106101918013.png)]

当两个任务中,任意一个future 任务完成时,执行任务

applyToEither;两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值

acceptEither: 两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值

runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值

/**
         * 两个任务,只要有一个完成,我们就执行任务
         * runAfterEnitherAsync:不感知结果,自己没有返回值
         * acceptEitherAsync:感知结果,自己没有返回值
         *  applyToEitherAsync:感知结果,自己有返回值
         */
//        future01.runAfterEitherAsync(future02,() ->{
//            System.out.println("任务3开始...之前的结果:");
//        },executor);

//        future01.acceptEitherAsync(future02,(res) -> {
//            System.out.println("任务3开始...之前的结果:" + res);
//        },executor);

        CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {
            System.out.println("任务3开始...之前的结果:" + res);
            return res.toString() + "->哈哈";
        }, executor);
多任务组合

[外链图片转存中…(img-ZTm2KXj6-1684464779563)]

allOf:等待所有任务完成

anyOf:只要有一个任务完成

        CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的图片信息");
            return "hello.jpg";
        });

        CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性");
            return "黑色+256G";
        });

        CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("查询商品介绍");
            return "华为";
        });

        // 等待全部执行完
//        CompletableFuture allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
//        allOf.get();

        // 只需要有一个执行完
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
        anyOf.get();
        System.out.println("main....end....." + anyOf.get());

商品业务 & 认证服务

环境搭建
  • 修改host文件

    192.168.91.100		item.gulimall.com
    192.168.91.100		auth.gulimall.com
    

    [外链图片转存中…(img-V8Fq96us-1684464779564)]

  • 上传商品详情页资源文件

    • 新建文件夹

      cd /mydata/nginx/html/static
      
      mkdir item
      
    • 上传资源

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Niq2rUZg-1684464779564)(images/谷粒商城项目笔记/image-20220619185729846.png)]

  • 上传登录注册页资源文件

    • 新建文件夹

      cd /mydata/nginx/html/static
      
      mkdir login
      
      mkdir reg
      
    • 上传资源文件

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQZghHhH-1684464779564)(images/谷粒商城项目笔记/image-20220620190134368.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X8ldEUZn-1684464779564)(images/谷粒商城项目笔记/image-20220620190217887.png)]

整合短信验证码
1、短信验证我们选择的是阿里云的短信服务

https://market.aliyun.com/products/57124001/cmapi00037170.html?spm=5176.730005.result.4.6276123eDrvPdz&innerSource=search_%E4%B8%89%E7%BD%91%E7%9F%AD%E4%BF%A1%E6%8E%A5%E5%8F%A3#sku=yuncode3117000001

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sv3rwQb9-1684464779565)(images/谷粒商城项目笔记/image-20220620193819698.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OqJfQtnI-1684464779565)(images/谷粒商城项目笔记/image-20220620194026673.png)]

[外链图片转存中…(img-kComzpoD-1684464779565)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwwfeU7D-1684464779565)(images/谷粒商城项目笔记/image-20220620194214068.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlXebTP5-1684464779565)(images/谷粒商城项目笔记/image-20220620194243185.png)]

往下翻

[外链图片转存中…(img-STvfMBNw-1684464779566)]

点击 去调试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMEROylG-1684464779566)(images/谷粒商城项目笔记/image-20220620195327734.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-prLwXewf-1684464779566)(images/谷粒商城项目笔记/image-20220620195421316.png)]

[外链图片转存中…(img-SOuyL1mf-1684464779566)]

使用postman调试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0xsqH58-1684464779567)(images/谷粒商城项目笔记/image-20220620201003988.png)]

[外链图片转存中…(img-vXXRJadB-1684464779567)]

点击Send 手机就会接收到验证码。

下面有使用java发送验证码:

[外链图片转存中…(img-i1jbPTJD-1684464779567)]

[外链图片转存中…(img-IU1MG0XO-1684464779568)]

[外链图片转存中…(img-EKu0By8Q-1684464779568)]

2、选择对应短信服务进行开通

在云市场就能看到购买的服务

[外链图片转存中…(img-g761hPPU-1684464779568)]

3、验证短信功能是否能发送

在购买短信的页面,能进行调试短信

[外链图片转存中…(img-e9zZJxQn-1684464779569)]

输入对应手机号,appCode 具体功能不做演示

[外链图片转存中…(img-8ztC0m7z-1684464779569)]

4、使用 Java 测试短信是否能进行发送

往下拉找到对应 Java 代码

注意:

​ 服务商提供的接口地址请求参数都不同,请参考服务商提供的测试代码

@Test
public void contextLoads() {
   String host = "http://dingxin.market.alicloudapi.com";
	    String path = "/dx/sendSms";
	    String method = "POST";
	    String appcode = "你自己的AppCode";
	    Map<String, String> headers = new HashMap<String, String>();
	    //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
	    headers.put("Authorization", "APPCODE " + appcode);
	    Map<String, String> querys = new HashMap<String, String>();
	    querys.put("mobile", "159xxxx9999");
	    querys.put("param", "code:1234");
	    querys.put("tpl_id", "TP1711063");
	    Map<String, String> bodys = new HashMap<String, String>();


	    try {
	    	/**
	    	* 重要提示如下:
	    	* HttpUtils请从
	    	* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
	    	* 下载
	    	*
	    	* 相应的依赖请参照
	    	* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
	    	*/
	    	HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
	    	System.out.println(response.toString());
	    	//获取response的body
	    	//System.out.println(EntityUtils.toString(response.getEntity()));
	    } catch (Exception e) {
	    	e.printStackTrace();
	    }
}

需要导入对应工具类,参照注释就行

验证码防刷校验

用户要是一直提交验证码

  • 前台:限制一分钟后提交
  • 后台:存入redis 如果有就返回
/**
 * 发送短信验证码
 * @param phone 手机号
 * @return
 */
@GetMapping("/sms/sendCode")
@ResponseBody
public R sendCode(@RequestParam("phone") String phone) {
    // TODO 1、接口防刷
    // 先从redis中拿取
    String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    if(!StringUtils.isEmpty(redisCode)) {
        // 拆分
        long l = Long.parseLong(redisCode.split("_")[1]);
        // 当前系统事件减去之前验证码存入的事件 小于60000毫秒=60秒
        if (System.currentTimeMillis() -l < 60000) {
            // 60秒内不能再发
            R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
        }
    }
    // 2、验证码的再次效验
    // 数据存入 =》redis key-phone value - code sms:code:131xxxxx - >45678
    String code = UUID.randomUUID().toString().substring(0,5).toUpperCase();
    // 拼接验证码
    String substring = code+"_" + System.currentTimeMillis();
    // redis缓存验证码 防止同一个phone在60秒内发出多次验证吗
    redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);

    // 调用第三方服务发送验证码
    thirdPartFeignService.sendCode(phone,code);
    return R.ok();
}
一步一坑注册页环境
1、编写 vo 接收页面提交
  • 使用到了 JSR303校验
/**
 * 注册数据封装Vo
 * @author gcq
 * @Create 2020-11-09
 */
@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 = "手机格式不正确")
    private String phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;
}
2、页面提交数据与Vo一致

设置 name 属性与 Vo 一致,方便将传递过来的数据转换成 JSON

[外链图片转存中…(img-VSkKU6A2-1684464779569)]

3、数据校验
/**
 * //TODO 重定向携带数据,利用session原理,将数据放在session中,
 * 只要跳转到下一个页面取出这个数据,session中的数据就会删掉
 * //TODO分布式下 session 的问题
 * RedirectAttributes redirectAttributes 重定向携带数据
 * redirectAttributes.addFlashAttribute("errors", errors); 只能取一次
 * @param vo 数据传输对象
 * @param result 用于验证参数
 * @param redirectAttributes 数据重定向
 * @return
 */
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
                     RedirectAttributes redirectAttributes) {
    // 校验是否通过
    if (result.hasErrors()) {
        // 拿到错误信息转换成Map
        Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        //用一次的属性
        redirectAttributes.addFlashAttribute("errors",errors);
        // 校验出错,转发到注册页
        return "redirect:http://auth.gulimall.com/reg.html";
    }

    // 将传递过来的验证码 与 存redis中的验证码进行比较
    String code = vo.getCode();
    String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
    if (!StringUtils.isEmpty(s)) {
        // 验证码和redis中的一致
        if(code.equals(s.split("_")[0])) {
            // 删除验证码:令牌机制
            redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
            // 调用远程服务,真正注册
            R r = memberFeignService.regist(vo);
            if (r.getCode() == 0) {
                // 远程调用注册服务成功
                return "redirect:http://auth.gulimall.com/login.html";
            } else {
                Map<String, String> errors = new HashMap<>();
                errors.put("msg",r.getData(new TypeReference<String>(){}));
                redirectAttributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        } else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code", "验证码错误");
            redirectAttributes.addFlashAttribute("code", "验证码错误");
            // 校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
    } else {
        Map<String, String> errors = new HashMap<>();
        errors.put("code", "验证码错误");
        redirectAttributes.addFlashAttribute("code", "验证码错误");
        // 校验出错,转发到注册页
        return "redirect:http://auth.gulimall.com/reg.html";
    }
}
4、前端页面接收错误信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NK7Uss1J-1684464779570)(images/谷粒商城项目笔记/image-20201110101306173.png)]

5、异常机制 & 用户注册
  • 用户注册单独抽出了一个服务

Controller

/**
 * 注册
 * @param registVo
 * @return
 */
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo registVo) {
    try {
        memberService.regist(registVo);
    } catch (PhoneExsitException e) {
        // 返回对应的异常信息
       return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
    } catch (UserNameExistException e) {
        return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
    }
    return R.ok();
}
@Override
public void regist(MemberRegistVo registVo) {
    MemberDao memberDao = this.baseMapper;
    MemberEntity entity = new MemberEntity();

    // 设置默认等级
    MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
    entity.setLevelId(memberLevelEntity.getId());


    // 检查手机号和用户名是否唯一
    checkPhoneUnique(registVo.getPhone());
    checkUserNameUnique(registVo.getUserName());

    entity.setMobile(registVo.getPhone());
    entity.setUsername(registVo.getUserName());

    //密码要加密存储
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encode = passwordEncoder.encode(registVo.getPassword());
    entity.setPassword(encode);

    memberDao.insert(entity);
}

@Override
public void checkPhoneUnique(String phone) throws PhoneExsitException {
    MemberDao memberDao = this.baseMapper;
    Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
    if (mobile > 0) {
        throw new PhoneExsitException();
    }
}

@Override
public void checkUserNameUnique(String username) throws UserNameExistException {
    MemberDao memberDao = this.baseMapper;
    Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
    if (count > 0) {
        throw new PhoneExsitException();
    }
}

此处引入一个问题

  • 密码是直接存入数据库吗? 这样子会导致数据的不安全,
  • 引出了使用 MD5进行加密,但是MD5加密后,别人任然可以暴力破解
  • 可以使用加盐的方式,将密码加密后,得到一串随机字符,
  • 随机字符和密码和进行验证相同结果返回true否则false

至此注册相关结束~

账号密码登录
1、定义 Vo 接收数据提交
/**
 * @author gcq
 * @Create 2020-11-10
 */
@Data
public class UserLoginVo {
    private String loginacct;
    private String password;
}

同时需要保证前端页面提交字段与 Vo 类中一致

2、在 Member 服务中编写接口
@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 memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>()
            .eq("username", loginacct).or().
                    eq("mobile", loginacct));
    if (memberDao == null) {
        // 登录失败
        return null;
    } else {
        // 获取数据库的密码
        String passwordDB = memberEntity.getPassword();
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 和用户密码进行校验
        boolean matches = passwordEncoder.matches(password, passwordDB);
        if(matches) {
            // 密码验证成功 返回对象
            return memberEntity;
        } else {
            return null;
        }
    }
}
分布式 Session不共享不同步问题

我们在auth.gulimall.com中保存session,但是网址跳转到 gulimall.com中,取不出auth.gulimall.com中保存的session,这就造成了微服务下的session不同步问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwqrDfqk-1684464779570)(images/谷粒商城项目笔记/image-20201111103637615.png)]

1、Session同步解决方案-分布式下session共享问题

同一个服务复制多个,但是session还是只能在一个服务上保存,浏览器也是只能读取到一个服务的session

[外链图片转存中…(img-nciPpBQs-1684464779570)]

2、Session共享问题解决-session复制

[外链图片转存中…(img-0alkFR0A-1684464779570)]

3、Session共享问题解决-客户端存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBYzyhtl-1684464779571)(images/谷粒商城项目笔记/image-20201111104913888.png)]

4、Session共享问题解决-hash一致性

[外链图片转存中…(img-odP5VouD-1684464779571)]

5、Session共享问题解决-统一存储

[外链图片转存中…(img-FPQt9lrA-1684464779571)]

SpringSession整合
1、官网文档 阅读
  • 进入到 Spring Framework

[外链图片转存中…(img-9jrug9WN-1684464779572)]

2、选择Spring Session文档

[外链图片转存中…(img-CMsOAZbr-1684464779572)]

[外链图片转存中…(img-xU1xrIBS-1684464779572)]

3、开始使用Spring Session

[外链图片转存中…(img-rGtwvfmV-1684464779572)]

[外链图片转存中…(img-FQW8vZsZ-1684464779572)]

整合SpringBoot
1、添加Pom.xml依赖

https://docs.spring.io/spring-session/docs/2.5.0/reference/html5/#samples

auth 服务、product 服务、 search 服务 pom文件


<dependency>
  <groupId>org.springframework.sessiongroupId>
  <artifactId>spring-session-data-redisartifactId>
dependency>
2、application.yml 配置
spring:
  session:
    store-type: redis

**主启动类增加注解:@EnableRedisHttpSession **

3、reids配置

[外链图片转存中…(img-Wo4JUXnH-1684464779573)]

4、启动类加上如下注解
@EnableRedisHttpSession // 整合spring session
自定义 SpringSession 完成 Session 子域共享
CookieSerializer

api文档参考:https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#api-cookieserializer

[外链图片转存中…(img-Hemiq2rO-1684464779573)]

指定redis序列化

文档地址:

https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#api-redisindexedsessionrepository-config

[外链图片转存中…(img-aasYShtK-1684464779573)]

redis中json序列化

官网文档地址:https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#samples

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUNGwEeS-1684464779574)(images/谷粒商城项目笔记/image-20210101125216426.png)]

提供的实例:

https://github.com/spring-projects/spring-session/blob/2.4.1/spring-session-samples/spring-session-sample-boot-redis-json/src/main/java/sample/config/SessionConfig.java

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIfbi8oF-1684464779574)(images/谷粒商城项目笔记/image-20210101125303807.png)]

/**
 * SpringSession整合子域
 * 以及redis数据存储为json
 * @author gcq
 * @Create 2020-11-11
 */
@Configuration
public class GulimallSessionConfig {

    /**
     * 设置cookie信息
     * @return
     */
    @Bean
    public CookieSerializer CookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        // 设置一个域名的名字
        cookieSerializer.setDomainName("gulimall.com");
        // cookie的路径
        cookieSerializer.setCookieName("GULIMALLSESSION");
        return cookieSerializer;
    }

    /**
     * 设置json转换
     * @return
     */
    @Bean
    public RedisSerializer springSessionDefaultRedisSerializer() {
        // 使用jackson提供的转换器
        return new GenericJackson2JsonRedisSerializer();
    }

}

SpringSession 原理
/**
 * 核心原理
 * 1、@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
 *      1、给容器中添加了一个组件
 *          sessionRepository = 》》》【RedisOperationsSessionRepository】 redis 操作 session session的增删改查封装类
 *      2、SessionRepositoryFilter==>:session存储过滤器,每个请求过来必须经过Filter
 *          1、创建的时候,就自动从容器中获取到了SessionRepostiory
 *          2、原始的request,response都被包装了 SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper
 *          3、以后获取session.request.getSession()
 *              SessionRepositoryResponseWrapper
 *          4、wrappedRequest.getSession() ==>SessionRepository
 *
 *          装饰者模式
 *          spring-redis的相关功能:
 *                 执行session相关操作后,redis里面存储的时间也会刷新
 */

核心源码是:

  • SessionRepositoryFilter 类下面的 doFilterInternal 方法

  • 及那个 requestresponse 包装成 SessionRepositoryRequestWrapper

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Ld9Hw4X-1684464779574)(images/谷粒商城项目笔记/image-20201111195249024.png)]

授权认证

OAuth2.0
  • **OAuth:**OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们的数据的内容
  • **OAuth2.0:**对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保存用户数据的安全和隐私,第三方网站访问用户数据前都需要显示向用户授权

文档地址:

相关流程分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v3fH5cC1-1684464779575)(images/谷粒商城项目笔记/image-20201110154532752.png)]

微博登录准备工作
1、进入微博开放平台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O44DfqHV-1684464779575)(images/谷粒商城项目笔记/image-20201110154702360.png)]

2、登录微博,进入微连接,选择网站接入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JlgLpbah-1684464779576)(images/谷粒商城项目笔记/image-20201110160834589.png)]

3、选择立即接入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLGe0COb-1684464779576)(images/谷粒商城项目笔记/image-20201110161001013.png)]

4、创建自己的应用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtWibxoT-1684464779577)(images/谷粒商城项目笔记/image-20201110161032203.png)]

5、我们可以在开发阶段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hR2mnZu5-1684464779577)(images/谷粒商城项目笔记/image-20201110161152105.png)]

6、进入高级信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jiXUaJOJ-1684464779577)(images/谷粒商城项目笔记/image-20201110161407018.png)]

7、添加测试账号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrRH0cYb-1684464779577)(images/谷粒商城项目笔记/image-20201110161451881.png)]

8、进入文档

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CxwQpnIr-1684464779578)(images/谷粒商城项目笔记/image-20201110161634486.png)]

微博登录代码实现
微博登录流程[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XGMYGxH-1684464779578)(images/谷粒商城项目笔记/image-20201231084733753.png)]
注册流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cItfYB9l-1684464779578)(images/谷粒商城项目笔记/image-20201231084909415.png)]

账号密码登录流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLTRcqf2-1684464779578)(images/谷粒商城项目笔记/image-20201231012134722.png)]

手机验证码发送流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dE8EH0WU-1684464779579)(images/谷粒商城项目笔记/image-20201231012207446.png)]

查看微博开放平台文档

https://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEa5YP84-1684464779579)(images/谷粒商城项目笔记/image-20201111093019560.png)]

点击微博登录后,跳转到微博授权页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-juKW57ge-1684464779579)(images/谷粒商城项目笔记/image-20201111093153199.png)]

用户授权后调用回调接口,并带上参数code换取AccessToken
/**
     * 回调接口
     * @param code
     * @return
     * @throws Exception
     */
    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code) throws Exception {
        // 1、根据code换取accessToken
        Map<String, String> map = new HashMap<>();
        map.put("client_id", "1133714539");
        map.put("client_secret", "f22eb330342e7f8797a7dbe173bd9424");
        map.put("grant_type", "authorization_code");
        map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code", code);


        HttpResponse response = HttpUtils.doPost("https://api.weibo.com",
                "/oauth2/access_token",
                "post",
                new HashMap<>(),
                map,
                new HashMap<>());

        // 状态码为200请求成功
        if (response.getStatusLine().getStatusCode() == 200 ){
            // 获取到了accessToken
            String json = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
            R r = memberFeignService.OAuthlogin(socialUser);
            if (r.getCode() == 0) {
                MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() {
                });
                log.info("登录成功:用户:{}",data.toString());

                // 2、登录成功跳转到首页
                return "redirect:http://gulimall.com";
            } else {
                // 注册失败
                return "redirect:http://auth.gulimall.com/login.html";
            }
        } else {
            // 请求失败
            // 注册失败
            return "redirect:http://auth.gulimall.com/login.html";
        }

        // 2、登录成功跳转到首页
        return "redirect:http://gulimall.com";
    }
拿到AccessToken 请求对应接口拿到信息
@Override
public MemberEntity login(SocialUser vo) {
    // 登录和注册合并逻辑
    String uid = vo.getUid();
    MemberDao memberDao = this.baseMapper;
    // 根据社交用户的uuid查询
    MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>()
            .eq("social_uid", uid));
    // 能查询到该用户
    if (memberEntity != null ){
        // 更新对应值
        MemberEntity update = new MemberEntity();
        update.setId(memberEntity.getId());
        update.setAccessToken(vo.getAccess_token());
        update.setExpiresIn(vo.getExpires_in());

        memberDao.updateById(update);

        memberEntity.setAccessToken(vo.getAccess_token());
        memberEntity.setExpiresIn(vo.getExpires_in());
        return memberEntity;
    } else {
        // 2、没有查询到当前社交用户对应的记录就需要注册一个
        MemberEntity regist = new MemberEntity();
        try {
            Map<String,String> query = new HashMap<>();
            // 设置请求参数
            query.put("access_token",vo.getAccess_token());
            query.put("uid",vo.getUid());
            // 发送get请求获取社交用户信息
            HttpResponse response = HttpUtils.doGet("https://api.weibo.com/",
                    "2/users/show.json",
                    "get",
                    new HashMap<>(),
                    query);
            // 状态码为200 说明请求成功
            if (response.getStatusLine().getStatusCode() == 200){
                // 将返回结果转换成json
                String json = EntityUtils.toString(response.getEntity());
                // 利用fastjson将请求返回的json转换为对象
                JSONObject jsonObject = JSON.parseObject(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(vo.getUid());
        regist.setAccessToken(vo.getAccess_token());
        regist.setExpiresIn(vo.getExpires_in());
        memberDao.insert(regist);
        return regist;
    }
}

购物车功能

1、导入资源
  • 导入资源

    cd /mydata/nginx/html/static/
    
    mkdir cart
    

    [外链图片转存中…(img-VlxbJIdg-1684464779579)]

  • 修改Host文件

    [外链图片转存中…(img-j9YukrPr-1684464779580)]

  • 323

  • 23

  • 23

  • 23

  • 23

  • 2

  • 32

SSO-单点登录
什么是SSO

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的 。

参考:https://gitee.com/xuxueli0323/xxl-sso

[外链图片转存中…(img-IohQVUBt-1684464779580)]
[外链图片转存中…(img-roBKjcPn-1684464779580)]

消息队列

Docker 安装RabbitMQ
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

#4369, 25672 (Erlang发现&集群端口)
#5672, 5671 (AMQP端口)
#15672 (web管理后台端口)
#61613, 61614 (STOMP协议端口)
#1883, 8883 (MQTT协议端口)

 # 自动启动
docker update rabbitmq --restart=always

http://192.168.91.100:15672/

用户名/密码:guest/guest

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aO592Zf6-1684464779580)(images/谷粒商城项目笔记/image-20201116102734767.png)]

[外链图片转存中…(img-752Anvjw-1684464779581)]

RabbitMQ 运行机制

AMQP 中的消息路由

AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 ExchangeBinding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIxfyzli-1684464779581)(images/谷粒商城项目笔记/image-20201116104235856.png)]

Exchange 类型

Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型

[外链图片转存中…(img-fMiSd6CH-1684464779581)]

[外链图片转存中…(img-u4Rvz3Jz-1684464779582)]

RabbitMQ 整合
1、引入 Spring-boot-starter-amqp
      <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>

2、application.yml配置
spring:
  rabbitmq:
    host: 192.168.56.10
    port: 5672
    virtual-host: /
3、测试RabbitMQ
1、AmqpAdmin:管理组件
 /**
     * 创建Exchange
     * 1、如何利用Exchange,Queue,Binding
     *      1、使用AmqpAdmin进行创建
     * 2、如何收发信息
     */
    @Test
    public void contextLoads() {
        //	public DirectExchange(
        //	String name, 交换机的名字
        //	boolean durable, 是否持久
        //	boolean autoDelete, 是否自动删除
        //	Map arguments)
        //	{
        DirectExchange directExchange = new DirectExchange("hello-java.exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("Exchange[{}]创建成功:","hello-java.exchange");
    }

    /**
     * 创建队列
     */
    @Test
    public void createQueue() {
        // public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments) {
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        log.info("Queue[{}]:","创建成功");
    }


/**
     * 绑定队列
     */
    @Test
    public void createBinding() {
        // public Binding(String destination, 目的地
        // DestinationType destinationType, 目的地类型
        // String exchange,交换机
        // String routingKey,//路由键
        Binding binding = new Binding("hello-java-queue",
                Binding.DestinationType.QUEUE,
                "hello-java.exchange",
                "hello.java",null);
        amqpAdmin.declareBinding(binding);
        log.info("Binding[{}]创建成功","hello-java-binding");
    }
2、RabbitTemplate:消息发送处理组件
 @Autowired
    @Test
    public void sendMessageTest() {
        for(int i = 1; i <=5; i++) {
            if(i%2==0) {
                OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                reasonEntity.setId(1l);
                reasonEntity.setCreateTime(new java.util.Date());
                reasonEntity.setName("哈哈");
                //
                String msg = "Hello World";
                // 发送的对象类型的消息,可以是一个json
                rabbitTemplate.convertAndSend("hello-java.exchange","hello.java",reasonEntity);
            } else {
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello-java.exchange","hello.java",orderEntity);
            }
            log.info("消息发送完成{}");
        }

    }
RabbitMQ消息确认机制 - 可靠到达
  • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
  • publisher confirmCallback 确认模式
  • publisher returnCallback 未投递到 queue 退回
  • consumer ack 机制

[外链图片转存中…(img-pPvvZFP8-1684464779582)]

可靠抵达 - ConfirmCallback

spring.rabbitmq.publisher-confirms=true

在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback

CorrelationData 用来表示当前消息唯一性

消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback

被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback

可靠抵达 - ReturnCallback

spring.rabbitmq.publisher-retuns=true

spring.rabbitmq.template.mandatory=true

confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式

这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据

可靠抵达 - Ack 消息确认机制
  • 消费者获取到消息,成功处理,可以回复Ack给Broker
    • basic.ack 用于肯定确认:broker 将移除此消息
    • basic.nack 用于否定确认:可以指定 beoker 是否丢弃此消息,可以批量
    • basic.reject用于否定确认,同上,但不能批量
  • 默认,消息被消费者收到,就会从broker的queue中移除
  • 消费者收到消息,默认自动ack,但是如果无法确定此消息是否被处理完成,或者成功处理,我们可以开启手动ack模式
    • 消息处理成功,ack(),接受下一条消息,此消息broker就会移除
    • 消息处理失败,nack()/reject() 重新发送给其他人进行处理,或者容错处理后ack
    • 消息一直没有调用ack/nack方法,brocker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人
RabbitMQ 延时队列(实现定时任务)

场景:

比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品

常用解决方案:

Spring的schedule 定时任务轮询数据库

缺点:

消耗系统内存,增加了数据库的压力,存在较大的时间误差

**解决:**rabbitmq的消息TTL和死信Exchange结合

使用场景

[外链图片转存中…(img-xzalSNYv-1684464779582)]

时效问题

上一轮扫描刚好扫描,而这个时候刚好下了订单,就没有扫描到,下一轮扫描的时候,订单还没有过期,等到订单过期后30分钟才被扫描到

[外链图片转存中…(img-dL3qWkFu-1684464779582)]

消息的TTL(Time To Live)
  • 消息的TTL 就是消息的存活时间
  • RabbitMQ可以对队列还有消息分别设置TTL
    • 对队列设置就是没有消费者连着的保持时间,也可以对每一个消息单独的设置,超过了这个时间我们可以认为这个消息他死了,称之为死信
    • 如果队列设置了,消息也设置了,那么会取小,所以一个消息如果被路由到不同的队列中,这个消息死亡时间有可能不一样的(不同队列设置),这里讲的是单个TTL 因为他是实现延时任务的关键,可以通过设置消息的 expiration 字段或者 x-message-ttl 来设置时间两者是一样的效果
Dead Letter Exchange(DLX)
  • 一个消息在满足如下条件下,会进死信路由,记住这里是路由不是队列,一个路由可以对应很多队列,(什么是死信)
    • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)
    • requeue= false上面的消息的TTL到了,消息过期了。
    • 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
  • Dead Letter Exchange其实就是一种普通的exchange, 和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了自动触发消息的转发,发送到Dead Letter Exchange中去。
  • 我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某一个指定的交换机, 结合C者,其实就可以实现一个延时队列
延时队列实现 - 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E8w5sXBe-1684464779582)(images/谷粒商城项目笔记/image-20201120132805292.png)]

延时队列实现 - 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-udASx52D-1684464779583)(images/谷粒商城项目笔记/image-20201120132922164.png)]

代码实现:

下单场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fxuhoVpA-1684464779583)(images/谷粒商城项目笔记/image-20201120133054368.png)]

模式升级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9VJbpFp-1684464779583)(images/谷粒商城项目笔记/image-20201120133258725.png)]

代码实现:

SpringBoot可以使用@Bean 来初始化Queue、exchange、Biding等

/**
 * 监听队列信息
 * @param orderEntity
 */
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
    System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
    // 确认接收到消息,不批量接收
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

/**
 * 容器中的 Binding、Queue、exchange 都会自动创建,(RabbitMQ没有的情况下)
 * @return
 */
@Bean
public Queue orderDelayQueue(){
    // 特殊参数
    Map<String,Object> map = new HashMap<>();
    // 设置交换器

    map.put("x-dead-letter-exchange", "order-event-exchange");
    // 路由键
    map.put("x-dead-letter-routing-key","order.release.order");
    // 消息过期时间
    map.put("x-message-ttl",60000);
    Queue queue = new Queue("order.delay.queue", true, false, false,map);
    return queue;
}

/**
 * 创建队列
 * @return
 */
@Bean
public Queue orderReleaseOrderQueue() {
    Queue queue = new Queue("order.release.order.queue", true, false, false);
    return queue;
}

/**
 * 创建交换机
 * @return
 */
@Bean
public Exchange orderEventExchange() {
    return new TopicExchange("order-event-exchange",true,false);
}

/**
 * 绑定关系 将delay.queue和event-exchange进行绑定
 * @return
 */
@Bean
public Binding orderCreateOrderBingding(){
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
}

/**
 * 将 release.queue 和 event-exchange进行绑定
 * @return
 */
@Bean
public Binding orderReleaseOrderBinding(){
    return new Binding("order.release.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.release.order",
            null);
}
如何保证消息可靠性 - 消息丢失 & 消息重复
1、消息丢失
  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-Catch) ,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
  • 消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
    • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2、消息重复
  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready, Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    • 使用防重表(redis/mysq|) ,发送消息每一 个都有业务的唯一 标识,处理过就不用处理
    • rabbitMQ的每一个消息都有redelivered字段, 可以获取是否是被重新投递过来的,而不是第一次投递过来的
3、消息积压
  • 消费者积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

订单

环境创建
  • 上传静态页面

    cd /mydata/nginx/html/static/
    
    mkdir order
    
    cd order/
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0mHnSTJ9-1684464779583)(images/谷粒商城项目笔记/image-20220623135450743.png)]

    cd /mydata/nginx/html/static/order/
    
    mkdir list
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTdEhivn-1684464779583)(images/谷粒商城项目笔记/image-20220623135730971.png)]

    cd /mydata/nginx/html/static/order/
    
    mkdir confirm
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTBM6gmF-1684464779584)(images/谷粒商城项目笔记/image-20220623135958385.png)]

    cd /mydata/nginx/html/static/order/
    
    mkdir pay
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-de4GdX9R-1684464779584)(images/谷粒商城项目笔记/image-20220623140213606.png)]

    192.168.91.100                        order.gulimall.com
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x42BEgou-1684464779584)(images/谷粒商城项目笔记/image-20220623140649267.png)]

  • 2

  • 2

  • 2

  • 2

订单中心

电商系列涉及到 3 流,分别为信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来

订单模块是电商系统的枢纽,在订单这个模块上获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息疏通

1、订单构成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IN9UhrCB-1684464779584)(images/谷粒商城项目笔记/image-20201117102129127.png)]

1、用户信息

用户信息包括是用户账号、用户等级、用户的收货地址、收货人、收货人电话、用户账号需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

2、订单基础信息

订单基础信息是订单流转的核心,其包括订单类型,父/子订单、订单编号、订单状态、订单流转时间等

  1. 订单类型包括实体订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分
  2. 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需
  3. 要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
  4. 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
  5. 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
  6. 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等
3、商品信息

商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户

下单行为记录的用户下单数量,商品合计价格等

4、优惠信息

优惠信息记录用户参与过的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优

惠卷信息,优惠卷满足条件的优惠卷需要展示出来,另外虚拟币抵扣信息等进行记录

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

5、支付信息

支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务

通过订单号和流水单号与支付通道进行对账使用。

支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个一-余额支付+第三方支付。

商品总金额,每个商品加总后的金额:运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,

优惠券优惠金额,虚拟积分或者虛拟币抵扣的金額,会员折扣的金额等之和;实付金额,用户实际需要

付款的金额。

用户实付金额=商品总金额+运费 - 优惠总金额

6、物流信息

物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来
获取和向用户展示物流每个状态节点。

2、订单状态
1、待付款

用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支
付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超
时后将自动取消订单,订单变更关闭状态。

2、已付款/代发货

用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调动、配货、分拣,出库等操作

3、待收货/已发货

仓库将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时熟悉商品的物流状态

4、已完成

用户确认收货后吗,订单交易完成,后续支付则进行计算,如果订单存在问题进入售后状态

5、已取消

付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态

6、售后中

用户在付款后申请退款,或商家发货后用户申请退货

售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待

商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单

状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了-套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与020订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一一个正常的网购步骤:订单生成>支付订单->卖家发货一确认收货>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,

可概括如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8MpKq5W-1684464779585)(images/谷粒商城项目笔记/image-20201117104613032.png)]

1、订单创建与支付

  1. 订单创建前需要预览订单,选择收货信息等
  2. 订单创建需要锁定库存,库存有才可创建,否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
  5. 支付的每笔流水都需要记录,以待查账
  6. 订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅

2、逆向流程

  1. 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
  2. 订单取消**,用户主动取消订单和用户超时未支付**,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的
幂等性处理
订单业务
1、搭建环境

在订单服务下准备好页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwg3RlOf-1684464779585)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105095210202.png)]

可以发现订单结算页,包含以下信息:

1.收货人信息:有更多地址,即有多个收货地址,其中有一个默认收货地址

2.支付方式:货到付款下在线支付,不需要后台提供

3.送货清单:配送方式(不做)及商品列表(根据购物车选中的skuld到数据库中查询)

4.发票:不做

5.优惠:查询用户领取的优惠券(不做)及可用积分(京豆)

整合SpringSession

1、引入pom

  
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>

2、配置文件添加

Spring:
	session:
    	store-type: redis

3、启动类加注解

@EnableRedisHttpSession

4、修改页面中登录

						<li style="border: 0;">
							<a th:if="${session.loginUser != null }" class="aa">[[${session.loginUser.nickname}]]a>
							<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/login.html">你好请登录a>
						li>
						<li>
							<a th:if="${session.loginUser == null }" style="color: red;" href="http://auth.gulimall.com/reg.html" class="li_2">免费注册a>
						li>
订单登录拦截

任何请求都需要先经过拦截器的验证,才能去执行目标方法,这里是用户是否登录,用户登录了则放行,否则跳转到登陆页面

  /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        // 指定的请求不拦截
        boolean match1 = new AntPathMatcher().match("/order/order/status/**", requestURI);
        boolean match2 = new AntPathMatcher().match("/payed/notify", requestURI);
        if (match1 || match2) {
            return true;
        }
        MemberRespVo memberRespVo= (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (memberRespVo != null) { // 用户登陆了
            loginUser.set(memberRespVo); // 放到共享数据中
            return true;
        } else { // 用户没登录
            // 给前端显示提示信息
            request.getSession().setAttribute("msg","请先进行登录");
            // 重定向到登录页面
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
2、订单确认页

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1xs5b566-1684464779585)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105142735857.png)]

根据图片中商品信息抽取成Vo

2.1、抽取Vo

订单确认OrderConfirmVo

/**
 * 订单确认页需要的数据
 * @author gcq
 * @Create 2020-11-17
 */

public class OrderConfirmVo {

    // 收货地址,ums_member_receive_address
    @Getter @Setter
    List<MemberAddressVo> address;

    // 所有选中的购物项
    @Getter @Setter
    List<OrderItemVo> item;

    // 发票记录...

    /**
     * 优惠卷信息
     */
    @Getter @Setter
    Integer integration;

    /**
     * 订单总额
     */
    BigDecimal total;
    public BigDecimal getTotal() {
        BigDecimal sum = new BigDecimal("0");
        if(item != null) {
            for (OrderItemVo orderItemVo : item) {
                BigDecimal multiply = orderItemVo.getPrice().multiply(new BigDecimal(orderItemVo.getCount().toString()));
                sum = sum.add(multiply);
            }
        }
        return sum;
    }
    @Getter @Setter
    Map<Long,Boolean> stocks;
    /**
     * 应付价格
     */
    BigDecimal payPrice;

    public BigDecimal getPayPrice() {
        BigDecimal sum = new BigDecimal("0");
        if(item != null) {
            for (OrderItemVo orderItemVo : item) {
                BigDecimal multiply = orderItemVo.getPrice().multiply(new BigDecimal(orderItemVo.getCount().toString()));
                sum = sum.add(multiply);
            }
        }
        return sum;
    }

    @Setter
    private Integer count;

    /**
     * 遍历item 拿到商品的数量
     * @return
     */
    public Integer getCount() {
        Integer i = 0;
        if (item != null) {
            for (OrderItemVo orderItemVo : item) {
                i+=orderItemVo.getCount();
            }
        }
        return i;
    }

    @Getter @Setter
    private String orderToken;

}

商品项orderItemVo

/**
 * 商品项
 * @author gcq
 * @Create 2020-11-17
 */
@Data
public class OrderItemVo {
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 购物车中是否选中
     */
    private Boolean check = true;
    /**
     * 商品的标题
     */
    private String title;
    /**
     * 商品的图片
     */
    private String image;
    /**
     * 商品的属性
     */
    private List<String> skuAttr;
    /**
     * 商品的价格
     */
    private BigDecimal price;
    /**
     * 商品的数量
     */
    private Integer count;
    /**
     * 购物车价格 使用自定义get、set
     */
    private BigDecimal totalPrice;


    private BigDecimal weight;

}

用户地址MemberAddressVo

/**
 * 用户地址信息
 * @author gcq
 * @Create 2020-11-17
 */
@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}
2.2、订单确认页数据查询
   @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = OrderInterceptor.loginUser.get();// 获取当前登录后的用户
        // 异步任务执行之前,先共享数据
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、第一个异步任务 远程查询用户地址信息
        CompletableFuture<Void> memberFuture = CompletableFuture.runAsync(() -> {
            // 在主线程中拿到原来的数据,在父线程里面共享RequestContextHolder
            // 只有共享,拦截其中才会有数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 根据会员id查出之前会员保存过的收货地址信息
            // 远程查询会员服务的收获地址信息
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            log.error("address:{}",address);
            confirmVo.setAddress(address);
        }, executor);

        // 2、第二个异步任务远程查询购物车中选中给的购物项
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
            // 每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 远程查询购物车中的购物项信息,用来结账
            List<OrderItemVo> currentUserCartItem = cartFeignServicea.getCurrentUserCartItem();// 获取当前用户的购物项数据
            log.error("currentUserCartItem:{}",currentUserCartItem);
            confirmVo.setItem(currentUserCartItem);
            // 查询到购物项信息后,再看查询购物的库存信息
        }, executor).thenRunAsync(() -> { // 只要上面任务执行完成,就开始执行thenRunAsync的任务
            // 3、商品是否有库存
            List<OrderItemVo> items = confirmVo.getItem();
            // 批量查询每一个商品的信息
            // 收集好商品id
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            // 远程查询购物项对应的库存信息
            R data = wareFeignService.hasStock(collect);
            // 得到每一个商品的库存状态信息
            List<SkuHasStockVo> hasStockVo = data.getData(new TypeReference<List<SkuHasStockVo>>() {
            });
            if (hasStockVo != null) {
                Map<Long, Boolean> stockMap = hasStockVo.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
                confirmVo.setStocks(stockMap);
                log.error("stockMap:{}",stockMap);
            }
        },executor);


        // 4、查询积分信息
        Integer integration = confirmVo.getIntegration();
        confirmVo.setIntegration(integration);

        // 等两个异步任务都完成
        CompletableFuture.allOf(memberFuture, addressFuture).get();
       // 4、防重令牌
        /**
         * 接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的
         * 不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,
         * 但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,
         * 此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,
         * 流水记录也变成了两条。。。这就没有保证接口幂等性
         */
        // 先是再页面中生成一个随机码把他叫做token先存到redis中,然后放到对象中在页面进行渲染。
        // 用户提交表单的时候,带着这个token和redis里面去匹配如果一直那么可以执行下面流程。
        // 匹配成功后再redis中删除这个token,下次请求再过来的时候就匹配不上直接返回
        // 生成防重令牌
        String token = UUID.randomUUID().toString().replace("-","");
        // 存到redis中 设置30分钟超时
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(),token,30, TimeUnit.SECONDS);
        // 放到页面进行显示token,然后订单中带着token来请求
        confirmVo.setOrderToken(token);

        return confirmVo;
3、创建订单
1、OrderWebController
 /**
    * @需求描述: 系统管理员 订单组 模块 用户下单功能
    * @创建人: 郭承乾
    * @创建时间: 2021/01/06 12:01
    * @修改需求:
    * @修改人:
    * @修改时间:
    * @需求思路:
    */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes) {
        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        log.error("======================订单创建成功{}:",responseVo);
        // 根据vo中定义的状态码来验证
        if (responseVo.getCode() == 0 ) { // 订单创建成功
            // 下单成功返回到支付页
            model.addAttribute("submitOrderResp",responseVo);
            return "pay";
        } else { // 下单失败
            // 根据状态码验证对应的状态
            String msg = "下单失败";
            switch (responseVo.getCode()) {
                case 1: msg += "订单信息过期,请刷新后再次提交"; break;
                case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
                case 3: msg += "库存锁定失败,商品库存不足"; break;
            }
            redirectAttributes.addFlashAttribute("msg",msg);
            // 重新回到订单确认页面
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }
2、Service

具体业务

 @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        // 先将参数放到共享变量中,方便之后方法使用该参数
        confirmVoThreadLocal.set(vo);
        // 接收返回数据
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        response.setCode(0);
        // 通过拦截器拿到用户的数据
        MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();
        /**
         * 不使用原子性验证令牌
         *      1、用户带着两个订单,提交速度非常快,两个订单的令牌都是123,去redis里面查查到的也是123。
         *          两个对比都通过,然后来删除令牌,那么就会出现用户重复提交的问题,
         *      2、第一次差的快,第二次查的慢,只要没删就会出现这些问题
         *      3、因此令牌的【验证和删除必须保证原子性】
         *      String orderToken = vo.getOrderToken();
         *      String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
         *         if (orderToken != null && orderToken.equals(redisToken)) {
         *             // 令牌验证通过 进行删除
         *             redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
         *         } else {
         *             // 不通过
         *         }
         */
        // 验证令牌【令牌的对比和删除必须保证原子性】
        // 因此使用redis中脚本来进行验证并删除令牌
        // 0【删除失败/验证失败】 1【删除成功】
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        /**
         * redis lur脚本命令解析
         * if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
         *  1、redis调用get方法来获取一个key的值,如果这个get出来的值等于我们传过来的值
         *  2、然后就执行删除,根据这个key进行删除,删除成功返回1,验证失败返回0
         *  3、删除否则就是0
         *  总结:相同的进行删除,不相同的返回0
         * 脚本大致意思
         */
        // 拿到令牌
        String orderToken = vo.getOrderToken();
        /**
         * 	public  T execute(RedisScript script // redis的脚本
         * 	    , List keys // 对应的key 参数中使用了Array.asList 将参数转成list集合
         * 	    , Object... args) { // 要删除的值
         */
        // 原子验证和删除
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                , Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId())
                , orderToken);
        if (result  == 0L) { // 验证令牌验证失败
            // 验证失败直接返回结果
            response.setCode(1);
            return response;
        } else { // 原子验证令牌成功
            // 下单 创建订单、验证令牌、验证价格、验证库存
            // 1、创建订单、订单项信息
            OrderCreateTo order = createOrder();
            // 2、应付总额
            BigDecimal payAmount = order.getOrder().getPayAmount();
            // 应付价格
            BigDecimal payPrice = vo.getPayPrice();
            /**
             * 电商项目对付款的金额精确到小数点后面两位
             * 订单创建好的应付总额 和购物车中计算好的应付价格求出绝对值。
             */
            if(Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                // 金额对比成功 保存订单
                saveOrder(order);
                // 创建锁定库存Vo
                WareSkuLockedVo wareSkuLockedVo = new WareSkuLockedVo();
                // 准备好商品项
                List<OrderItemVo> lock = order.getOrderItem().stream().map(orderItemEntity -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    // 商品购买数量
                    orderItemVo.setCount(orderItemEntity.getSkuQuantity());
                    // skuid 用来查询商品信息
                    orderItemVo.setSkuId(orderItemEntity.getSkuId());
                    // 商品标题
                    orderItemVo.setTitle(orderItemEntity.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                // 订单号
                wareSkuLockedVo.setOrderSn(order.getOrder().getOrderSn());
                // 商品项
                wareSkuLockedVo.setLocks(lock);
                // 远程调用库存服务锁定库存
                R r = wareFeignService.orderLockStock(wareSkuLockedVo);
                if (r.getCode() == 0) { // 库存锁定成功
                    // 将订单对象放到返回Vo中
                    response.setOrder(order.getOrder());
                    // 设置状态码
                    response.setCode(0);
                    // 订单创建成功发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange"
                            ,"order.create.order"
                            ,order.getOrder());
                    return response;
                } else {
                    // 远程锁定库存失败
                    response.setCode(3);
                    return response;
                }
            } else {
                // 商品价格比较失败
                response.setCode(2);
                return response;
            }
        }
    }
 /**
     * 创建订单和订单项
     * @return
     */
    private OrderCreateTo createOrder() {
        OrderCreateTo orderCreateTo = new OrderCreateTo();
        // 1、生成订单号
        String orderSn = IdWorker.getTimeId();
        // 2、构建订单
        OrderEntity orderEntity = buildOrder(orderSn);
        // 3、构建订单项
        List<OrderItemEntity> itemEntities = builderOrderItems(orderSn);
        // 4、设置价格、积分相关信息
        computPrice(orderEntity,itemEntities);
        // 5、设置订单项
        orderCreateTo.setOrderItem(itemEntities);
        // 6、设置订单
        orderCreateTo.setOrder(orderEntity);
        return orderCreateTo;
    }
 /**
     * 构建订单
     * @param orderSn
     * @return
     */
    private OrderEntity buildOrder(String orderSn) {
        // 拿到共享数据
        OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
        // 用户登录登录数据
        MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();

        OrderEntity orderEntity = new OrderEntity();
        // 设置订单号
        orderEntity.setOrderSn(orderSn);
        // 用户id
        orderEntity.setMemberId(memberRespVo.getId());
        // 根据用户收货地址id查询出用户的收获地址信息
        R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
        FareVo data = fare.getData(new TypeReference<FareVo>() {
        });
        //将查询到的会员收货地址信息设置到订单对象中
        // 运费金额
        orderEntity.setFreightAmount(data.getFare());
        // 城市
        orderEntity.setReceiverCity(data.getMemberAddressVo().getCity());
        // 详细地区
        orderEntity.setReceiverDetailAddress(data.getMemberAddressVo().getDetailAddress());
        // 收货人姓名
        orderEntity.setReceiverName(data.getMemberAddressVo().getName());
        // 收货人手机号
        orderEntity.setReceiverPhone(data.getMemberAddressVo().getPhone());
        // 区
        orderEntity.setReceiverRegion(data.getMemberAddressVo().getRegion());
        // 省份直辖市
        orderEntity.setReceiverProvince(data.getMemberAddressVo().getProvince());
        // 订单刚创建状态设置为 待付款,用户支付成功后将该该状态改成已付款
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        // 自动确认时间
        orderEntity.setAutoConfirmDay(7);

        return orderEntity;
    }
/**
     * 构建订单项
     * @param orderSn
     * @return
     */
    private List<OrderItemEntity> builderOrderItems(String orderSn) {
        // 获取购物车中选中的商品
        List<OrderItemVo> currentUserCartItem = cartFeignServicea.getCurrentUserCartItem();
        if (currentUserCartItem != null && currentUserCartItem.size() > 0) {
            List<OrderItemEntity> collect = currentUserCartItem.stream().map(orderItemVo -> {
                // 构建订单项
                OrderItemEntity itemEntity = builderOrderItem(orderItemVo);
                itemEntity.setOrderSn(orderSn);
                return itemEntity;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }
  /**
     * 构建订单项信息
     * @param cartItem
     * @return
     */
    private OrderItemEntity builderOrderItem(OrderItemVo cartItem) {
        OrderItemEntity itemEntity = new OrderItemEntity();
        // 1、根据skuid查询关联的spuinfo信息
        Long skuId = cartItem.getSkuId();
        R spuinfo = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo spuInfoVo = spuinfo.getData(new TypeReference<SpuInfoVo>() {
        });
        // 2、设置商品项spu信息
        // 品牌信息
        itemEntity.setSpuBrand(spuInfoVo.getBrandId().toString());
        // 商品分类信息
        itemEntity.setCategoryId(spuInfoVo.getCatalogId());
        // spuid
        itemEntity.setSpuId(spuInfoVo.getId());
        // spu_name 商品名字
        itemEntity.setSpuName(spuInfoVo.getSpuName());

        // 3、设置商品sku信息
        // skuid
        itemEntity.setSkuId(skuId);
        // 商品标题
        itemEntity.setSkuName(cartItem.getTitle());
        // 商品图片
        itemEntity.setSkuPic(cartItem.getImage());
        // 商品sku价格
        itemEntity.setSkuPrice(cartItem.getPrice());
        // 商品属性以 ; 拆分
        String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
        itemEntity.setSkuAttrsVals(skuAttr);
        // 商品购买数量
        itemEntity.setSkuQuantity(cartItem.getCount());

        // 4、设置商品优惠信息【不做】
        // 5、设置商品积分信息
        // 赠送积分 移弃小数值
        itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        // 赠送成长值
        itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());

        // 6、订单项的价格信息
        // 这里需要计算商品的分解信息
        // 商品促销分解金额
        itemEntity.setPromotionAmount(new BigDecimal("0"));
        // 优惠券优惠分解金额
        itemEntity.setCouponAmount(new BigDecimal("0"));
        // 积分优惠分解金额
        itemEntity.setIntegrationAmount(new BigDecimal("0"));
        // 商品价格乘以商品购买数量=总金额(未包含优惠信息)
        BigDecimal origin = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
        // 总价格减去优惠卷-积分优惠-商品促销金额 = 总金额
        origin.subtract(itemEntity.getPromotionAmount())
                .subtract(itemEntity.getCouponAmount())
                .subtract(itemEntity.getIntegrationAmount());
        // 该商品经过优惠后的分解金额
        itemEntity.setRealAmount(origin);
        return itemEntity;
    }
 /**
     * 计算订单涉及到的积分、优惠卷抵扣、促销优惠信息等信息
     * @param orderEntity
     * @param itemEntities
     * @return
     */
    private OrderEntity computPrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
        // 1、定义好相关金额,然后遍历购物项进行计算
        // 总价格
        BigDecimal total = new BigDecimal("0");
        //相关优惠信息
        // 优惠卷抵扣金额
        BigDecimal coupon = new BigDecimal("0");
        // 积分优惠金额
        BigDecimal integration = new BigDecimal("0");
        // 促销优惠金额
        BigDecimal promotion = new BigDecimal("0");
        // 积分
        BigDecimal gift = new BigDecimal("0");
        // 成长值
        BigDecimal growth = new BigDecimal("0");

        // 遍历订单项将所有的优惠信息进行相加
        for (OrderItemEntity itemEntity : itemEntities) {
            coupon = coupon.add(itemEntity.getCouponAmount()); // 优惠卷抵扣
            integration = integration.add(itemEntity.getIntegrationAmount()); // 积分优惠分解金额
            promotion = promotion.add(itemEntity.getPromotionAmount()); // 商品促销分解金额
            gift = gift.add(new BigDecimal(itemEntity.getGiftIntegration().toString())); // 赠送积分
            growth = growth.add(new BigDecimal(itemEntity.getGiftGrowth())); // 赠送成长值
            total = total.add(itemEntity.getRealAmount()); //优惠后的总金额
        }

        // 2、设置订单金额
        // 订单总金额
        orderEntity.setTotalAmount(total);
        // 应付总额 = 订单总额 + 运费信息
        orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
        // 促销优化金额(促销价、满减、阶梯价)
        orderEntity.setPromotionAmount(promotion);
        // 优惠券抵扣金额
        orderEntity.setCouponAmount(coupon);

        // 3、设置积分信息
        // 订单购买后可以获得的成长值
        orderEntity.setGrowth(growth.intValue());
        // 积分抵扣金额
        orderEntity.setIntegrationAmount(integration);
        // 可以获得的积分
        orderEntity.setIntegration(gift.intValue());
        // 删除状态【0->未删除;1->已删除】
        orderEntity.setDeleteStatus(0);
        return orderEntity;
    }
3、库存自动解锁—>MQ
库存解锁、StockReleaseListener
package com.atguigu.gulimall.ware.listener;

import com.atguigu.common.to.mq.OrderTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * 监听库存延时队列
 * @author gcq
 * @Create 2021-01-07
 */
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    /**
     * 监听库存队列
     * @param lockedTo
     * @param message
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo lockedTo, Message message, Channel channel) throws IOException {
        System.out.println("收到解锁库存的信息");
        try {
            wareSkuService.unLockStock(lockedTo);
            //库存解锁成功没有抛出异常,自动ack机制确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            // 重发
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /**
     * 订单释放
     *  订单30分钟未支付,订单关闭后发送的消息
     */
    @RabbitHandler
    public void handlerOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
        System.out.println("订单关闭准备解锁库存......");
        try {
            wareSkuService.unLockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

Service

 /**
     * 解锁库存
     * @param lockedTo
     */
    @Override
    public void unLockStock(StockLockedTo lockedTo) {
        // 工作单详情
        StockDetailTo detail = lockedTo.getDetail();
        // 工作单详情id
        Long detailId = detail.getId();
        // 查询到库存工作单详情
        WareOrderTaskDetailEntity taskDetailEntity = wareOrderTaskDetailService.getById(detailId);
        if (taskDetailEntity != null) {
            // 解锁库存
            // 库存工作单id
            Long id = lockedTo.getId();
            // 查询到库存工作单
            WareOrderTaskEntity TaskEntity = wareOrderTaskService.getById(id);
            // 拿到订单号
            String orderSn = TaskEntity.getOrderSn();
            // 根据订单号查询订单的状态
            R orderStatus = orderFeignService.getOrderStatus(orderSn);
            if (orderStatus.getCode() == 0) {
                OrderVo data = orderStatus.getData(new TypeReference<OrderVo>() {
                });
                // 订单状态为已关闭,那么就需要解锁库存
                if (data == null || data.getStatus() == 4) {
                    // 库存工作单锁定状态为锁定才进行解锁
                    if (taskDetailEntity.getLockStatus() == 1) {
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                    }
                }
            } else {
                // 消息被拒绝后重新放到队列里面,让别人继续消费解锁
                throw new RuntimeException("远败");
            }
        }
    }
支付

选择的是支付宝支付,根据老师所提供的素材 alipayTemplate、PayVo、PaySyncVo,引入到项目中进行开发

1、Controller

跳转到支付宝支付页面,支付完成后跳转到支付成功的回调页面

/**
     * 1、跳转到支付页面
     * 2、用户支付成功后,我们要跳转到用户的订单列表页
     * produces 明确方法会返回什么类型,这里返回的是html页面
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
    @ResponseBody
    @GetMapping(value = "/payOrder",produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
//        PayVo payVo = new PayVo();
//        payVo.setBody(); // 商品描述
//        payVo.setSubject(); //订单名称
//        payVo.setOut_trade_no(); // 订单号
//        payVo.setTotal_amount(); //总金额
        PayVo payvo = orderService.payOrder(orderSn);
        // 将返回支付宝的支付页面,需要将这个页面进行显示
        String pay = alipayTemplate.pay(payvo);
        System.out.println(pay);
        return pay;
    }
2、Service
 /**
     * 计算商品支付需要的信息
     * @param orderSn
     * @return
     */
    @Override
    public PayVo payOrder(String orderSn) {
        PayVo payVo = new PayVo();
        OrderEntity orderEntity = this.getOrderByOrderSn(orderSn);// 根据订单号查询到商品
        // 数据库中付款金额小数有4位,但是支付宝只接受2位,所以向上取整两位数
        BigDecimal decimal = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
        payVo.setTotal_amount(decimal.toString());
        // 商户订单号
        payVo.setOut_trade_no(orderSn);
        // 查询出订单项,用来设置商品的描述和商品名称
        List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
                .eq("order_sn", orderSn));
        OrderItemEntity itemEntity = itemEntities.get(0);
        // 订单名称使用商品项的名字
        payVo.setSubject(itemEntity.getSkuName());
        // 商品的描述使用商品项的属性
        payVo.setBody(itemEntity.getSkuAttrsVals());
        return payVo;
    }
3、支付成功后跳转页面
  • 新建文件夹

    cd /mydata/nginx/html/static/
    
    mkdir member
    
  • 上传资源文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMLoagwO-1684464779586)(images/谷粒商城项目笔记/image-20220626123758373.png)]

  • 修改host文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4WrpNYC-1684464779586)(images/谷粒商城项目笔记/image-20220626124219918.png)]

  • 2

  • 2

  • 2

支付成功后跳转到订单页面

 /**
    * @需求描述: 系统管理员 会员服务组 模块 用户支付成功后跳转到该页面
    * @创建人: 
    * @创建时间: 2021/01/08 11:13
    * @修改需求:
    * @修改人:
    * @修改时间:
    * @需求思路:
    */
    @GetMapping("/memberOrder.html")
    public String memberList(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model) {
        // 准备分页参数
        Map<String,Object> params = new HashMap<>();
        params.put("page",pageNum);
        // 远程查询当前用户的所有订单
        R r = orderFeignService.listwithItem(params);
        System.out.println(JSON.toJSONString(r));
        if (r.getCode() == 0) {
            model.addAttribute("orders",r);
        }
        return "list";
    }

Service

 /**
     * 查询当前用户所有订单
     * @param params
     * @return
     */
    @Override
    public PageUtils queryPageWithItem(Map<String, Object> params) {
        // 当前用户登录数据
        MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();
        // 查询当前用户所有的订单记录
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>()
                        .eq("member_id",memberRespVo.getId())
                        .orderByDesc("id")
        );
        List<OrderEntity> records = page.getRecords(); // 拿到分页查询结果
        List<OrderEntity> orderEntityList = records.stream().map(item -> {
            // 根据订单号查询当订单号对应的订单项
            List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
                    .eq("order_sn", item.getOrderSn()));
            item.setOrderEntityItem(itemEntities);
            return item;
        }).collect(Collectors.toList());
        // 重新设置分页数据
        page.setRecords(orderEntityList);

        return new PageUtils(page);
    }

然后页面渲染数据

支付宝文档地址:

https://opendocs.alipay.com/open/270/105900

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y79W3DMQ-1684464779586)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210108143131033.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iSVC7u0K-1684464779586)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105093259313.png)]

收单
订单:OrderListener
@RabbitListener(queues = "order.release.order.queue") // 监听订单的释放队列,能到这个里面的消息都是30分钟后过来的
@Service
public class OrderListener {

    @Autowired
    OrderService orderService;

    /**
     * 订单定时关单
     *      商品下单后,会向MQ中发送一条消息告诉MQ订单创建成功。
     *      那么订单创建30分钟后用户还没有下单,MQ就会关闭该订单
     * @param orderEntity 订单对象
     * @param channel 信道
     * @param message 交换机
     * @throws IOException
     */
    @RabbitHandler
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息,准备关闭订单:" + orderEntity.getOrderSn());
        try {
            orderService.closeOrder(orderEntity);
            // 关闭订单成功后,ack信息确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}

Service

   @Override
    public void closeOrder(OrderEntity orderEntity) {
        // 订单30分钟的时间可能有属性变动,所以需要根据属性再次查询一次
        OrderEntity entity = this.getById(orderEntity.getId());
        // 当前状态为待付款,说明用户30分钟内还没有付款
        if(entity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
            OrderEntity updateOrder = new OrderEntity();
            // 根据订单id更新
            updateOrder.setId(entity.getId());
            // 订单状态改成已取消
            updateOrder.setStatus(OrderStatusEnum.CANCLED.getCode());
            // 根据订单对象更新
            this.updateById(updateOrder);
            // 准备共享对象用于发送到MQ中
            OrderTo orderTo = new OrderTo();
            // 拷贝属性
            BeanUtils.copyProperties(entity,orderTo);
            try {
                rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DDyuqUzw-1684464779586)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105093437013.png)]

  • 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
    • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
  • 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
    • 订单解锁,手动调用收单
  • 网络阻塞问题,订单支付成功的异步通知一直不到达
    • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
  • 其他各种问题
    • 每天晚上闲时下载支付宝对账单,一 一 进行对账

幂等性

什么是幂等性

接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性

那些情况需要防止

用户多次点击按钮

用户页面回退再次提交

微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制

其他业务情况

什么情况下需要幂等

以 SQL 为例,有些操作时天然幂等

SELECT * FROM table WHERE id =? 无论执行多少次都不会改变状态是天然的幂等

UPDATE tab1 SET col1=1 WHERE col2=2 无论执行成功多少状态都是一致的,也是幂等操作

delete from user where userid=1 多次操作,结果一样,具备幂等

insert into user(userid,name) values(1,’ a’ ) 如userid为唯一主键,即重复上面的业务,只会插入一条用户记录,具备幂等


UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。insert into user(userid,name) values(,a")如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。

幂等解决方案
token 机制

1、服务端提供了发送 token 的接口,我们在分析业务的时候,哪些业务是存在幂等性问题的,就必须在执行业务前,先获取 token,服务器会把 token 保存到 redis 中

2、然后调用业务接口请求时, 把 token 携带过去,一般放在请求头部

3、服务器判断 token 是否存在 redis,存在表示第一次请求,然后删除 token,继续执行业务

4、如果判断 token 不存在 redis 中,就表示重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行

危险性:

1、先删除 token 还是后删除 token:
  1. 先删除可能导致,业务确实没有执行,重试还得带上之前的 token, 由于防重设计导致,请求还是不能执行
  2. 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除掉token,别人继续重试,导致业务被执行两次
  3. 我们最后设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求
2、Token 获取,比较 和删除 必须是原子性
  1. redis.get(token),token.equals、redis.del(token),如果说这两个操作都不是原子,可能导致,在高并发下,都 get 同样的数据,判断都成功,继续业务并发执行
  2. 可以在 redis 使用 lua 脚本完成这个操作
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
各种锁机制
1、数据库悲观锁

select * from xxx where id = 1 for update;

for update 查询的时候锁定这条记录 别人需要等待

悲观锁使用的时候一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用,另外需要注意的是,id字段一定是主键或唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦

2、数据库的乐观锁

这种方法适合在更新的场景中

update t_goods set count = count - 1,version = version + 1 where good_id = 2 and version = 1

根据 version 版本,也就是在操作数据库存前先获取当前商品的 version 版本号,然后操作的时候带上 version 版本号,我们梳理下,我们第一次操作库存时,得

到 version 为 1,调用库存服务 version = 2,但返回给订单服务出现了问题,订单服务又一次调用了库存服务,当订单服务传的 version 还是 1,再执行上面的

sql 语句 就不会执行,因为 version 已经变成 2 了,where 条件不成立,这样就保证了不管调用几次,只会真正处理一次,乐观锁主要使用于处理读多写少的问题

3、业务层分布锁

如果多个机器可能在同一时间处理相同的数据,比如多台机器定时任务拿到了相同的数据,我们就可以加分布式锁,锁定此数据,处理完成后后释放锁,获取锁必须先判断这个数据是否被处理过

各种唯一约束
1、数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同订单就不可能有两条订单插入,我们在数据库层面防止重复

这个机制利用了数据库的主键唯一约束的特性,解决了 insert场 景时幂等问题,但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键

如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关

2、redis set 防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的

set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理

防重表

使用订单表 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中,这样就保证了重复请求时,因为去重表有唯一

约束,导致请求失败,避免了幂等性等问题,去重表和业务表应该在同一个库中,这样就保证了在同一个事务,即使业务操作失败,也会把去重表的数据回滚,这

个很好的保证了数据的一致性,

redis防重也算

全局请求唯一id

调用接口时,生成一个唯一的id,redis 将数据保存到集合中(去重),存在即处理过,可以使用 nginx 设置每一个请求一个唯一id

proxy_set_header X-Request-Id $Request_id

秒杀

  • 配置host文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YuaZhsrt-1684464779587)(images/谷粒商城项目笔记/image-20220626150850457.png)]

  • 2

  • 2

  • 2

Sentinel

sentinel简介
    主要处理熔断和降级、限流
    熔断:一个服务出现问题,然后其它服务调用这个服务的时候迅速返回错误页面而不是阻塞。相当于熔断了访问这个服务的链路
    降级:比如现在是秒杀服务,大量流量涌进,需要更多资源,这个时候可以先暂停注册服务,提供更多的资源。别人访问注册服务的时候返回一个等候或者是一个错误页面。
    熔断与降级的不同:熔断是被调用方出现问题,降级是主动调整
    限流:根据服务可处理的并发量放流量进来
    它有一个核心库和dashborad
    它是一个保护资源的框架,使用的方法

    定义资源
    定义规则
    测试是否有效果

Hystrix 与Sentinel

①Hystrix基于线程池,每个接口都有自己的一个线程池,独立处理。但是浪费大量的资源。Sentinel基于信号量。不需要创建线程池,减少消耗的资源

②熔断降级策略:Hystrix基于异常比率,Sentinel基于响应时间,异常比率,异常数

③限流:Sentinel提供更多的限流方案和功能。
整合SpringBoot
  • 引入sentinel的依赖

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
    dependency>
    
  • 下载sentinel-dashboard-1.6.3.jar (阿里网盘 IT技术学习 - gulimall-soft)

  • 启动sentinel的控制台服务

    java -jar sentinel-dashboard-1.6.3.jar --server.port=8333
    
  • 本地访问:localhost:8333 (用户名/密码 sentinel/sentinel)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-80gJPPi6-1684464779587)(images/谷粒商城项目笔记/image-20220715150051225.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EMBSvp4J-1684464779587)(images/谷粒商城项目笔记/image-20220715150154574.png)]

  • 配置sentinel控制台地址信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAiVTIXd-1684464779587)(images/谷粒商城项目笔记/image-20220715150801073.png)]

  • 2

自定义流控
  • 引入actutor依赖,然后设置好yml开放接口

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    
    #暴露所有端点
    management:
      endpoints:
        web:
          exposure:
            include: '*'
    
  • 2

  • 2

  • 2

  • 2

  • 2

运维

k8s

环境搭建
1、创建三台虚拟机

(具体的方法见 前言 - 启动项目 - 虚拟机vmware

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6tVnayRp-1684464779588)(images/谷粒商城项目笔记/image-20220702095035936.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7YsKNtK-1684464779588)(images/谷粒商城项目笔记/image-20220702095046316.png)]

2、前置配置
  • 关闭防火墙

    systemctl stop firewalld
    
    systemctl disable firewalld
    
  • 禁用selinux安全策略

    sed -i 's/enforcing/disabled/' /etc/selinux/config
    
    setenforce 0
    
  • 关闭swap

    swapoff -a #临时关闭
    sed -ri 's/.*swap.*/#&/' /etc/fstab #永久关闭
    free -g #验证,swap必须为0
    
  • 添加主机名与IP对应关系

    vi /etc/hosts
    192.168.91.102 k8s-master
    192.168.91.103 k8s-node1
    192.168.91.104 k8s-node2
    
  • 将桥接的IPV4流量传递到iptables的链

    cat > /etc/sysctl.d/k8s.conf <<EOF
    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1
    EOF
    
3、所有节点安装docker、kubeadm、kubelet、kubectl
  • 安装Docker
    • 卸载之前的docker

      sudo yum remove docker \
                        docker-client \
                        docker-client-latest \
                        docker-common \
                        docker-latest \
                        docker-latest-logrotate \
                        docker-logrotate \
                        docker-engine
      
    • 安装Docker -CE

      sudo yum install -y yum-utils \
      device-mapper-persistent-data \
      lvm2
      
      # 设置docker repo的yum位置
      sudo yum-config-manager \
          --add-repo \
          https://download.docker.com/linux/centos/docker-ce.repo
          
          # 安装docker,docker-cli
      sudo yum -y install docker-ce docker-ce-cli containerd.io   
      
    • 配置docker加速

      sudo mkdir -p /etc/docker
      sudo tee /etc/docker/daemon.json <<-'EOF'
      {
        "registry-mirrors": ["https://lcfrsqb4.mirror.aliyuncs.com"]
      }
      EOF
      sudo systemctl daemon-reload
      sudo systemctl restart docker
      
    • 启动Docker && 设置docker开机启

      systemctl enable docker
      
    • 添加阿里与Yum源
      cat > /etc/yum.repos.d/kubernetes.repo << EOF
      [kubernetes]
      name=Kubernetes
      baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
      enabled=1
      gpgcheck=0
      repo_gpgcheck=0
      gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
      EOF
      
    • 安装kubeadm,kubelet和kubectl
      yum install -y kubelet-1.17.3 kubeadm-1.17.3 kubectl-1.17.3
      
    • 开机启动

      systemctl enable kubelet && systemctl start kubelet
      
    • 查看kubelet的状态

      systemctl status kubelet
      
    • 查看kubelet版本

      [root@k8s-node2 ~]# kubelet --version
      Kubernetes v1.17.3
      
4、部署k8s-master
  • master节点初始化
  • 上传k8s文件到master

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPGNBbXk-1684464779588)(images/谷粒商城项目笔记/image-20220702122550173.png)]

  • 修改执行权限

    cd /root/k8s
    
    chmod 700 master_images.sh
    
    ./master_images.sh
    
  • 初始化kubeadm
    kubeadm init \
    --apiserver-advertise-address=192.168.91.102 \
    --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers \
    --kubernetes-version   v1.17.3 \
    --service-cidr=10.96.0.0/16  \
    --pod-network-cidr=10.244.0.0/16
    
  • 出现如下表示成功

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOFNRZvu-1684464779588)(images/谷粒商城项目笔记/image-20220702131433242.png)]

  • 执行如下

    mkdir -p $HOME/.kube
    sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
    sudo chown $(id -u):$(id -g) $HOME/.kube/config
    
  • 复制一下如下位置的命令

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uD5dD1q2-1684464779588)(images/谷粒商城项目笔记/image-20220702131714821.png)]

    You should now deploy a pod network to the cluster.
    Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
      https://kubernetes.io/docs/concepts/cluster-administration/addons/
    
    Then you can join any number of worker nodes by running the following on each as root:
    
    kubeadm join 192.168.91.102:6443 --token i58flt.n6lsqphqh368ah93 \
        --discovery-token-ca-cert-hash sha256:5aacce5804fb6f2136f332b2576cb82175e83045caedcc73c77dbdf71aef164e
    
  • 安装POD网络插件(CNI)

    kubectl apply -f kube-flannel.yml 
    
  • 查看节点

    kubectl get nodes
    

    出现如下时就可以了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQ8EgKG9-1684464779589)(images/谷粒商城项目笔记/image-20220702132457267.png)]

5、把node节点加进去
  • 复制前面备份的这句话

    kubeadm join 192.168.91.102:6443 --token i58flt.n6lsqphqh368ah93 \
        --discovery-token-ca-cert-hash sha256:5aacce5804fb6f2136f332b2576cb82175e83045caedcc73c77dbdf71aef164e
    
  • 执行

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arQ3Zlia-1684464779589)(images/谷粒商城项目笔记/image-20220702132851299.png)]

  • 监控pod进度

    # 在master执行
    watch kubectl get pod -n kube-system -o wide
    
  • 在主节点查看node

    kubectl get nodes
    

    出现如下表示搭建成功

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVJRWz6d-1684464779589)(images/谷粒商城项目笔记/image-20220702133240563.png)]

  • 2

6、安装kubesphere
  • 安装步骤:https://blog.csdn.net/RookiexiaoMu_a/article/details/119859930

  • 定制化安装

    • 通过修改 ks-installer 的 configmap 可以选装组件,执行以下命令。

      kubectl edit cm -n kubesphere-system ks-installer
      
    • 2

    • 2

    • 2

    • 2

    • 2

    • 2

    • 2

    • 2

你可能感兴趣的:(谷粒商城学习笔记,java)