赞
踩
mysql安装在腾讯云
redis安装在本地虚拟机master上
运行时,renren-fast这个项目要到单独开个idea窗口打开。
微服务:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
各个服务都注册到注册中心,别的服务可以通过注册中心发现我们的服务。
安装vmware(阿里云盘:IT技术学习 - gulimall-soft)
本机搭建Linux (centos)
点击 创建新的虚拟机
点击 自定义 下一步
点击 下一步
点击 稍后安装操作系统 下一步
点击 Linux CentOS 7 64位 下一步
修改虚拟机名称slave 修改位置 下一步
处理器配置设置 下一步
内存3072 下一步
网络类型
虚拟机向导
磁盘类型
选择磁盘
指定磁盘容量50G
指定磁盘文件(直接下一步)
点击完成
点击 编辑虚拟机设置
点击 CD/DVD 使用ISO映像文件 浏览
选择镜像文件所在位置(下载地址:http://ftp.sjtu.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso)
点击 确定
点击 开启此虚拟机
鼠标点进去 点回车
继续点 回车
点击 ESC (停止检测)
选择中文简体 继续
稍等片刻,待出现如下时,点击软件选择
选择 基础设施服务器 完成
稍等片刻,待出现如下时,点击 安装位置
点击 完成
点击 网络和主机名
点击 配置
点击 常规 选中-可用时自动链接到这个网络
查看网段
点击 编辑 虚拟网络编辑
复制好这个网段 192.168.91.0
按如下配置,完了点保存
设置主机名
点击 开始安装
设置 root密码
稍等片刻,安装完成!!!
视频地址: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
位置:阿里云盘(IT技术学习 - gulimall-soft - Xmanager-7)
解压安装包运行安装程序,点击下一步,选择安装位置,建议安D盘(英文路径),记住自己的安装位置,后面要用到
解压插件包
解压后如下图所示
全选复制解压后的文件,到安装包安装的路径,例如我安装的路径 D:\ruanjian\Xmanager-7,粘贴->替换目标中的文件
破解完毕!!!
原版地址:https://www.yuque.com/yinghuashuxia-cohok/ahov4c/eipegl
sudo mkdir -p /etc/docker
// 阿里云 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
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
// 阿里云的源地址
$ 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
sudo yum install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
sudo systemctl enable docker
docker pull mysql:5.7
# --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 容器端口映射到主机的端口
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
docker restart mysql
sudo docker update mysql --restart=always //docker中开机自启
docker pull redis
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
vim /mydata/redis/conf/redis.conf
具体内容
appendonly yes
表示数据持久化,不会重启就丢。
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
docker exec -it redis redis-cli
docker restart redis
sudo docker update redis --restart=always
安装包在阿里云盘。(IT技术学习 - gulimall-soft)
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
下载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)]
配置阿里云镜像
<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)]
下载地址:https://code.visualstudio.com/Download (阿里网盘也有备份)
安装插件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FEKrngd-1684464779515)(images/谷粒商城项目笔记/image-20220502162914480.png)]
安装git (https://git-scm.com/)(阿里网盘有备份:IT技术学习 - gulimall-soft)
配置用户名
git config --global user.name "liuhandong" #这个不需要和注册时一样
配置邮箱
git config --global user.email "1920459132@qq.com" # 注册账号使用的邮箱
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htmFg8L2-1684464779515)(images/谷粒商城项目笔记/image-20220502163225891.png)]
配置 ssh 免密登录
ssh-keygen -t rsa -C "1920459132@qq.com"
连点三次回车
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 git@gitee.com
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ws89M2Lh-1684464779516)(images/谷粒商城项目笔记/image-20220502164714488.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjs2cjVK-1684464779516)(images/谷粒商城项目笔记/image-20220502164939532.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNphaNbB-1684464779516)(images/谷粒商城项目笔记/image-20220502165236550.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UDBslGaR-1684464779517)(images/谷粒商城项目笔记/image-20220502175101221.png)]
创建数据库(服务器中的数据库,每个服务分别对应一个数据库)
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)]
打开renren-generator项目
修改application.yml中的数据库配置
修改generator.properties配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W84R96N5-1684464779517)(images/谷粒商城项目笔记/image-20220506101644326.png)]
启动该项目
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTK2YXy9-1684464779518)(images/谷粒商城项目笔记/image-20220506102339977.png)]
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
配置数据源
https://mvnrepository.com/artifact/mysql/mysql-connector-java
<!--导入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
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包扫描:
使用@MapperScanner
告诉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起效:
例如:
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;
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
<spring-boot.version>2.1.8.RELEASE</spring-boot.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
<!--服务注册/发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> # 下面是依赖管理,相当于以后再dependencies里引spring cloud alibaba就不用写版本号, 全用dependencyManagement进行管理 <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
地址:https://github.com/alibaba/nacos/releases/tag/1.1.3 (阿里网盘有备份)
双击nacos/bin目录下的startup.cmd
spring:
application:
name: gulimall-coupon # 注意这里的name一定要有,不然注册不进去
cloud:
nacos:
discovery:
server-addr: localhost:8848
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
启动刚才使用了@EnableDiscoveryClient注解的服务
访问:http://localhost:8848/nacos/#/serviceManagement?dataId=&group=&appName=&namespace=
点击 服务列表 ,看到该服务已注册进去。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fvaFRdoZ-1684464779518)(images/谷粒商城项目笔记/image-20220503115337555.png)]
给调用者导包(此时他就有了远程调用其他服务的能力)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</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)]
中文文档: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.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</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);
}
}
命名空间:配置集隔离
默认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下的配置文件了
类似文件名
如果不写就默认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这个组的配置
每个微服务创建自己的命名空间,使用配置分组来区分环境
在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
重启后配置生效
官方文档: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)]
安装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
新增一级目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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>/?.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast # lb负载均衡 到指定的服务
predicates:
- Path=/api/** # path指定对应路径
filters: # 重写路径
- RewritePath=/api/(?<segment>/?.*), /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)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhDiTclS-1684464779529)(images\谷粒商城项目笔记\image-20220613204431958.png)]
和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储。
阿里云上使使用对象存储方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9dtFQnme-1684464779529)(images/谷粒商城项目笔记/image-20220507213107240.png)]
地址: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
在Maven项目中加入依赖项(推荐方式)
在 Maven 工程中使用 OSS Java SDK,只需在 pom.xml 中加入相应依赖即可。以 3.8.0 版本为例,在 pom内加入如下内容:
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
以下代码用于上传文件流:
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
String accessKeyId = "<yourAccessKeyId>";
String accessKeySecret = "<yourAccessKeySecret>";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流。
InputStream inputStream = new FileInputStream("<yourlocalFile>");
ossClient.putObject("<yourBucketName>", "<yourObjectName>", 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参数上面就是。
添加权限
图片的路径:比如,“D:\User\zzpic15479.jpg”
上传后的文件名,自己取,比如:15479.jpg
运行此程序
查看上传的图片
接入OSS
导包(把上面导的那个包先注掉)(这个包放在common里面)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</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
启动该服务
官方文档:
问题引入:填写form时应该有前端校验,后端也应该有校验
前端
前端的校验是element-ui表单验证
Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。
后端
@NotNull等
在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
<!--jsr3参数校验器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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
@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
@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
可以使用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.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</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: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;
索引:库
类型:表
数据是json格式
文档:就相当于表中一列列数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYTfQvho-1684464779537)(images/谷粒商城项目笔记/image-20220510123223174.png)]
倒排索引
下载镜像文件
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
运行镜像
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" } } } } } }
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;
即保存一条数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识。
接口: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)]
接口:POST http://192.168.163.131:9200/customer/external/
如果不带id则是新增,带了Id,如果有就是更新,没有就是新增。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6Sdxa8m-1684464779538)(images/谷粒商城项目笔记/1615299124434-93f69d46-00f1-423e-b795-313bb1d2f3c9.png)]
PUT和POST都可以
/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 当携带数据与实际值不匹配时更新失败
接口:POST http://192.168.163.131:9200/customer/external/1/_update
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DO8VvAab-1684464779539)(images/谷粒商城项目笔记/1615345823323-a4869fd9-d54d-461a-bcf2-77046a9f1970.png)]
在上面索引文档即保存文档的时候介绍,还有两种更新文档的方式:
这两种请求类似,即带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)]
语法格式:
{action:{metadata}}\n // 例如index保存记录,update更新
{request body }\n
{action:{metadata}}\n
{request body }\n
接口:POST /customer/external/_bulk
参数:
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}
在Kibana中使用dev-tools测试批量:
[外链图片转存中…(img-6mFM23Z5-1684464779540)]
接口: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)]
官方测试数据地址: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)]
下面的请求都是在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" : "bradshawmckenzie@euron.com", "city" : "Hobucken", "state" : "CO" }, "sort" : [ 0 ] }, ... ] } }
took
– how long it took Elasticsearch to run the query, in millisecondstimed_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 foundhits.total.value
- how many matching documents were foundhits.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支持两种基本方式检索;
也就是说除了上面示例的请求接口,根据请求体进行检索外;
还可以用GET请求参数的方式检索:
GET bank/_search?q=*&sort=account_number:asc
# q=* 查询所有
# sort=account_number:asc 按照account_number进行升序排列
本小节参考官方文档:Query DSL
Elasticsearch提供了一个可以执行查询的Json风格的DSL。这个被称为Query DSL,该查询语言非常全面。
一个查询语句的典型结构:
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 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;
请求示例:
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 ] }, ... ] } }
精确查询-基本数据类型(非文本)
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 短语匹配
将需要匹配的值当成一整个单词(不分词)进行检索
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
# 这里会检索 address 匹配包含短语 mill lane 的数据
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": [
"city",
"address"
]
}
}
}
# 检索 city 或 address 匹配包含 mill 的数据,会对查询条件分词
复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间
可以互相嵌套,可以表达非常复杂的逻辑。
GET bank/_search { "query": { "bool": { "must": [ { "match": { "gender": "M" } }, { "match": { "address": "mill" } } ] } } } # 查询 gender 为 M 且 address 包含 mill 的数据
并不是所有的查询都需要产生分数,特别是哪些仅用于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
, should
和must_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”子句中的条件被视为“过滤器”。
它影响文档是否包含在结果中,但不影响文档的评分方式。还可以显式地指定任意过滤器来包含或排除基于结构化数据的文档。
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 的数据
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)
}
}
}
}
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 } } }
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 } } ] } } }
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 } }, ... ] } } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DAaHEs74-1684464779541)(images/谷粒商城项目笔记/image-20201026074813810.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4l1gMLil-1684464779541)(images/谷粒商城项目笔记/image-20201026074841875.png)]
Mapping(映射)
Mapping 是用来定义一个文档(document),以及他所包含的属性(field)是如何存储索引的,比如使用 mapping来定义的:
查看 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)]
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(自定义分词器)
注意:不能用默认的 elasticsearch-plugin.install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases 下载与 es对应的版本
安装后拷贝到 plugins 目录下
下载安装包:(阿里云盘有备份:IT技术学习 - gulimall-s)
在/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
使用前记得重启elasticsearch
docker restart elasticsearch
分词器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kd0cFEUQ-1684464779542)(images/谷粒商城项目笔记/image-20201026092255250.png)]
docker run -p 80:80 --name nginx -d nginx:1.10
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 '<h1><a target="_blank" href="https://github.com/zsy0216/guli-mall">谷粒商城源码</a></h1>' \
>/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)]
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、Pom.xml
<!-- 导入es的 rest-high-level-client -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
为什么要导入这个?这个配置那里来的?
官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-maven.html
/** * @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
测试是否注入成功
@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)]
修改 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)]
访问跳转分析
后续网关配置
之后为了统一管理我们的各种服务,我们将通过配置网关作为 nginx 转发的目标。最后通过配置网关根据不同的域名来判断跳转对应的服务。
[外链图片转存中…(img-zQtzpXIC-1684464779545)]
简介
压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统
在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误,有两种错误类型是:
内存泄漏、并发与同步
有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化
1、Jvm内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGPVndiC-1684464779545)(images/谷粒商城项目笔记/image-20201029112517466.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdDalfLm-1684464779546)(images/谷粒商城项目笔记/image-20201029113956043.png)]
所有的对象实例以及数组都要在堆上分配,堆时垃圾收集器管理的主要区域,也被称为 “GC堆”,也是我们优化最多考虑的地方
堆可以细分为:
新生代
老年代
永久代/原空间
垃圾回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbZ04yzV-1684464779546)(images/谷粒商城项目笔记/image-20201029114153244.png)]
从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一个新的区域 - 元空间(MetaSpac)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U6s0KZwb-1684464779546)(images/谷粒商城项目笔记/image-20201029114218716.png)]
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)]
此时打开某个java应用,就发现多了个Visual GC
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIOu6V1a-1684464779549)(images/谷粒商城项目笔记/image-20220617195055516.png)]
docker stats 查看相关命令
(阿里云盘:IT技术学习 - gulimall-soft)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MyGqcHC7-1684464779549)(images/谷粒商城项目笔记/image-20220504155752345.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NCLLGLey-1684464779550)(images/谷粒商城项目笔记/image-20220504155833695.png)]
解压后打开bin目录
点击jmeter.bat
就开启了jmeter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FK4MQiZ0-1684464779550)(images/谷粒商城项目笔记/image-20201029084634498.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCs6ZJ3M-1684464779550)(images/谷粒商城项目笔记/image-20201029085843220.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tqhj4CQ6-1684464779550)(images/谷粒商城项目笔记/image-20201029085942442.png)]
汇总图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]
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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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
SpringBoot 整合 redis,查看SpringBoot提供的 starts
[外链图片转存中…(img-BDZpIDTr-1684464779552)]
pom.xml
<!--引入redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!--不加载自身的 lettuce--> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!--jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
堆外内存溢出异常:
这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。
下面进行分析:
解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。
application.yaml
Spring:
redis:
host: 192.168.56.10
port: 6379
RedisAutoConfig.java
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gziOAEFg-1684464779552)(images/谷粒商城项目笔记/image-20201031154710108.png)]
@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);
}
/** * 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<String, List<Catelog2Vo>> getCatelogJson() { // 给缓存中放 json 字符串、拿出的是 json 字符串,还要逆转为能用的对象类型【序列化和反序列化】 // 1、加入缓存逻辑,缓存中放的数据是 json 字符串 // JSON 跨语言,跨平台兼容 String catelogJSON = redisTemplate.opsForValue().get("catelogJSON"); if (StringUtils.isEmpty(catelogJSON)) { // 2、缓存没有,从数据库中查询 Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb(); // 3、查询到数据,将数据转成 JSON 后放入缓存中 String s = JSON.toJSONString(catelogJsonFromDb); redisTemplate.opsForValue().set("catelogJSON",s); return catelogJsonFromDb; } // 转换为我们指定的对象 Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {}); return result; }
[外链图片转存中…(img-fdkVCYEz-1684464779553)]
**理解:**就先当1000个人去占一个厕所,厕所只能有一个人占到这个坑,占到这个坑其他人就只能在外面等待,等待一段时间后可以再次来占坑,业务执行后,释放锁,那么其他人就可以来占这个坑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pmY4YGt-1684464779553)(images/谷粒商城项目笔记/image-20201031123441336.png)]
代码:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0");
if (lock) {
// 加锁成功..执行业务
Map<String,List<Catelog2Vo>> 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<String,List<Catelog2Vo>> 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<String,List<Catelog2Vo>> 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<String,List<Catelog2Vo>> 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<Long>(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<String,List<Catelog2Vo>> 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<Long>(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 作为分布锁,分布式对象等功能-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
@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); } }
// 1. 获取一把锁
Rlock lock = redisson.getLock("my-lock");
// 2. 加锁, 阻塞式等待
lock.lock();
try {
System.out.println("加锁成功,执行业务...");
} catch (Exception e) {
} finally {
// 3. 解锁 假设解锁代码没有运行,Redisson 会出现死锁吗?(不会)
lock.unlock();
}
@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)]
二话不说,上代码!!!
/** * 保证一定能读取到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享锁)读锁是一个共享锁 * 写锁没释放读锁就必须等待 * 读 + 读 相当于无锁,并发读,只会在 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)]
官网!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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就会放行
官网!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]
三个连接
一号连接 写数据库 然后删缓存
二号连接 写数据库时网络连接慢,还没有写入成功
三号链接 直接读取数据,读到的是一号连接写入的数据,此时 二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存
无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实力同时更新会出事,怎么办?
总结:
最后符上 三级分类数据 加上分布式锁
org.springframework.cache.Cache
和 org.sprngframework.cache.CacheManager
接口睐统一不同的缓存技术JCache
(JSR-107)注解简化我们的开发Cache
接口下 Spring 提供了各种 XXXCache的实现,如 RedisCache
、EhCache
,ConcrrentMapCache
等等,Spring
会检查检查指定参数的马努表犯法是否已经被嗲用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次直接调用从缓存中获取Sprng
缓存抽象时我们需要关注的点有以下两点
官网地址: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)]
从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管理所有缓存分区中 */
对于缓存声明,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; }
配置
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 # 是否允许缓存空值
/** * 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; }
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
- 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";
}
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
将资料中的前端页面放到 search 服务模块下的 resource/templates 下;
配置 Windows hosts 文件:
192.168.56.100 search.gulimall.com
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4P4KFUz-1684464779560)(images/谷粒商城项目笔记/image-20220512132128091.png)]
1、继承 Thread
2、实现 Runnable
3、实现 Callable 接口 + FutureTask(可以拿到返回结果,可以处理异常)
4、线程池
方式一和方式二 主进程无法获取线程的运算结果,不适合当前场景
方式三:主进程可以获取当前线程的运算结果,但是不利于控制服务器种的线程资源,可以导致服务器资源耗尽
方式四:通过如下两种方式初始化线程池
Executors.newFixedThreadPool(3);
//或者
new ThreadPollExecutor(corePoolSize,maximumPoolSize,keepAliveTime,TimeUnit,unit,workQueue,threadFactory,handler);
通过线程池性能稳定,也可以获取执行结果,并捕获异常,但是,在业务复杂情况下,一个异步调用可能会依赖另一个异步调用的执行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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个默认拒绝策略
newCacheThreadPool
newFixedThreadPool
newScheduleThreadPool
newSingleThreadExecutor
业务场景:
查询商品详情页逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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;
});
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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<Void> 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)]
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)]
在云市场就能看到购买的服务
[外链图片转存中…(img-g761hPPU-1684464779568)]
在购买短信的页面,能进行调试短信
[外链图片转存中…(img-e9zZJxQn-1684464779569)]
输入对应手机号,appCode 具体功能不做演示
[外链图片转存中…(img-8ztC0m7z-1684464779569)]
往下拉找到对应 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(); } }
需要导入对应工具类,参照注释就行
用户要是一直提交验证码
/** * 发送短信验证码 * @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(); }
/** * 注册数据封装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; }
设置 name
属性与 Vo
一致,方便将传递过来的数据转换成 JSON
[外链图片转存中…(img-VSkKU6A2-1684464779569)]
/** * //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"; } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NK7Uss1J-1684464779570)(images/谷粒商城项目笔记/image-20201110101306173.png)]
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(); } }
此处引入一个问题
至此注册相关结束~
/**
* @author gcq
* @Create 2020-11-10
*/
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
同时需要保证前端页面提交字段与 Vo 类中一致
@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; } } }
我们在auth.gulimall.com中保存session,但是网址跳转到 gulimall.com中,取不出auth.gulimall.com中保存的session,这就造成了微服务下的session不同步问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwqrDfqk-1684464779570)(images/谷粒商城项目笔记/image-20201111103637615.png)]
同一个服务复制多个,但是session还是只能在一个服务上保存,浏览器也是只能读取到一个服务的session
[外链图片转存中…(img-nciPpBQs-1684464779570)]
[外链图片转存中…(img-0alkFR0A-1684464779570)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBYzyhtl-1684464779571)(images/谷粒商城项目笔记/image-20201111104913888.png)]
[外链图片转存中…(img-odP5VouD-1684464779571)]
[外链图片转存中…(img-FPQt9lrA-1684464779571)]
[外链图片转存中…(img-9jrug9WN-1684464779572)]
[外链图片转存中…(img-CMsOAZbr-1684464779572)]
[外链图片转存中…(img-xU1xrIBS-1684464779572)]
[外链图片转存中…(img-rGtwvfmV-1684464779572)]
[外链图片转存中…(img-FQW8vZsZ-1684464779572)]
https://docs.spring.io/spring-session/docs/2.5.0/reference/html5/#samples
auth 服务、product 服务、 search 服务 pom文件
<!-- 整合 spring session 实现 session 共享-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
session:
store-type: redis
**主启动类增加注解:@EnableRedisHttpSession **
[外链图片转存中…(img-Wo4JUXnH-1684464779573)]
@EnableRedisHttpSession // 整合spring session
CookieSerializer
api文档参考:https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#api-cookieserializer
[外链图片转存中…(img-Hemiq2rO-1684464779573)]
文档地址:
[外链图片转存中…(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)]
提供的实例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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<Object> springSessionDefaultRedisSerializer() { // 使用jackson提供的转换器 return new GenericJackson2JsonRedisSerializer(); } }
/** * 核心原理 * 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
方法
及那个 request
、response
包装成 SessionRepositoryRequestWrapper
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Ld9Hw4X-1684464779574)(images/谷粒商城项目笔记/image-20201111195249024.png)]
文档地址:
相关流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v3fH5cC1-1684464779575)(images/谷粒商城项目笔记/image-20201110154532752.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O44DfqHV-1684464779575)(images/谷粒商城项目笔记/image-20201110154702360.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JlgLpbah-1684464779576)(images/谷粒商城项目笔记/image-20201110160834589.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLGe0COb-1684464779576)(images/谷粒商城项目笔记/image-20201110161001013.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtWibxoT-1684464779577)(images/谷粒商城项目笔记/image-20201110161032203.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hR2mnZu5-1684464779577)(images/谷粒商城项目笔记/image-20201110161152105.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jiXUaJOJ-1684464779577)(images/谷粒商城项目笔记/image-20201110161407018.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrRH0cYb-1684464779577)(images/谷粒商城项目笔记/image-20201110161451881.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CxwQpnIr-1684464779578)(images/谷粒商城项目笔记/image-20201110161634486.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)]
/** * 回调接口 * @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"; }
@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; } }
导入资源
cd /mydata/nginx/html/static/
mkdir cart
[外链图片转存中…(img-VlxbJIdg-1684464779579)]
修改Host文件
[外链图片转存中…(img-j9YukrPr-1684464779580)]
323
23
23
23
23
2
32
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的 。
参考:https://gitee.com/xuxueli0323/xxl-sso
[外链图片转存中…(img-IohQVUBt-1684464779580)]
[外链图片转存中…(img-roBKjcPn-1684464779580)]
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)]
AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 Exchange 和 Binding 的角色 生产者把消息发布到 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)]
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: 192.168.56.10
port: 5672
virtual-host: /
/** * 创建Exchange * 1、如何利用Exchange,Queue,Binding * 1、使用AmqpAdmin进行创建 * 2、如何收发信息 */ @Test public void contextLoads() { // public DirectExchange( // String name, 交换机的名字 // boolean durable, 是否持久 // boolean autoDelete, 是否自动删除 // Map<String, Object> 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<String, Object> 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"); }
@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("消息发送完成{}"); } }
[外链图片转存中…(img-pPvvZFP8-1684464779582)]
spring.rabbitmq.publisher-confirms=true
在创建 connectionFactory
的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback
。
CorrelationData
用来表示当前消息唯一性
消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback
被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback
spring.rabbitmq.publisher-retuns=true
spring.rabbitmq.template.mandatory=true
confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式
这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据
场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
常用解决方案:
Spring的schedule 定时任务轮询数据库
缺点:
消耗系统内存,增加了数据库的压力,存在较大的时间误差
**解决:**rabbitmq的消息TTL和死信Exchange结合
[外链图片转存中…(img-xzalSNYv-1684464779582)]
时效问题
上一轮扫描刚好扫描,而这个时候刚好下了订单,就没有扫描到,下一轮扫描的时候,订单还没有过期,等到订单过期后30分钟才被扫描到
[外链图片转存中…(img-dL3qWkFu-1684464779582)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E8w5sXBe-1684464779582)(images/谷粒商城项目笔记/image-20201120132805292.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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); }
上传静态页面
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 流,分别为信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
订单模块是电商系统的枢纽,在订单这个模块上获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息疏通
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IN9UhrCB-1684464779584)(images/谷粒商城项目笔记/image-20201117102129127.png)]
用户信息包括是用户账号、用户等级、用户的收货地址、收货人、收货人电话、用户账号需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等
订单基础信息是订单流转的核心,其包括订单类型,父/子订单、订单编号、订单状态、订单流转时间等
商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户
下单行为记录的用户下单数量,商品合计价格等
优惠信息记录用户参与过的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优
惠卷信息,优惠卷满足条件的优惠卷需要展示出来,另外虚拟币抵扣信息等进行记录
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。
支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务
通过订单号和流水单号与支付通道进行对账使用。
支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个一-余额支付+第三方支付。
商品总金额,每个商品加总后的金额:运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,
优惠券优惠金额,虚拟积分或者虛拟币抵扣的金額,会员折扣的金额等之和;实付金额,用户实际需要
付款的金额。
用户实付金额=商品总金额+运费 - 优惠总金额
物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来
获取和向用户展示物流每个状态节点。
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支
付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超
时后将自动取消订单,订单变更关闭状态。
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调动、配货、分拣,出库等操作
仓库将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时熟悉商品的物流状态
用户确认收货后吗,订单交易完成,后续支付则进行计算,如果订单存在问题进入售后状态
付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态
用户在付款后申请退款,或商家发货后用户申请退货
售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待
商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单
状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。
订单流程是指从订单产生到完成整个流转的过程,从而行程了-套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与020订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一一个正常的网购步骤:订单生成>支付订单->卖家发货一确认收货>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,
可概括如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8MpKq5W-1684464779585)(images/谷粒商城项目笔记/image-20201117104613032.png)]
1、订单创建与支付
2、逆向流程
在订单服务下准备好页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwg3RlOf-1684464779585)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105095210202.png)]
可以发现订单结算页,包含以下信息:
1.收货人信息:有更多地址,即有多个收货地址,其中有一个默认收货地址
2.支付方式:货到付款下在线支付,不需要后台提供
3.送货清单:配送方式(不做)及商品列表(根据购物车选中的skuld到数据库中查询)
4.发票:不做
5.优惠:查询用户领取的优惠券(不做)及可用积分(京豆)
1、引入pom
<!--整合spring session 解决session问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</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; } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1xs5b566-1684464779585)(…/…/User/download/brain-map-master/docs/谷粒商城项目笔记/分布式高级篇/高级篇笔记/image-20210105142735857.png)]
根据图片中商品信息抽取成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; }
@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;
/** * @需求描述: 系统管理员 订单组 模块 用户下单功能 * @创建人: 郭承乾 * @创建时间: 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"; } }
具体业务
@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> T execute(RedisScript<T> script // redis的脚本 * , List<K> 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; }
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、跳转到支付页面 * 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; }
/** * 计算商品支付需要的信息 * @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; }
新建文件夹
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)]
@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)]
接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败,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不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
1、服务端提供了发送 token
的接口,我们在分析业务的时候,哪些业务是存在幂等性问题的,就必须在执行业务前,先获取 token
,服务器会把 token
保存到 redis 中
2、然后调用业务接口请求时, 把 token
携带过去,一般放在请求头部
3、服务器判断 token
是否存在 redis
,存在表示第一次请求,然后删除 token
,继续执行业务
4、如果判断 token
不存在 redis
中,就表示重复操作,直接返回重复标记给 client
,这样就保证了业务代码,不被重复执行
危险性:
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
select * from xxx where id = 1 for update;
for update 查询的时候锁定这条记录 别人需要等待
悲观锁使用的时候一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用,另外需要注意的是,id字段一定是主键或唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦
这种方法适合在更新的场景中
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 条件不成立,这样就保证了不管调用几次,只会真正处理一次,乐观锁主要使用于处理读多写少的问题
如果多个机器可能在同一时间处理相同的数据,比如多台机器定时任务拿到了相同的数据,我们就可以加分布式锁,锁定此数据,处理完成后后释放锁,获取锁必须先判断这个数据是否被处理过
插入数据,应该按照唯一索引进行插入,比如订单号,相同订单就不可能有两条订单插入,我们在数据库层面防止重复
这个机制利用了数据库的主键唯一约束的特性,解决了 insert场 景时幂等问题,但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关
很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的
set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理
使用订单表 orderNo
做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中,这样就保证了重复请求时,因为去重表有唯一
约束,导致请求失败,避免了幂等性等问题,去重表和业务表应该在同一个库中,这样就保证了在同一个事务,即使业务操作失败,也会把去重表的数据回滚,这
个很好的保证了数据的一致性,
redis防重也算
调用接口时,生成一个唯一的id,redis 将数据保存到集合中(去重),存在即处理过,可以使用 nginx 设置每一个请求一个唯一id
proxy_set_header X-Request-Id $Request_id
配置host文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YuaZhsrt-1684464779587)(images/谷粒商城项目笔记/image-20220626150850457.png)]
2
2
2
主要处理熔断和降级、限流 熔断:一个服务出现问题,然后其它服务调用这个服务的时候迅速返回错误页面而不是阻塞。相当于熔断了访问这个服务的链路 降级:比如现在是秒杀服务,大量流量涌进,需要更多资源,这个时候可以先暂停注册服务,提供更多的资源。别人访问注册服务的时候返回一个等候或者是一个错误页面。 熔断与降级的不同:熔断是被调用方出现问题,降级是主动调整 限流:根据服务可处理的并发量放流量进来 它有一个核心库和dashborad 它是一个保护资源的框架,使用的方法 定义资源 定义规则 测试是否有效果 Hystrix 与Sentinel ①Hystrix基于线程池,每个接口都有自己的一个线程池,独立处理。但是浪费大量的资源。Sentinel基于信号量。不需要创建线程池,减少消耗的资源 ②熔断降级策略:Hystrix基于异常比率,Sentinel基于响应时间,异常比率,异常数 ③限流:Sentinel提供更多的限流方案和功能。
引入sentinel的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</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.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
#暴露所有端点
management:
endpoints:
web:
exposure:
include: '*'
2
2
2
2
2
(具体的方法见 前言 - 启动项目 - 虚拟机vmware)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6tVnayRp-1684464779588)(images/谷粒商城项目笔记/image-20220702095035936.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7YsKNtK-1684464779588)(images/谷粒商城项目笔记/image-20220702095046316.png)]
关闭防火墙
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
卸载之前的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
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
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
上传k8s文件到master
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPGNBbXk-1684464779588)(images/谷粒商城项目笔记/image-20220702122550173.png)]
修改执行权限
cd /root/k8s
chmod 700 master_images.sh
./master_images.sh
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
kubectl apply -f kube-flannel.yml
查看节点
kubectl get nodes
出现如下时就可以了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQ8EgKG9-1684464779589)(images/谷粒商城项目笔记/image-20220702132457267.png)]
复制前面备份的这句话
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
安装步骤: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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。