# luban
**Repository Path**: dengxing1277/luban
## Basic Information
- **Project Name**: luban
- **Description**: 微服务项目-鲁班上门
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 2
- **Created**: 2023-10-20
- **Last Updated**: 2024-02-28
## Categories & Tags
**Categories**: Uncategorized
**Tags**: luban
## README
# 1 课程介绍
## 1.1 课程阶段
- luban-demo: demo案例,提取一个电商原型.购物车,订单,库存.
- 特点: 轻业务,重技术,重框架.
- 目的: 学习技术框架.
- 鲁班上门: 真实项目实战
- 特点: 重业务,重技术,重经验
- 目的: 积累工作经验
- 梳理项目逐字稿|**模板**:
- 目的: 整理自定义的项目.
## 1.2 知识点|技术
### 1.2.1 微服务
- spring cloud aliaba 组件集合
- nacos
- dubbo
- sentinel
- rocketmq
- spring cloud gateway 网关
### 1.2.2 工具软件
- mysql
- redis
- 深入扩展的知识点:
- 缓存架构
- 分布式环境的 锁
- rocketmq: 消息队列
# 2 讲师联系方式
- 姓名: 肖旭伟
- 微信联系方式:
- 备注格式: 中心名称+班级+姓名
- 例子: 北京中关村JSD2306王翠花
- 微信号: xiaoxuwei4478
# 3 环境准备
## 3.1 克隆笔记仓库
https://gitee.com/xiaolaoshi2021/jsd2306-notes.git
包含以下内容:
- 每天笔记
- md格式
- pdf格式(主要是看图片)
- 资源文件
- 随着课程推进,需要什么文件,就放什么
## 3.2 克隆luban-demo
原则: 所有的软件 代码 安装和保存的路径 **不能有中文和空格**
## 3.3 替换settings.xml
当前系统中存在|不存在. 一个maven的环境系统配置文件settings.xml.
- 阿里镜像id值和名字 最好和你当前仓库使用的ali的id和名字一致
- 对照自己的settings.xml 修改id和name然后替换到 ${user.home}\\.m2 文件夹里.粘贴替换
- 替换之后 maven的对于仓库资源的工作逻辑

## 3.4 运行maven生命周期命令
问题: 在运行和使用maven的时候 总是出现一些问题,但是没有提示特别明确的内容.
### 3.4.1 生命周期
作为maven项目 maven软件管理工具 对项目的每一步都有响应的命令.
- clean: 清空 项目本地编译目录
- compile: 主动执行项目编译工作 src/main 输出到 target/classes src/test 会输出到target/test-classes
- test: 运行 在src/test 编写的测试代码 生成测试报告(目前对于后端开发没有太大作用)
- package: 将项目打成jar包.
- install: 将项目 安装到本地仓库(多个项目相互依赖 相互使用时.)
### 3.4.2 运行命令方式
- idea自带插件
- 自定义命令
### 3.4.3 查看maven详细错误
在自定义运行命令 添加一个选项 -X(大写)
例如: mvn clean compile -X
## 3.5 导入项目的sql文件
/doc/sql文件夹 有luban-demo.sql 导入当先windows的本地mysql数据库中.
- idea连接mysql数据库 配置连接
- 找到sql文件,右键运行 run
- 选择一下当前idea配置的数据库 点击确定
## 3.6 luban-demo流程时序图
## 3.7 日志配置文件
### 3.7.1 日志框架
市面上有很多日志框架 log4j log4j2 slf4j logback.... springboot中默认使用的日志框架是logback.
不同的日志框架 ,web应用中都可以使用不同**依赖+不同配置xml**实现日志输出和配置.
自行查看 study项目 luban-log-study.
### 3.7.2 自行学习日志框架
原则: 自学途径 官方权威的
- 官方网站: get stated demo example tutorial learn study...... 最终指向都是 doc
- 书籍: 最好 英文翻译的书籍.
## 3.8 启动访问
http://localhost:9000/doc.html 查看swagger接口页面.
- 增加购物车
- 减少库存
注意: 测试接口容易出现的问题. 参数匹配关系要注意.
# 4 单体架构项目的讨论
## 4.1 单体架构
开发一个web应用,将所有接口功能(绝大部分业务接口),都集中管理开发在一个项目中.
这种项目的架构就是单体架构.
例如当前demo案例: 包含的业务接口 横跨 3个大业务 **购物车** **订单** **库存**
思考: 第四阶段的项目 某个运行web应用中是否 满足单体架构的概念描述?
## 4.2 单体架构得优缺点分析
### 4.2.1 优点
- 结构简单,创建和开发成本比较低.
- 运维简单,部署成本低.
穿插知识点: 开发的项目,使用jar包如何运行
```shell
java -jar ****.jar
```
### 4.2.2 缺点
前提: 业务壮大,在发展,在变复杂.(**项目代码的接口多了**)
- 代码在大型系统中臃肿.维护更新很困难.
- 并发流量的木桶原则.
### 4.2.3 应用场景
互联网公司项目,面对用户群体比较庞大. **功能多,扩展快**. 不合适单体架构
传统项目,用户体量不大.功能更新扩展不迅速.
例如: 早期公司oa,财务, 养殖,国企项目.
# 5 项目的纵向拆分
## 5.1 纵向拆分的概念
系统结构演变过程 单体架构---分布式架构 纵向拆分是这种演变必要过渡阶段.
将一个系统中 按照**功能**,**逻辑**,划分成多个**独立运行**的单体系统---纵向拆分的过程.
将这个项目中依赖关系,根据应用功能,进行纵向拆分.
必定导致2个直接的问题出现:
- 拆分的独立系统之间 需要引入解决方案解决沟通问题.
- 如果需要沟通,相互应该传递应用信息.
结果: 在拆分之后,要解决更多新的问题,否则架构不能使用.
## 5.2 纵向拆分练习
### 5.2.1 目标|步骤
- 步骤
- 课上带领拆分出来order业务
- 练习将剩下的cart业务 和stock业务拆分
- 目标:
- 成功拆分
- 成功运行
- 拆分的maven应用模块结构
### 5.2.2 如何创建maven项目
- 右键项目任何项目标题 new project
- 选中左侧maven项目 next 注意 jdk版本(1.8)
- 填写详情信息 注意继承 和项目名称
### 5.2.3 创建 子工程 order-api order-main
(略)
### 5.2.4 粘贴和删除代码
拆分出来的order系统 只管理和运行order订单的业务.
- 将原有代码 luban-demo-all-main 粘贴到order-main
直接粘贴src.
- 将所有业务代码和order无关的,都删除
- controller: cartController stockController
- api: cartService stockService
- mapper: cartMapper stockMapper
- 调整订单业务实现OrderServiceImpl
### 5.2.5 依赖管理
目标: 根据需求 提供最精简的依赖资源.
例如1: 连接数据库拿到数据库JDBC连接 使用什么依赖 mysql-connector-java
例如2: 在 springboot中整合mybatis需要至少用到哪些依赖 mybatis-spring-boot-starter mysql-connector-java
```xml
org.springframework.boot
spring-boot-starter-web
com.tarena.luban.demo
luban-demo-protocol
0.0.1-SNAPSHOT
mysql
mysql-connector-java
org.mybatis.spring.boot
mybatis-spring-boot-starter
```
## 5.3 课堂跟踪练习
练习内容: 仿照课上order的拆分步骤 完成cart和stock拆分.
# 附录
# 1 问题集合
## 1.1 maven的pom文件 过期|灰色|无法识别
### 1.1.1 现象
### 1.1.2 原因
当前idea版本 对与删除的(remove-module)maven项目 做了自动忽略ignore操作.
### 1.1.3 解决办法
file-->settings-->搜索maven-->ignore files --> 勾掉忽略的文件.
# 提前准备好课前资料
# 1 问题解析
## 1.1 maven\
### 1.1.1 现象
项目 顶级父工程 pom文件 在depdencyManagement中爆红.
### 1.1.2 原因
在本地库中 没有爆红提示的资源. 能否根据 资源依赖的标签 到本地库找到对应文件夹.
dependency 依赖资源都有三个标签.
groupId: 表示某个公司的一个项目
artifactId: 表示这个项目的某个模块
version: 这个模块的版本
```xml
org.projectlombok
lombok
1.18.20
```
groupId: 多级文件夹 /org/projectlombok/
artifactId: 一个文件夹 /lombok
version: 一个文件夹 /1.18.20
maven本地库目录/org/projectlombok/lombok/1.18.20/**.jar
### 1.1.3 标签dependencyManagement作用
依赖管理标签 声明版本的.不会直接在依赖资源中出现的.不会下载,不会使用.
### 1.1.4 用法
一般情况下是在父工程中使用 ,定义 声明好的所有版本兼容的依赖资源,子工程继承过去使用.
- 单个资源声明
```xml
org.projectlombok
lombok
1.18.21
```
- 批量资源声明
```xml
org.springframework.boot
spring-boot-dependency
2.5.9
import
pom
```
### 1.1.5 结论
不需要单独处理,因为没有使用.
# 2 回顾
## 2.1 昨天的思路
- 单体架构
- 优缺点
- 鲁班高并发高可用需求项目 不适用单体架构
- 纵向拆分
- 未解决的问题
- 拆出来的相互不认识(程序进程相互认识 通信 需要ip port)
- 无法实现调用
# 3 微服务框架
## 3.1 微服务概念
### 3.1.1 微服务是什么
微服务(micro api)概念 Martin Fowler 2014提出(可以尝试搜索论文).
每一个**微服务**都是一个独立运行**服务系统**.
**微:** 小. 原项目中集中的功能,如果做了纵向拆分,拆分出来的结果 独立运行,相比于拆分之前,功能变得微小了. 理论上来说每一个服务最小管理一个http接口.
**服务:** 来自于一种面向服务的编程思想. 可以调用的任意功能都叫做**服务**
纵向拆分出来的 order cart stock 就可以看成是微服务.
## 3.2 微服务落地框架
开源框架 spring cloud alibaba 我们要学习的解决微服务架构中的问题的**组件**
- nacos
- sentinel
- dubbo
- rocketmq
spring cloud gateway
## 3.3 spring cloud框架
早期 spring cloud 整合netflix(奈非) 一套微服务方案(**一站式**的解决了微服务架构的问题)
奈飞停更了(可惜了 学了一套这个教程),基于这样名声 spring cloud扩展了非常多的框架组件 添加到当前微服务架构落地中,形成一套框架集.
spring cloud alibaba (阿里提供的一套 一站式解决微服务框架)
# 4 spring cloud alibaba 微服务框架集
## 4.1 本阶段学习特点
- 技术独立的
- 单独学习 没有整体效果
- 至少学习三个组件才能有整体效果
- nacos
- dubbo
- gateway
## 4.2 调整安装nacos的环境
### 4.2.1 解压文件
- 课前资料
- nacos-server压缩包 **解压 没有中文 没有空格的路径
### 4.2.2 检查JAVA_HOME环境变量
在当前系统中 检查是否存在JAVA_HOME环境变量.
windows cmd终端
```shell
echo %JAVA_HOME%
```
mac 终端
```shell
echo $JAVA_HOME
```
必须配置 并且要求 使用**jdk1.8** 要是路径**没有空格.**
环境变量配置(windows)
此电脑--> 右键属性-->高级系统设置-->高级-->环境变量
### 4.2.3 运行nacos启动脚本
- 找到nacos家目录中的bin文件夹
- 使用终端 进入这个文件夹bin
windows
```shell
startup.cmd -m standalone
```
mac
```shell
startup -m standalone
```
```shell
./startup -m standalone
```
windows cmd 控制台运行输入日志时,有可能会卡主.多敲击几次回车,反之这种情况出现.
- 访问页面控制台
http://localhost:8848/nacos
用户名 \ 密码: nacos \ nacos
### 4.2.4 停止nacos
- 在终端窗口 ctrl+c
比较慢.
- 停止脚本
在bin文件夹
```shell
shutdown.cmd
```
mac
```shel
shutdown
```
```shell
./shutdown
```
## 4.2 Nacos介绍
官网: https://nacos.io/en-us/docs/v2/what-is-nacos.html
Nacos 致力于帮助您**发现**、**配置**和管理**微服务**。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos在微服务架构中,实现使用的功能包括 服务治理,配置的管理.
当前市场上任何一种微服务落地方案,都需要这种软件存在,即使不使用nacos,也需要别的相同功能的软件技术来代替(console eureka zookeeper)
## 4.3 注册发现功能
### 4.3.1 基本结构
nacos运行过程中 包括2个重要角色(2种不同的进程)
- nacos-server: nacos服务端进程(刚才咱们启动的那个进程)
- nacos-client: 配置组合到我们开发的web应用的一个子进程.
### 4.3.2 order服务整合nacos-client
springboot框架的存在 spring很多其它子项目 都能做到快速开发.(springboot 和 spring什么关系)
在springboot装备 spring cloud框架集中的某个组件步骤. 就需要简单的三步.
- 用什么 就添加什么依赖: 准备依赖
- 根据使用软件技术 配置 yaml文件: 准备 yaml文件属性
- 简单的自定义配置 或者直接使用注解
**第一步:** order-main 添加一个nacos依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
```
**第二步:** yaml文件配置
只需要在当前阶段 准备一个 和nacos-server连接的地址.
```yml
spring:
application:
#spring 应用 只要开发的springboot 必定是spring应用
#服务业务功能名称 服务名称
name: luban-demo-order
cloud:
nacos:
discovery:
#当前已经整合的nacos-client会读取这个server-addr从而和nacos-server通信
server-addr: localhost:8848
```
**第三步:** 使用nacos的发现注解(非必要的配置)
在启动类中 添加一个nacos发现注解. 强调当前web应用是一个具有nacos-client客户端角色进程的web应用.
@EnableDiscoveryClient
order-main 启动 之后可以和nacos-server进行通信和交互.
访问 localhost:8848/nacos 控制台 左侧静态菜单--服务管理
### 4.3.3 课堂跟踪练习 实现 cart 和stock 整合nacos-client

1. (基本):将cart和stock2个web应用中 也整合进入nacos-client角色 同时在 nacos-server进行注册
luban-demo-cart luban-demo-stock
2. (扩展): 搭建一个微服务的集群 ,同时在nacos-server注册 同一个服务的多个实例.
### 4.3.4 微服务集群 多实例
每个服务提供功能,为了应对高并发,同一个服务(luban-demo-order),在当前集群中启动多个**实例**.
来形成微服务集群-----**分布式集群结构**
**分布式:**
广义意义: 所有程序软件 多个进程 分摊任务执行同一件事,可以叫做分布式
狭义意义:
- 业务分布式: 微服务
- 存储容器分布式: redis 集群 mysql集群 es集群 队列集群
使用idea配置多实例的web应用启动-cart-main为例.
- 端口不同(多个实例同时在一个服务器运行)
利用java运行时 传递的选项 覆盖源码中 yaml文件端口配置
同时可以提交JVM参数.
-Xmx128m 最大堆内存
-Xms128m 最小堆内存
还有一批选项 是可以覆盖源码配置的,例如我想覆盖源码 server.port
--server.port=8000
同一个服务多实例配置 如下步骤
**第一步**: 准备一个可以启动的项目.
**第二步:** 修改启动配置项




### 4.3.5集群属性配置详解
nacos可以管理的微服务集群 多实例的 庞大的架构体系. 所以有很多管理的细节.需要通过 实例配置yaml属性来了解.
- 临时实例/永久实例
服务下面都有若干实例,对于nacos可以分为**永久**实例和**临时**实例,默认是临时的.
当前启动进程是永久实例还是临时实例 true 表示临时实例 false表示永久实例.
```yaml
spring:
cloud:
nacos:
discovery:
ephemeral:
```
临时实例特点:如果临时实例宕机,会被nacos服务端删除注册信息.--**根据高峰并发的需求,临时增加,抗住高峰之后,直接停止**
永久实例特点:nacos永远不会删除永久实例的注册信息,即使永久实例宕机,也仅仅只是记录健康状态为不健康--**最低集群规模,保证日常使用**
- 服务器的ip地址
注册到nacos服务端的时候,客户端要携带一个重要的参数 ip地址,决定了发现的一点是否能准确调用 提供者服务功能.
默认nacos-client 会从某一个网卡中读取一个ip地址使用(windows本地跑进程,任何一个网卡的ip地址都可以使用).
如果在服务器,集群,物理机等位置运行进程,网卡的ip地址,最好是手动配置.默认未必能够找到正确的ip地址.
```yaml
spring:
cloud:
nacos:
discovery:
ip:
```
- 服务剔除 心跳检测(**逻辑重要**)
nacos实例,作为一个进程,内部包含了一个nacos客户端,在启动注册之后,需要和nacos服务端建立一个健康状态检测机制.
**永久实例:** nacos服务端会进行下探. 主动询问. 如果下探发现没有响应,记录健康状态为false.下探不会停止.
**临时实例:** 临时实例自己,主动上报(心跳检测). nacos服务端记录每次上报的时间戳.

超时剔除
- 永久实例: 永不剔除
- 临时实例: 每个一段时间,发送一次心跳上报请求,nacos-server记录每个临时实例一个最后lastHeartBeatTimeStamp. 并且nacos-server会定时循环所有临时实例,检查心跳时间是否超时. 如果超过临界值,记录健康状态,如果超过剔除的临界值将临时实例的注册信息剔除掉.
```yaml
spring:
cloud:
nacos:
discovery:
heart-beat-interval: 5
ip-delete-timeout: 20
```
heart-beat-interval: 5 表示对当前实例心跳间隔时间
ip-delete-timeout: 20 对于临时实例 最长的心跳超时时间,超过这个时间将会剔除
特点: 本地有一个保护机制,一旦发现不健康的临时实例 用最快的定时删除.
配置多长时间,有什么因素影响.
1. 心跳间隔 一定要小于超时删除时间.
2. 网络情况. 没有抖动(网络通常的),一般配置时间间隔相对可以短一点.尽可能保证实时状态.
3. 网络有抖动,状态不是特别好,为了避免频繁的心跳,短时间配置导致的误判.时间可以延长.
- 命名空间
微服务集群的规模是非常庞大的. nacos根据实际场景 对所有的实例的集群做分级管理.
命名空间就是分级管理的第一层.
在开发过程中,有很多的环境,开发环境dev,测试环境test,生产环境prod. 数据需要隔离.
默认命名空间 叫做public. 我们可以手动创建.比如当前开发环境dev.可以创建命名空间 dev.

可以通过实例进程yaml属性配置指定命名空间得id值.
```yaml
spring:
cloud:
nacos:
discovery:
namespace:
```
- 分组策略(灰度发布)
在同一个命名空间,可以通过分组的配置,使得服务之间进行隔离,
分级管理的第二层----分组配置
分组的应用功能---**灰度发布**. 灰度发布的意思就是在同一个环境中 存在多个不同版本的服务进程.
例如: 电商 互联网项目(美团)
持续发布的项目: 同时存在很多版本
版本发布项目: 只能存在一个固定版本
通过yaml属性配置分组
```yaml
spring:
cloud:
nacos:
discovery:
group:
```
- 服务名称
nacos客户端 可以通过一个属性 自定义服务名称,如果不指定默认值是spring.application.name得引用. 如果都不存在 null就会报错.
```yaml
spring:
cloud:
nacos:
discovery:
api:
```
总结分级管理:
1. nacos作为微服务集群的管理组件,庞大的微服务集群,存在非常多的进程实例
2. 每一个进程实例进入nacos注册信息 必定包含3层管理分级结构
3. 第一层: 命名空间,区分隔离开不同的环境 dev test prod
4. 第二层: 同一个命名空间 使用分组相互隔离实例集群,可以做到同时存在多个版本服务端进程
5. 第三层: 服务名称,功能业务的隔离,不同的业务实例一定属于不同的服务名称.
### 4.3.6 课堂跟踪练习
根据下图,完成多实例微服务集群的启动.




# 1 问题解析
## 1.1 问题1
### 1.1.1 问题现象
启动永久实例 健康状态为false
### 1.1.1 原因
使用注册的ip地址不足以保证nacos-server下探成功
# 2 NACOS配置中心
## 2.1 配置中心概括
### 2.1.1 应用场景
开发学习一个微服务架构,其中会有很多的服务,每个服务又有很多的实例---微服务集群.
每个服务开发过程中,需要编辑配置大量的web文件,比如 **yaml,json,html,xml,....**
如果各自服务 管理配置各自配置文件. 出现问题: **重复配置**,**难以管理**
**在多团队和多人员开发的场景中,一般这种文件都要交给配置中心管理.**
### 2.1.2NACOS配置中心基本结构
Naocs配置中心管理结构和注册中心管理结构极其相似.
- 角色分配
- 配置中心服务端: nacos-config-server 负责管理同一个的配置文件 按照三级分层管理,命名空间 分组 文件id
- 配置中心客户端: 省略公用的配置文件,连接nacos-config-server在启动应用程序之前 先读取远端配置文件.

而且 不一定把所有当前集群管理不同服务管理的公用配置都放到一个远端文件.

## 2.2 配置中心应用
### 2.2.1 nacos-config-client整合到web应用
**以order为例**,做nacos配置中心客户端的整合,步骤:
- 依赖
- yaml属性
- 配置类或者注解的使用(省略了)
**第一步**: 添加nacos-config-client依赖
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
```
**第二步**: yaml文件配置
springboot 2.5.9 需要在配置中心客户端配置的yaml不再是 application.yaml
而叫做 bootstrap.yaml 配置nacos-config-client的属性.需要引入一个资源jar包帮助springboot项目 读取**bootstrap.yaml**.
如果是springboot版本2.3.X之前 不需要额外的依赖资源,自动支持bootstrap.yaml加载逻辑.
1. 准备一个配置文件 bootstrap.yaml
2. bootstrap.yaml加载流程
目的: 理解bootstrap.yaml和application.yaml得关系.

结论:
bootstrap.yaml完全可以代替application.yaml工作 但是 不允许这么干.
bootstrap.yaml中配置的属性会被applicaiton.yaml后加载的逻辑覆盖.
bootstrap.yaml最主要的功能是利用它在应用程序启动之前连接 nacos配置中心读取通用复用的配置文件.
具体配置内容
```yaml
# 指定一个环境后缀 值无法被application.yaml覆盖
spring:
profiles:
active: local
#使用 分隔符 --- 区分不同的环境配置
---
#当前区域配置环境 local
spring:
config:
activate:
on-profile: local
application:
# 一般 spring的应用名称 在bootstrap存在的时候 需要配置
name: luban-demo-order
#bootstrap提前加载的目的 就是配置nacos服务端信息
cloud:
nacos:
config:
server-addr: localhost:8848
---
spring:
config:
activate:
on-profile: test
---
spring:
config:
activate:
on-profile: dev
```

**第三步**: 启动检查日志
发现nacos-config-client已经生效了,而且在读取3个默认的文件.

1. 证明nacos-config-client 正常工作
2. 默认读取一批配置文件,虽然这个文件在nacos-config-server不存在.
名字规律:
{spring.application.name}
{spring.application.name}.{fileExtension}
{spring.application.name}-{spring.profiles.active}.{fileExtension}
**第四步:**根据默认读取的配置文件 准备远程配置文件. 准备默认读取的第一个文件


结论: 将本地 spring datasource属性删除,没有从远程读取之后 解决本地需求问题
1. 文件格式 很重要 文件名称 luban-demo-order 不符合spring boot 要求 properties yml
2. 尝试配置 properties后缀的默认文件 luban-demo-order.properties
properties
```proper
spring.datasource.url=jdbc:mysql://localhost:3306/luban_demo_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
```
课堂小练习:
1. 利用nacos-config-client连接远程nacos-config-server
2. 准备好一个文件 {spring.application.name}.**yaml**
3. 读取远程datasource属性
### 2.2.2 指定读取配置文件
自己指定相关读取的配置文件,例如: redis.yaml mysql.yaml es.yaml mybatis.yaml datasource.yaml
了解 nacos-config-client配置属性内容:
- 指向配置中心服务
```yaml
spring:
cloud:
nacos:
config:
server-addr: localhost:8848
```
- 文件所属命名空间
```yaml
spring:
cloud:
nacos:
config:
namespace: 123456
```
不指定命名空间 就到public读取配置文件.
- 分组配置
```yaml
spring:
cloud:
nacos:
config:
group: 1.0.0
```
不指定分组 默认 DEFAULT_GROUP 你的文件如果不是这个分组名称 一定读不到.
- dataIds 文件名称(读取的指定文件)
可以指定一个配置中心下 多个自定义的文件名称. 甚至每个文件都可以单独指定分组 group refresh
```yaml
spring:
cloud:
nacos:
config:
shared-configs:
- dataId: datasource.yaml
- dataId: aaa.json
```
- 指定读取的默认文件 的后缀
```yaml
spring:
cloud:
nacos:
config:
file-extension: yaml
```
默认值properties,并不影响 sharded-configs 中单独配置的文件.
最后完成一个任务 将bootstrap.yaml文件配置完整
```yaml
# 指定一个环境后缀 值无法被application.yaml覆盖
spring:
profiles:
active: local
#使用 分隔符 --- 区分不同的环境配置
---
#当前区域配置环境 local
spring:
config:
activate:
on-profile: local
application:
# 一般 spring的应用名称 在bootstrap存在的时候 需要配置
name: luban-demo-order
#bootstrap提前加载的目的 就是配置nacos服务端信息
cloud:
nacos:
config:
server-addr: localhost:8848
# 不会影响 sharded-configs
file-extension: yaml
namespace: 123456
# 不会影响sharded-config
group: 1.0.0
shared-configs:
- dataId: datasource.yaml
- dataId: aaa.json
---
spring:
config:
activate:
on-profile: test
---
spring:
config:
activate:
on-profile: dev
```
使用datasource.yaml 作为本地文件的替换.
### 2.2.3 综合练习
- 需求目标
- 练习操作步骤
**第一步**: 依赖 (参考整合入门案例)
**第二步:** 准备一个bootstrap.yaml配置
1. 开启的环境配置名称
2. 按照格式配置的某一个环境
3. 隔离不同环境的符号 ---
1. 连接nacos-config-server 地址
2. 命名空间.
3. 文件后缀 ,只会影响默认的文件读取
4. 分组名称,只会影响默认的文件读取
5. 自定义文件,每个文件参数都是一个对象数据
5.1. 文件名称 dataId
5.2. 文件分组 group 默认DEFAULT_GROUP
5.3. 是否在服务端文件发生变动时,给nacos-config-client推送变动信息
**第三步:** 根据本地客户端配置在nacos-config-server准备对应文件
1. datasource.yaml(检查对应关系和内容)

2. mybatis.yaml

通过案例 理解 真实场景中 能够扩展和应用的结构

### 2.2.4 课堂跟踪练习
需求: 将mybatis 和datasource的属性 提取到远程配置中心之后. luban-demo-cart luban-demo-stock 同时整合nacos-config-client读取文件.
# 3 nacos运行原理
## 3.1 注册中心
### 3.1.1 基于CS结构运行的
C: client 客户端(整合web应用中)
S: server服务端(单独启动的进程)
### 3.1.2 服务端
启动那个nacos进程 作为服务,提供了对外可以访问的接口功能,支持 http协议 grpc协议等不同的访问渠道.
目前nacos-client是通过http协议访问的,也就是web应用运行后 nacos-client跟服务端进行请求响应交互的.
每次客户端启动 都会根据我们在应用配置文件中提供的属性(有一部分有默认值).拼接一个请求http.
向指定的nacos服务端发送这个请求. 服务接收请求之后 处理服务信息注册申请,在内存中保存一份注册信息.


官方网址 接口文档:https://nacos.io/zh-cn/docs/open-api.html
## 3.2 配置中心
和注册中心结构相同 CLIENT SERVER 主要客户端功能获取需要的配置
接口


# 4 NACOS总结
## 4.1 配置中心
在复杂的微服务集群中,真正运行部署时,需要通过配置中心来协调搭建和配置工作.
但是在学习过程中,开发课程项目,不需要使用.
## 4.1 注册中心
总结成以下的一个原则:
注册中心功能:
- **注册**: 注册的目的,就是为了让其他调用当前业务功能的系统可以及时发现.
- **发现**: 需要调用别的业务,就一定要在nacos进行发现(get)得操作.
场景: 如果不需要被调用 就可以不注册,如果不需要调用被人就可以不发现.
定时任务:
背景: 下单减库存后 没有及时支付,因为采用一个方案来解决这种超时未支付的订单.
**解决方案1**: 开发一个定时服务,每个**1分钟/2分钟/5分钟 **扫描一下订单表格. 将所有的超时未支付订单扫描出来 进行关闭操作.(怎么扫描,表格的select操作); 判断**超时 ** **未支付**
select order.* from order where pay_status=0 create_time+15min girlFriends;
}
public class School{
private String name;
}
public class GirlFriend{
private String nickname;
}
```
- yaml
```yaml
user:
username: 成恒
school:
name: 清华大学
girlfriends:
- nickname: 真真
- nickname: 莲莲
- nickname: 爱爱
```
### 1.2.4 案例4
- 类
```java
@ConfigurationProperties(prefix="user")
public class UserProperties{
private String username;
private School school;
private List girlFriends;
}
public class School{
private String name;
}
public class GirlFriend{
private String nickname;
private Integer age;
}
```
- yaml
```yaml
user:
username: 成恒
school:
name: 清华大学
girlfriends:
- nickname: 真真
age: 18
- nickname: 莲莲
age: 19
- nickname: 爱爱
age: 20
```
### 1.2.5 案例5
- 类
```java
@ConfigurationProperties(prefix="user")
public class UserProperties{
private String username;
private School school;
private List girlFriends;
private Map favourites;
}
public class School{
private String name;
}
public class GirlFriend{
private String nickname;
private Integer age;
}
```
- yaml
```yaml
user:
username: 成恒
school:
name: 清华大学
girlfriends:
- nickname: 真真
age: 18
- nickname: 莲莲
age: 19
- nickname: 爱爱
age: 20
favourite:
"食品类": "披萨"
"游戏类": "原神"
```
### 1.2.6 案例6
- 类
```java
@ConfigurationProperties(prefix="user")
public class UserProperties{
private String username;
private School school;
private List girlFriends;
private Map> favourites;
}
public class School{
private String name;
}
public class GirlFriend{
private String nickname;
private Integer age;
}
```
- yaml
```yaml
user:
username: 成恒
school:
name: 清华大学
girlfriends:
- nickname: 真真
age: 18
- nickname: 莲莲
age: 19
- nickname: 爱爱
age: 20
favourite:
"食品类":
- "披萨"
- "面条"
"游戏类":
- "原神"
- "星穹铁道"
```
### 1.2.7 案例7
- 类
```java
@ConfigurationProperties(prefix="user")
public class UserProperties{
private String username;
private School school;
private List girlFriends;
private Map> favourites;
}
public class School{
private String name;
}
public class GirlFriend{
private String nickname;
private Integer age;
}
public class Favourite{
private String name;
}
```
# 1 阶段性架构图

纵向拆分,提出了2个比较明显的问题
1. 集群规模变大 ,相互之间 无法寻找(没有注册信息)
2. 无法实现相互调用(RPC)
# 2 DUBBO框架
## 2.1 RPC通信协议
### 2.1.1 计算机网络通信协议
http
tcp/ip
udp
通信协议 相当于网络中 端到端 通信的途径 手段.

### 2.1.2 通信序列化协议

1. 通信输出的数据本身是内存数据
2. 通信接收的数据也是内存数据
3. 输出的一方要进行序列化操作
4. 接收的一方要进行反序列化
序列化内存数据(变成二进制)的方式有哪些种:
JAVA序列化
**google proto buf**
avro
thrift
**hessian**
序列化框架不同,数据 序列化计算的方法就不同,性能有所有差别.
### 2.1.3 通信案例
- 案例版本version1
Server
```java
package com.tarena.test.luban.demo.rpc.demo01.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 利用socketServer监听一个端口 20000
* 等待客户端 通过socket发送请求
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server=new ServerSocket(20000);
//每当有一个请求 服务端要处理一个请求socket对象
while(true){
Socket socket = server.accept();
System.out.println("接收到了");
socket.close();
}
}
}
```
client
```java
package com.tarena.test.luban.demo.rpc.demo01.client;
import java.io.IOException;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//建立一个链接 127.0.0.1:20000
Socket socket=new Socket("127.0.0.1",20000);
}
}
```
上述代码 只是简单的建立了一下通信.使用的底层通信协议(Socket封装是tcp) tcp.
但是建立这种通信,不仅仅可以使用socket tcp协议还可以使用其它api包装的其它协议.
- 案例版本version2
传递一些数据 String "你好啊"
socket数据传递 流的关系

client
```java
package com.tarena.test.luban.demo.rpc.demo01.client;
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//建立一个链接 127.0.0.1:20000
Socket socket=new Socket("127.0.0.1",20000);
//client 内存数据
String message="你好啊";
//拿到当前socket的数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(outputStream);
//dos输出字符数据 过程涉及到 java序列化
dos.writeUTF(message);
//刷出缓存
dos.flush();
//不仅仅输出 到server 还可以接收server返回数据
InputStream inputStream = socket.getInputStream();
DataInputStream dis=new DataInputStream(inputStream);
//返序列化将server数据解析
String result = dis.readUTF();
System.out.println("服务端返回:"+result);
}
}
```
server
```java
package com.tarena.test.luban.demo.rpc.demo01.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 利用socketServer监听一个端口 20000
* 等待客户端 通过socket发送请求
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server=new ServerSocket(20000);
//每当有一个请求 服务端要处理一个请求socket对象
while(true){
Socket socket = server.accept();
process(socket);
System.out.println("接收到了");
socket.close();
}
}
public static void process(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
DataInputStream dis=new DataInputStream(inputStream);
//反序列化
String input = dis.readUTF();
System.out.println("服务端解析:"+input);
//返回服务端数据
String result="你也好啊";
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(outputStream);
dos.writeUTF(result);
dos.flush();
}
}
```
### 2.1.4 RPC概念
RPC (remote precedure call) 远程过程通信. 基于通信手段 选择序列化协议,实现远程调用和本地调用没有区别.
- 包含了通信协议(http tcp udp)
- 包含了通信序列化方法(java avro thrift google protobuf hessian)
- 远程通信调用 客户端在**调用**方法或者功能时和本地没区别.
更新案例代码version3
client
```java
package com.tarena.test.luban.demo.rpc.demo01.client;
import com.tarena.test.luban.demo.rpc.demo01.client.api.PeopleProxy;
import com.tarena.test.luban.demo.rpc.demo01.server.api.People;
import java.io.*;
public class Client {
public static void main(String[] args) throws IOException {
People people=new PeopleProxy();
String 王老师 = people.sayHi("王老师");
System.out.println(王老师);
}
}
```
client 知道server对外暴露的接口 但是他没有这个接口的真正实现.
他只有代理,实现底层通信过程.
PeopleProxy
```java
package com.tarena.test.luban.demo.rpc.demo01.client.api;
import com.tarena.test.luban.demo.rpc.demo01.server.api.People;
import java.io.*;
import java.net.Socket;
public class PeopleProxy implements People {
@Override
public String sayHi(String name) throws IOException {
//建立一个链接 127.0.0.1:20000
Socket socket=new Socket("127.0.0.1",20000);
//拿到当前socket的数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(outputStream);
//dos输出字符数据 过程涉及到 java序列化
dos.writeUTF(name);
//刷出缓存
dos.flush();
//不仅仅输出 到server 还可以接收server返回数据
InputStream inputStream = socket.getInputStream();
DataInputStream dis=new DataInputStream(inputStream);
//返序列化将server数据解析
String result = dis.readUTF();
System.out.println("服务端返回:"+result);
return result;
}
}
```
客户端一方 包装了底层通信 序列化的一个代理对象.实现的接口是server端对外暴露的接口.
server
```java
package com.tarena.test.luban.demo.rpc.demo01.server;
import com.tarena.test.luban.demo.rpc.demo01.server.api.People;
import com.tarena.test.luban.demo.rpc.demo01.server.api.impl.PeopleImpl;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 利用socketServer监听一个端口 20000
* 等待客户端 通过socket发送请求
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server=new ServerSocket(20000);
//每当有一个请求 服务端要处理一个请求socket对象
while(true){
Socket socket = server.accept();
process(socket);
System.out.println("接收到了");
socket.close();
}
}
public static void process(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
DataInputStream dis=new DataInputStream(inputStream);
//反序列化
String name = dis.readUTF();
System.out.println("服务端解析:"+name);
//调用本地方法
People people=new PeopleImpl();
String result = people.sayHi(name);
//返回服务端数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(outputStream);
dos.writeUTF(result);
dos.flush();
}
}
```
监听 绑定端口 等待socket请求 解析反序列化调用 本地方法
People
```java
package com.tarena.test.luban.demo.rpc.demo01.server.api;
import java.io.IOException;
/**
* server端对外暴露的接口
* 任何client都能看到和使用这个接口
*/
public interface People {
public String sayHi(String name) throws IOException;
}
```
对外暴露接口
PeopleImpl
```java
package com.tarena.test.luban.demo.rpc.demo01.server.api.impl;
import com.tarena.test.luban.demo.rpc.demo01.server.api.People;
/**
* server本地代码不对外暴露
*/
public class PeopleImpl implements People {
@Override
public String sayHi(String name) {
return "hello nice to meet "+name;
}
}
```
People接口本地实现.
当前这套代码 灵活度,可扩展度.并不高.
通信地址 无法动态获取.
## 2.2 DUBBO介绍
官网: https://dubbo.apache.org **apache dubbo 2.7.8** dubbo3.0版本有很大变化
dubbo是一个**RPC**框架. 序列化,底层通信协议,都可以已经包装好的.
Apache Dubbo 是一款高性能、**轻量级**的开源服务框架。
额外问题: Spring是不是**轻量级**框架.
**轻量级:** 使用**方便快捷,**不会在使用过程中过多的**投入**精力和成本在环境配置上.
Spring总体是轻量级框架,但是早期的配置纯粹的重量级(Springboot出现之后不存在).
## 2.2 DUBBO基本结构
基于当前正在需要解决的需求: **luban-demo-order** 调用**luban-demo-cart** 删除购物车
在dubbo框架实现rpc调用过程中 存在3个核心角色
- provider: 服务提供者
- consumer: 服务调用者
- 服务协调器: 注册中心
## 2.4 DUBBO入门案例
### 2.4.1 Provider
- 确定谁是provider
luban-demo-cart购物车是provider.
- 提供对外调用的接口
选在luban-demo-cart-api 单独项目 单独布置接口类
```java
package com.tarena.luban.demo.cart.api;
/**
* 对外暴露的接口 案例练习
*/
public interface DubboTestRpcApi {
/**
* sayHi 方法
* param: String 任命
* return: 打招呼字符串
*/
public String sayHi(String name);
}
```
- main依赖 api
luban-demo-cart-main pom添加以下依赖关系

- 对外暴露接口的实现
在luban-demo-cart-main中实现刚刚定义的暴露接口.
第一次使用luban-demo-cart-api,需要在实现之前 做一下main 依赖 api
```java
package com.tarena.demo.luban.all.main.rpc;
import com.tarena.luban.demo.cart.api.DubboTestRpcApi;
public class DubboTestRpcApiImpl implements DubboTestRpcApi {
@Override
public String sayHi(String name) {
return "hello dubbo demo i am "+name;
}
}
```
- 引入 dubbo依赖
luban-demo-cart-main 加入dubbo依赖.

- dubbo实现provider服务端进程 底层代码
dubbo提供的api方法 ,可以直接用来完成provider 服务提供者 **监听端口** **等待访问**,**调用实现**,**注册服务**信息的所有逻辑.
```java
package com.tarena.test.luban.demo.cart;
import com.tarena.demo.luban.all.main.rpc.DubboTestRpcApiImpl;
import com.tarena.luban.demo.cart.api.DubboTestRpcApi;
import org.apache.dubbo.config.ProtocolConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.ServiceConfig;
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
/**
* 开启一个dubbo底层的客户端进程
* 监听端口 等待访问调用
*/
public class DubboProvider {
public static void main(String[] args) {
//配置信息类 那些接口 需要实现类访问 ,实现类是谁 注册中心是谁
ServiceConfig providerService=new ServiceConfig<>();
// 注明当前服务 对外暴露的具体接口
providerService.setInterface(DubboTestRpcApi.class);
// 真正实现 是谁 new DubboTestRpcApi()
providerService.setRef(new DubboTestRpcApiImpl());
//创建一个 dubbo-provider 进程对象
DubboBootstrap provider = DubboBootstrap.getInstance();
//provider进程对象中填写具体属性
//dubbo应用名称 必填,可以和spring.application.name复用
provider.application("cart-provider");
//注册中心
provider.registry(new RegistryConfig("nacos://localhost:8848"));
//本地服务端进程 监听端口
provider.protocol(new ProtocolConfig("dubbo",20990));
//提供的服务
provider.api(providerService);
//启动等待
provider.start().await();
}
}
```
### 2.4.2 Consumer
确定consumer是谁
luban-demo-order是消费端.
- 依赖暴露的接口
order作为消费者,需要暴露的接口代码来实现调用. 实现者是dubbo创建的具备底层连接对象的那个代理.

- 缺少dubbo依赖
luban-demo-order-main 依赖dubbo,才能编写consumer代码.
```java
package com.tarena.test.luban.demo.order.rpc;
import com.tarena.luban.demo.cart.api.DubboTestRpcApi;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
/**
* dubbo调用远程方法的消费端 客户端
*/
public class Consumer {
public static void main(String[] args) {
//最终就负责调用 模拟对象注入的场景 service业务层可以调用 controller控制层 repo
DubboTestRpcApi testRpcApi = getApi();
String result = testRpcApi.sayHi("王翠花");
System.out.println("获取sayHi方法结果"+result);
while(true);
}
//通过dubbo拿到可以远程调用的接口对象
//实现逻辑是底层通信 连接
public static DubboTestRpcApi getApi(){
//指定配置 消费端的配置类
ReferenceConfig consumerConfig=new ReferenceConfig<>();
//只需要设置接口
consumerConfig.setInterface(DubboTestRpcApi.class);
//dubbo客户端对象
DubboBootstrap instance = DubboBootstrap.getInstance();
//服务信息 调用consumer还是provider 都应该属于某一个dubbo应用
instance.application("order-consumer");
//注册中心抓取数据
instance.registry(new RegistryConfig("nacos://localhost:8848"));
//在实例中 添加 consumer配置类
instance.reference(consumerConfig);
//调用get获取 proxy代理对象
DubboTestRpcApi dubboTestRpcApi = consumerConfig.get();
//打印 类名称
System.out.println(dubboTestRpcApi.getClass().getName());
return dubboTestRpcApi;
}
}
```
- nacos显示consumer信息

### 2.4.3 DUBBO底层api
dubbo底层api确实可以实现远程调用.不关心使用通信协议 和序列化框架.
但是 配置步骤在代码里比较繁琐. 如果在业务逻辑中频繁不断地编写这种 配置代码,会造成业务的**侵入性**过大,代码**内聚性**比较低(侵入性大小 内聚性高低 取决于你能否专心的写业务).
所以如果想要将dubbo和spring springboot整合 需要单独创建配置类---dubbo已经实现了.
## 2.4 DUBBO整合SPRINGBOOT框架
### 2.4.1 springboot整合的老三步
- 依赖dubbo
- **yaml配置**
- **注解+配置**
yaml配置属性+注解配置类 合并到一个xml配置文件.
#### 2.4.1.2 PROVIDER
luban-demo-cart
- provider角色项目添加dubbo依赖
```xml
org.apache.dubbo
dubbo
2.7.8
```
- provider配置xml
将底层配置的dubbo api代码 迁移到xml配置文件.

dubbo.xml 内容主要作用,就是配置dubbo的provider和consumer 代替了之前编写的dubbo api.
程序加载这个xml之后 ,会在内存中创建dubbo整合spring相关配置类.
```
```
配合dubbo:service引用注入的对象,实现类需要添加注解

- web应用加载xml
在启动类中 添加一个注解@ImportResource.

- 启动springboot
在注册中心 这个web应用 整合了2个组件 nacos-client dubbo。所以会出现2个注册信息。

#### 2.4.1.2 CONSUMER
luban-demo-order
- 依赖(略)
和provider一样
- 配置dubbo.xml
```
```
- web应用加载xml
luban-demo-order-main 启动类 添加@ImortResource注解

- 业务代码注入接口对象调用方法

### 2.4.2 功能扩展
- 方法扩展(dubbo框架中不需要做额外配置)


- 接口扩展
1. provider



2. consumer


# 3 dubbo总结回顾

# 附录
# 1 当前dubbo配置的问题
## 1.1 jdk版本问题
### 1.1.1 现象
启动任何当前测试dubbo的案例 都无法运行。 抛出异常是 无法解析。
### 1.1.2 原因
dubbo 2.7.8 最稳定的jdk版本是1.8 所以如果是java 16 java 17 就一定会出这个问题
### 1.1.3 解决方法
- 调整当前项目的jdk版本为1.8
FILE--> project structure--> project jdk调整1.8



- 执行maven clean
在顶级父工程 执行mvn clean。重新运行。
# REDIS 哨兵集群
# 1 REDIS SENTINEL
## 1.1 REDIS结构变化
### 1.1.1 单机版
- 存在的问题
1. 读写瓶颈有上限(3万-5万上限,但是1万是保证稳定的运行 1万条/S)
2. **单机故障**
- 应用场景
1. 开发环境 只需要走通业务逻辑,不需要关心故障,关心并发
哨兵集群 就是为了解决单机故障存在的一个结构.
## 1.2 哨兵(sentinel)集群
### 1.2.1 主从结构
redis 支持主从结构. 在数据存储容器技术中,主从是解决单机故障的必要方案.
### 1.2.2 演示redis主从
redis支持一主多从,支持多级主从.但是考虑同步数据成本问题,一般 1主 1从
演示的结构 一主 二从
总结: 主从演示完毕 可以实现主从数据同步
但是,能否实现**高可用(HA)**
### 1.2.3 哨兵进程
是一种特殊的redis进程,不负责读写数据(单独启动的这个哨兵有点浪费)
主要负责监听监控主从集群
启动哨兵集群,监听一个主从,集群哨兵中选取一个leader负责心跳检测.
leader 连接主节点和从节点.
- 从节点宕机: ping/pong 失败, 通知其它哨兵 连接 防止单个哨兵判断失误.最终投票做结论.如果判断结果是宕机 记录从节点宕机状态.
- 主节点宕机: ping/pong失败,通知其它节点哨兵判断,如果最终投票失败,将会进行新一轮投票.从2个从节点选举一个新的节点作为主节点使用.
- 回复的主节点在恢复的一瞬间 角色还是master.哨兵leader 发现回复.哨兵会通知这个主节点,你已经不是主节点了,重新发送命令挂接新的主节点
主从结构中,永远都是master做写操作. 读操作主从都能实现. 提升读的效率,总体来讲,并发和读写瓶颈还是比较高的. 无法解决高并发访问. 比如 1秒钟搜索50万条商品数据.
### 1.2.4 redis五种经典数据类型
String类型
- set
- get
- incr/decr 自增和自减1
- incrby/decrby 自增自减自定义部署
- append
- setnx 在不存在的key的情况下可以set数据
Hash类型
- hset: 生成一个hash数据
- hget: 获取一个hash的field
- hkeys: 拿到所有field
- hvals: 拿到所有value
- hincrby: 对某一个field数字类型数据 做增加或者减少
- hmset: 批量写
- hmget: 批量读
List
- lpush/rpush
- lrange
- rpop/lpop
- lset
- linsert
Set
- sadd
- srem
- scard
- sismember
ZSet
- zadd
- zcard
- zrank
- zrange
- zrangebyscore
# 1 DUBBO
## 1.1 阶段性结构
## 1.2 实现订单调用删除购物车
### 1.2.1 实现CartRpcApi
```java
package com.tarena.demo.luban.all.main.rpc;
import com.tarena.demo.luban.all.main.service.CartService;
import com.tarena.demo.luban.protocol.cart.param.CartDeleteParam;
import com.tarena.luban.demo.cart.api.CartRpcApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* bean的id是默认的 cartRpcImpl
*/
@Component
public class CartRpcImpl implements CartRpcApi {
@Autowired
private CartService cartService;
@Override
public void cartDelete(String userId, String productCode) {
//调用cartService删除购物车
CartDeleteParam cartDeleteParam=new CartDeleteParam();
cartDeleteParam.setUserId(userId);
cartDeleteParam.setProductCode(productCode);
cartService.deleteCart(cartDeleteParam);
}
}
```
### 1.2.2 订单调用stock减库存
- 确认角色
- stock: provider
- order: consumer
- provider:
- 暴露接口: 思考减库存 记录库存日志 传递什么参数(count productCode orderSn)
- 实现接口: 依赖关系(接口放到api 实现放到main)
- 配置dubbo
- 依赖dubbo
- xml配置: 三个固定标签 application registry protocol 一个根据代码dubbo:service
- 启动类 提供导入
- consumer:
- 依赖暴露的接口
- 配置dubbo
- 依赖dubbo(略)
- xml配置: 补充一个dubbo:reference
- 启动类 导入xml(略)
## 1.3 DUBBO高级特性
### 1.3.1 负载均衡
- 场景
dubbo远程调用的时候 provider一般都是一个多实例的集群.
**负载**: 访问就是负载.基本可以理解为流量 和访问 请求
**均衡**: 按照逻辑 进行不同算法的分配负载.
- 负载均衡的算法
1. 加权随机(Weighted Random)
dubbo负载均衡调用 默认算法.
按照随机分配 默认权重是1. 但是可以根据服务器性能 变动权重值.
权重值越大,随机碰撞概率越高 收到负载访问越多.
2. 加权轮询(RoundRobin LoadBalance)
轮询: 挨个访问,加权轮询 权重值越高,轮询访问的次数越多
例如:
| 节点 | 自身权重 |
| ---- | -------- |
| A | 5 |
| B | 2 |
| C | 1 |
按照权重分配 负载均衡时 访问规律
AAABBCAAABBCAAABBC.......
会造成 一个问题 访问权重高的 过于集中.
dubbo对轮询算法 做了微小的改动. 假设按照上面的权重分配.
| 轮前权重 | 本轮胜者 | 合计权重 | 轮后权重(胜者减去合计权重,败者叠加自身权重) |
| ---------------- | -------- | -------- | ------------------------------------------- |
| A(3),B(2),C(1) | A | 6 | A(-3),B(4),C(2) |
| A(-3),B(4),C(2) | B | 6 | A(0),B(-2),C(3) |
| A(0),B(-2),C(3) | C | 6 | A(3),B(0),C(-3) |
| A(3),B(0),C(-3) | A | 6 | A(-3),B(2),C(-2) |
| A(-3),B(2),C(-2) | B | 6 | A(0),B(-4),C(-1) |
| A(0),B(-4),C(-1) | A | 6 | A(3),B(-2),C(0) |
- LeastActive
**最少活跃优先**. 在dubbo 调用的实例会进行计数,一段窗口期,会寻找最小计数的节点实例安排优先级,更高的权重访问.
- ShortesReponse
**最短响应时间**,调用响应时间 越短,被访问调用的次数就越多.
### 1.3.2 重试机制
dubbo 底层创建连接的时候,远程consummer调用provider存在重试机制,防止因为网络抖动的情况,造成一次调用的失败.
默认 retries=3 timeout= 1000.
可以通过xml修改 consumer远程调用失败重试的参数
例如:
网络环境非常稳定的,retries=2 timeout=500
网络环境非常不稳定,retries=5 timeout=3000
```xml
```
## 1.4 luban-demo调整nacos注册信息

luban-demo-order luban-demo-cart luban-demo-stock 全部 调整nacos客户端注册属性
**保证在 localhost:8848 public命名空间 DEFAULT_GROUP 注册信息.**
# 2 Spring Boot 自动配置逻辑
## 2.1 目标
- [ ] 通过学习springboot自动配置 **理解**自动配置原理
- [ ] 核心注解: 组合注解
- [ ] 开启自动配注解: @EnableAutoConfiguration流程
- [ ] 实现自定义starter
- [ ] 项目名称**-spring-boot-starter
- [ ] 根据功能需求编写自动配置类**AutoConfiguration
- [ ] 根据META-INF/spring.factories格式 填写这个类名进去
- [ ] 第三方其它项目只要依赖当前自动配置资源
## 2.2 测试工程
### 2.2.1 创建一个项目
luban-spring-boot-starter: **-spring-boot-starter 表示第三方 按照springboot自动配置 实现的jar包.可以扩展springboot没有实现的自动配置逻辑,例如: mybatis-spring-boot-starter
spring-boot-starter-** : springboot自己实现的支持自动配置逻辑的jar包
例如: spring-boot-starter-web spring-boot-starter-security spring-boot-starter-jdbc...
### 2.2.1 在这个项目中准备依赖
写测试代码 junit
写spring测试案例 spring-boot-starter
```xml
org.springframework.boot
spring-boot-starter
junit
junit
test
```
## 2.3 spring版本迭代
### 2.3.1 Spring1.X
大量编写xml配置文件的节点,spring框架开发应用程序,每个xml中都会使用大量bean标签,来实现
SPRING容器的IOC DI功能.**不存在注解** @Autowired @Component @Service @Controller @Repository
### 2.3.2 Spring2.X
java出现了jdk1.5,新特性**注解**,**反射**,枚举等功能.SPRING随之推出了基于java5的注解功能的新特性,IOC容器的注解,使得扫描注解能够构造bean对象,@Component @Service @Controller @Repository DI注入@Qualifier @Autowired.让在1.x时代编写大量的xml配置文件的工作减少了很多很多.
约定俗成的习惯
业务代码: 使用注解开发
技术软件: xml引入的 比如 Redis RabbitMQ RocketMQ Mybatis
### 2.3.3 Spring3.X
基于java5的注解功能上,spring扩展了大量的功能注解,比如**@Configuration @Bean**
**@ComponentScan @Import @ImportResource @PropertyResource**等等,他们可以让在2.x时代残留的那种xml配置,彻底的消失了,从xml配置完全转化成为代码注解的编写;
趋势: **配置XML越来越简单** springboot的出现打下了坚实的基础
### 2.3.4 Spring4.X/5.X
都是在基于这个趋势,实现更多注解的扩展,让代码的功能变得更强,开发的效率变得更高,出现了很多组合注解,@RestController 4.X时代,spring提供了一个叫做**条件注解的** **@Conditional**,springboot能够做到0 xml配置文件/约定大于配置 并不是springboot功劳. 本质是 spring的支持.
## 2.4 详解Spring 3.X注解
### 2.4.1 @Configuration
早期 spring框架 容器 启动需要加载一个xml文件 生成运行spring 上下文.
这种xml文件 描述了一个spring容器中几乎所有运行 bean对象的详细信息----**元数据**(META DATA)文件.
**元数据**: 描述数据的数据.例如: 楼房是数据 元数据包括层高 施工单位 地理位置. 人是数据 身份信息 年龄 身高 体重 眼镜度数 牙齿净白程度. maven工程 元数据集中存在于pom.xml spring容器 元数据 包括我们加载的所有配置逻辑 注解 标签.
@Configuration 属于描述一个spring容器的元数据-注解元数据.
**作用是完全代替一个xml文件**(原来23个xml,现在只需要23个@Configuration配置代码类)
- 测试案例
**第一步**: 准备一个测试使用spring.xml配置文件
test/resources 准备一个**demo01.xml** 自动添加最基本的spring约束头

```
```
**第二步**: 配置xml逻辑
在xml中准备一个bean标签,一旦spring 容器加载这个xml bean标签对应内容也被加载 bean对象在容器驻留了.
```
```
当前代码环境中不存在com.tarena.spring.boot.bean.Bean01
**第三步**: 创建Bean01
```java
package com.tarena.spring.boot.bean;
public class Bean01 {
/**
* 功能方法 表明自己身份
*/
public void identity(){
System.out.println("我是bean01");
}
/**
* 容器如果加载这个bean对象
* 默认调用无参构造方法
*/
public Bean01() {
System.out.println("当前容器加载了Bean01");
}
}
```
**第四步:** 编写运行spring容器的测试代码
```java
package com.tarena.spring.boot;
import com.tarena.spring.boot.bean.Bean01;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RunSpring {
/**
* 加载xml创建spring容器
* 获取spring上下文对象
*/
@Test
public void xmlLoad(){
ClassPathXmlApplicationContext
context =new ClassPathXmlApplicationContext("demo01.xml");
//获取bean对象
Bean01 bean01 = context.getBean(Bean01.class);//获取一个容器的bean对象 类型Bean01,如果只有一个Bean01的对象
//就返回 如果有2个,报异常
bean01.identity();
}
}
```
案例version1结论: 模拟spring1.x时代的代码开发场景. 一堆xml 每个xml一堆标签.
- 测试案例version2
**第一步**:编写一个对标demo01.xml的配置了 Demo01Config
```java
package com.tarena.spring.boot.config;
import com.tarena.spring.boot.bean.Bean01;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 对标 demo01.xml
* 添加一个spring3.0出现的配置注解
* @Configuration
*/
@Configuration
public class Demo01Config {
//加载配置类的标志
public Demo01Config() {
System.out.println("当前容器加载了demo01Config");
}
//使用注解@Bean 将方法的返回对象 驻留到容器中 管理
//相当于一个配置xml中bean标签 bean对象id默认就是方法名称
@Bean("bean01")
public Bean01 bean01(){
return new Bean01();
}
}
```
**第二步:** 配置类加载启动测试spring容器
```java
/**
* 加载配置类 启动spring容器
*/
@Test
public void configLoad(){
AnnotationConfigApplicationContext
context=new AnnotationConfigApplicationContext(Demo01Config.class);
Bean01 bean01 = context.getBean(Bean01.class);//获取一个容器的bean对象 类型Bean01,如果只有一个Bean01的对象
bean01.identity();
}
```
问题: 如果早期spring项目开发 需要大量的xml 提供配置管理 元数据,对标xml的配置类 也需要很多.是不是工作量也不小? 但是springboot 没有太多机会写很多配置类?
### 2.4.2 @Bean
在测试案例version2中,已经配合一个配置类完成@Bean注解的使用.
作用在一个配置类中的方法上,可以将方法的返回对象,加载到容器管理成bean.
对象id默认是方法名称.可以使用@Bean注解的属性name自定义id值.
### 2.4.3 @ComponentScan
这个注解的作用是扫描代码包,并且将扫描到spring注解 使其生效. @Configuration @Component @Controller @Service @Repository @Import @Autowired.....
可以提供注解的属性String[] basePackages 扫描不同的包.
xml中 对应的是一个扫描标签
````
```
- 测试案例version3
**第一步**: 准备一个可以被扫描而创建的bean对象Bean02
```java
package com.tarena.spring.boot.bean;
import org.springframework.stereotype.Component;
@Component
public class Bean02 {
public void identity(){
System.out.println("我是bean02");
}
public Bean02() {
System.out.println("当前容器加载了Bean02");
}
}
```
不是使用 xml bean标签创建的容器对象,而是通过扫描.
**第二步**: xml实现包的扫描
```xml
```
**第三步**: 运行 xmlLoad测试方法.
- 测试案例version4
在配置类 只要添加一个@ComponentScan 可以起到和componet-scan标签一样的 甚至更丰富的作用.
**第一步**: 在配置类上添加@ComponentScan
```java
package com.tarena.spring.boot.config;
import com.tarena.spring.boot.bean.Bean01;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 对标 demo01.xml
* 添加一个spring3.0出现的配置注解
* @Configuration
*/
@Configuration
@ComponentScan(basePackages ={"com.tarena.spring.boot.bean"})
public class Demo01Config {
//加载配置类的标志
public Demo01Config() {
System.out.println("当前容器加载了demo01Config");
}
//使用注解@Bean 将方法的返回对象 驻留到容器中 管理
//相当于一个配置xml中bean标签 bean对象id默认就是方法名称
@Bean("bean01")
public Bean01 bean01(){
return new Bean01();
}
}
```
@ComponentScan注解 需要配置扫描范围,如果不指定扫描包,默认扫描当前所在配置类的包路径.
**第二步**: 运行configLoad测试方法
````
### 2.4.4 @Import
场景: 早期xml配置时代,大量的xml都需要加载.如果需要加载一个xml同时 加载其它N的xml配置文件.可以使用导入的功能. \标签实现
对应这种标签导入xml的功能,配置类也可以导入其它配置类.
- 测试案例version5
使用xml 的import标签实现多个xml同时加载.
**第一步**: 准备测试Bean03
```java
package com.tarena.spring.boot.bean;
public class Bean03 {
public void identity(){
System.out.println("我是bean03");
}
public Bean03() {
System.out.println("当前容器加载了Bean03");
}
}
```
**第二步:**单独准备一个demo02.xml配置文件 利用bean标签创建Bean03
```
```
**第三步:**demo01.xml 通过import导入demo02.xml
```
```
可以通过@Import注解实现一个入口配置类 加载导入其它配置的功能.
- 测试案例version6
**第一步**: 新准备一个配置类Demo02Config,在这个配置类里 管理创建Bean03
```java
package com.tarena.spring.boot.config;
import com.tarena.spring.boot.bean.Bean03;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Demo02Config {
@Bean
public Bean03 bean03(){
return new Bean03();
}
}
```
**第二步:** 在Demo01Config进行配置类导入 第一种用法--直接导入配置类
```java
package com.tarena.spring.boot.config;
import com.tarena.spring.boot.bean.Bean01;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan(basePackages ={"com.tarena.spring.boot.bean"})
@Import({Demo02Config.class})
public class Demo01Config {
public Demo01Config() {
System.out.println("当前容器加载了demo01Config");
}
@Bean("bean01")
public Bean01 bean01(){
return new Bean01();
}
}
```
@Import 第二种导入的可以是一个配置类的选择器(Selector).为了应对的场景是同时导入一批配置类.
- 测试案例version7
实现选择器的方法导入
**第一步:** 创建一个选择器,实现一个选择器的接口
```java
package com.tarena.spring.boot.selector;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.util.function.Predicate;
public class DemoImportSelector implements ImportSelector {
/**
* @param metaData 收集所有的实现了某一个固定接口的配置类信息
* @return String[] 包括了你想创建加载的所有配置类全路径名称 数组
*/
@Override
public String[] selectImports(AnnotationMetadata metaData) {
String[] demoConfigs={"com.tarena.spring.boot.config.Demo02Config"};
//如果创建编写的配置类比较多,可以准备外部的配置文件 提供所有类的全路径名称
return demoConfigs;
}
//可以配合 导入选择器的逻辑,如果希望在一堆String[] 元素中
//排除掉某些配置类,可以定义filter通过这个方法 返回
@Override
public Predicate getExclusionFilter() {
return ImportSelector.super.getExclusionFilter();
}
}
```
**第二步:** 修改导入方式(代码略)
### 2.4.5 @ImportResource
虽然springboot 没有xml,但是spring依然允许在一个springboot中存在配置类 也存在额外的xml.
使用这个注解可以导入额外的xml配置. 例如: dubbo使用spring整合的时候 就是通过xml导入生效的.
### 2.4.6 @PropertySource
可以**配合配置类使用**,导入一个外部的properties配置文件,读取key-value数据放到内存使用.
- 测试案例version8(略)
xml中可以配置标签 导入额外的properties数据文件.
```xml
classpath:conn.properties
classpath:bonn.properties
```
可以使用注解完成这个标签相同的功能.
**第一步:**准备jdbc.properties
**第二步:** 使用注解读取文件
在加载的配置类中 使用这个@PropertySource注解
## 2.5 详解Spring Boot条件注解
springboot中扩展了很多条件注解,是springboot能够实现自动配置的核心注解们.
### 2.5.1 介绍条件注解
在Spring4.x版本 出现了一个条件注解的基础注解.@Conditional.
Springboot就是出现条件注解之后 创建的第一个版本. 基于这个条件注解创建了非常多的条件衍生注解.具体的编写了条件规则.
### 2.5.2 @ConditionalOnClass|@ConditionalOnMissingClass
这两个注解的条件逻辑相反,都是类和方法的注解.如果作用在类上,一般也是**配置类**.
这两个注解会根据条件属性,判断某个各,某几个指定的类是否存在于当前依赖环境.
存在或不存在是满足条件的前提,如果满足条件,对应的类或者方法才会选择加载或者不加载. **一个配置类中代码是否需要加载由这些条件注解判断**.
- 测试案例version9
**第一步:** 准备一个新的配置类ConditionalDemo01Config
```java
package com.tarena.spring.boot.config.condition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Configuration;
@Configuration
//指定类 存在 条件才满足 true 满足才加载类
//@ConditionalOnClass(name={"com.tarena.spring.boot.bean.Bean01"})
//指定类 不存在 条件才满足
@ConditionalOnMissingClass(value={"com.tarena.spring.boot.bean.Bean01"})
public class ConditionDemo01Config {
public ConditionDemo01Config() {
System.out.println("条件满足,condition-demo01被容器加载");
}
}
```
**第二步:** 通过Demo01Config入口配置类 扫描了新的配置类所在包

新的条件配置类 是否加载取决于条件逻辑 返回是否是true.
### 2.5.3 @ConditionalOnBean|@CondtionalOnMissingBean
判断当前环境中是否存在指定对象.
onBean 存在这个对象则满足
onMissingBean 不存在这个对象则满足
- 测试案例version10
**第一步:** 准备一个新的条件配置类 ConditionDemo02Config
```java
package com.tarena.spring.boot.config.condition;
import com.tarena.spring.boot.bean.Bean02;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Configuration;
@Configuration
//指定类 bean在容器中存在 则条件满足
//@ConditionalOnBean(value={Bean02.class})
@ConditionalOnMissingBean(value={Bean02.class})
public class ConditionDemo02Config {
public ConditionDemo02Config() {
System.out.println("条件满足,condition-demo02被容器加载");
}
}
```
### 2.5.4 @ConditionalOnProperty
作用: 配合配置类 作用在配置类 或者配置类的方法,决定类或者方法到底加载还是不加载.
判断的逻辑使用的内存中读取到的properties文件值.
注解属性有4个核心值:
1. prefix: 表示key值的前缀
2. value: 拼接prefix 寻找key值的
3. havingValue: 判断依据 是否存在{prefix}.{value}的key值 如果不存在 则条件不满足 如果存在 还要再次判断是否有指定的值,没有则条件不满足
4. matchIfmissing: 如果{prefix}.{value}的key值不存在是否直接判断条件满足还是不满足
- 测试案例version11
准备新的条件配置类ConditionDemo03Config
## 2.6 Spring boot自动配置逻辑
### 2.6.1 springboot的自动配置类
springboot可以**根据当前项目的各种各样的环境**,帮助我们实现自动配置逻辑(约定大于配置).
例如:
当我们依赖了spring-boot-starter-web 自动给我们配置tomcat容器,运行web服务.
当我们依赖了spring-boot-starter-jdbc 自动给我们配置datasource 要求我们提供datasource属性了,如果不提供,创建datasource 自动配置失败.
当我们依赖了spring-boot-starter-redis|spring-boot-starter-data-redis 自动给我们配置RedisTemplate的对象.
问题: **他为什么能做到这个功能,根据我们的需求 实现自动配置.**
直接原因: springboot基于spring的扩展功能在4.X,编写准备了一堆自动配置类,名字**AutoConfiguration. 只要存在spring-boot-starter的依赖资源 一定有这些自动配置类.
1. RedisAutoConfiguration
2. RabbitAutoConfiguration
3. DatasourceAutoConfiguration
4. ...
这种自动配置类 直接生效的时131(2.5.9)个,部分配置类中Import导入功能间接加载还有 千个左右.
问题: **这些配置类131个 怎么加载到我的项目的.**
### 2.6.2 核心注解
每一个启动类 都存在一个核心注解@SpringBootApplication.
Spring4.X 出现了组合注解的功能.SpringBoot基于Spring4.x版本创建的,也存在大量的组合注解
@SpringBootApplication包含了3个核心组合注解
- SpringBootConfiguration: 包装了一下@Configuration 就代表配置类注解
- ComponentScan: 扫描,默认扫描.当前启动类所在的包就是扫描范围.
- EnableAutoConfiguration: **但凡注解叫做Enable**** 一定包含了Import注解
**导入的是一个选择器**,选择器返回值是String[] 数组 包含了springboot给我们开发的所有配置类(131个配置类 和额外的第三方自动配置类)
问题: 131个自动配置类都用么? 到底用哪个配置类怎么判断的?
### 2.6.3 自动配置类条件注解
虽然通过启动类 可以导入众多的自动配置类 ,但是自动配置类最终是否加载,取决于每个配置中配置的条件注解.只有条件满足的配置类才会加载.
如果想要直观看到 哪些配置类 满足条件 哪些配置类不满足条件.
需要配置日志级别为debug. 在日志输出的控制台可以看到2部分内容
**Positive Matched: 满足条件的所有自动配置类和 条件详情**
**Nagative Matched: 不满足条件的所有自动配置类 和条件详情**
#附录
##SpringBoot自动配置的原理是什么?
2023年10月25日 17:47 - 2023年10月25日 17:50
聊到SpringBoot 配置原理 就不得不聊一聊它的核心注解 SpringBootApplication
这个注解是一个组合注解 该注解包括三个重要的注解
第一个注解是SpringBootConfiguration 第二个注解是 ComponentScan 第三个注解是 EnableAutoConfiguration
第一个注解就是对Spring原生的@Configuration 做了一个包装 该注解本身是一个配置类的注解
这个注解使得启动类成为一个配置类 Spring 启动的时候必定会加载配置类 就像原来加载xml一样
第二个注解叫 ComponentScan 它默认值没有添加扫描的包 所以它会对启动类所在的包当作扫描值进行扫描
它的作用是为了我们自己的代码在容器中生效 我们会写一些业务代码 需要它来扫我们的Controller Service Repository
第三个注解是EnableAutoConfiguration 当Spring容器加载启动类的时候 这个EnableAutoConfiguration注解里面会导入一个选择器
而这个选择器里面的String数组的值 就是SpringBoot 当前版本给我们准备的配置类 当然这一百多个配置类导入之后并不是全部都会
加载生效 取决这个配置类的条件注解 @Conditional 这个条件由我们的开发工程师来决定 我们如果要使用redis 就要使用redis 的依赖和属性
只有我们依赖和属性同时满足的时候就可以加载
这个就是SpringBoot 的配置原理
# 1 SPRING BOOT自动配置逻辑
## 1.1 目标
- [x] 通过学习springboot自动配置 **理解**自动配置原理
- [x] 核心注解: 组合注解
- [x] 开启自动配注解: @EnableAutoConfiguration流程
- [x] 实现自定义starter
- [x] 项目名称**-spring-boot-starter
- [x] 根据功能需求编写自动配置类**AutoConfiguration
- [x] 根据META-INF/spring.factories格式 填写这个类名进去
- [x] 第三方其它项目只要依赖当前自动配置资源
## 1.2 spring.factories文件
### 1.2.1 springboot加载自动配置
- 启动类由于@SpringBootApplication组合注解,是一个配置类
- @EnableAutoConfiguration 开启自动配所有**AutoConfiguration加载
- @EnableAutoConfiguration使用的选择器selector导入
- **选择器 筛选所有当前环境的自动配置类 拿到全部全路径名称**
- 自动配置类根据自身的条件,最终是否被springboot中spring容器加载,取决于开发需求.
问题: 选择器配置类全路径名称 是从哪里获取的?
### 1.2.2 SpringFactoriesLoader加载
spring提供了一个加载器,SpringFactoriesLoader.能够固定加载一个当前所有环境中存在的文件叫做spring.factories.
提供自动配置类的依赖资源 必定需要满足springboot的自动加载逻辑.需要提供一个**META-INF**文件夹下的spring.factories的文件,这样利用SpringFactoriesLoader就可以加载这个文件了.从获取到所有自动配置类的全路径名称.
- 测试案例version1
SpringFactoriesLoader加载这个文件的效果.
```java
package com.tarena.spring.boot;
import org.junit.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
public class RunSpringFacotires {
@Test
public void loadSpringFactories(){
/**
* loadFactoryNames有2个参数
* factorType 是一个文件的key值 Class类型反射对象
* classLoader 当前代码类加载器
*/
List stringList = SpringFactoriesLoader.loadFactoryNames(
EnableAutoConfiguration.class,
RunSpringFacotires.class.getClassLoader());
System.out.println(stringList);
}
}
```
### 1.2.3 spring.factories格式
自定一个spring.factories
```
org.springframework.boot.autoconfigure.EnableAutoConfiguration={依赖资源中提供的所有自动配置类}
```
案例:
```txt
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tarena.aa.UserAutoConfiguration,\
com.tarena.bb.TeacherAutoConfiguration
```
- 测试案例version2
在测试代码中 准备一个META-INF/spring.factories文件
按照上述的格式,填写内容.
通过这个案例 检测是否可以将当前环境 所有 spring.factories一起读取
上述代码流程逻辑 能够实现 第三方完成spring-boot-starter 一般这种第三方名称**-spring-boot-starter.
例如: mybatis-spring-boot-starter 一定包含两部分内容 自动配置类 META-INF/spring.factories

### 1.2.4 结论
springboot 自动配置原理流程中 负责导入的选择器 是利用SpringFactories的机制 从当前环境读取到所有依赖资源中 META-INF/spring.factories的文件 并且拿到EnableAutoConfiguration这个key的所有value值(依赖中 提供的所有自动配置类)
## 1.3 实现第三方的starter
### 1.3.1 需求
- 开发一个 luban-spring-boot-starter项目
- 存在一个自动配置类,创建一个User的容器bean对象
- 让依赖luban-spring-boot-starter的其它项目 可以加载这个自动配置类
- 自动配置类开启条件是 必须存在一个属性 luban.user.enable=true的值 值也必须是true 不存在视为条件不满足 自动配置类不加载
- 如果环境中已经存在一个User容器bean对象 则不在创建这个自动配置的bean对象.
### 1.3.2 开发第三方starter
- 整理luban-spring-boot-starter依赖
```xml
org.springframework.boot
spring-boot-starter
junit
junit
test
```
- 创建一个类 LubanUser
```java
package cn.tedu.auto.config.bean;
public class LubanUser {
public LubanUser() {
System.out.println("环境满足条件 LubanUser加载了");
}
}
```
- 创建一个自动配置类
```java
package cn.tedu.auto.config;
import cn.tedu.auto.config.bean.LubanUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
//必须有一个属性 luban.user.enable 值value必须是true
@ConditionalOnProperty(prefix = "luban.user",value="enable",havingValue = "true")
public class LubanUserAutoConfiguration {
@Bean(name="lubanUser")
//如果使用自动配置的一方 自行配置了LubanUser 这个条件不满足
@ConditionalOnMissingBean(LubanUser.class)
public LubanUser initLubanUser(){
return new LubanUser();
}
}
```
- 准备一个META-INF/spring.factories

文件夹 META-INF
文件名 spring.factories
文本格式 参考上图
### 1.3.3 验证一下
通过在日志中 是否查看到LubanUser的构造方法打桩,作为判断条件
- 只做依赖
springboot读取自动配置类流程 会法相LubanUserAutoConfiguration 但是条件不满足
类的条件ConditionalOnProperty.
- 配置属性
```yaml
luban:
user:
enable: true
```
LubanUser加载创建打印 构造方法的打桩语句
- stock自己创建一个LubanUser对象
```java
package com.tarena.demo.luban.all.main.config;
import cn.tedu.auto.config.bean.LubanUser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LocalLubanUserConfiguration {
@Bean
public LubanUser lubanUser(){
return new LubanUser();
}
}
```
导致 自动配置类满足条件 因为配置了开启属性enable
但是自动配置类的方法initLubanUser 不满足条件 不在创建LubanUser.
# 2 Spring cloud gateway
## 2.1 阶段性架构

nacos管理治理集群实例
dubbo负责集群内部的相互调用
问题: 外部调用怎么进入?

## 2.2 网关介绍
网关作为微服务集群**唯一**的对外入口,可以实现很多功能.
例如:
统一的解决**跨域(一个ajax请求 origin域名和请求目标url中域名不同,ip不同,域名不同,端口不同,都会引发的问题)**问题.
统一的身份认证.认证解析JWT.
如果网关能够结合一些其它的插件 可以实现入口流量控制(sentinel).做到监控预警(Prometheus)等
网关的落地方案(Spring cloud gateway),还有其它选择
1. netflix zuul: 原来比较火的一个网关组件,但是netflix奈飞停更了,zuul2流产了.
2. Kong: 也是一种选择
3. Nginx: 反向代理网关,并发能力,速度都可以保证. 一般要配合网关组件实现更前端的入口控制
spring cloud gateway 实现的具体功能:
- 请求转发: 网关不具备处理某个请求的能力,所以一定要转发给微服务.
- **路由**匹配: (网络路由表示 端到端的路径网络选择)根据请求条件(包括**地址**,**头**,**)**,功能判断具体由哪个服务来完成这次处理.
- 过滤器(拦截功能): 可以在过滤逻辑中,对请求做自定义的逻辑处理(比如验证身份.记录请求次数,记录并发)
## 2.3 网关入门案例
### 2.3.1 需求描述
- 使用网关接收请求,但是不处理请求,转发给后端服务
- 使用stock作为转发测试的目标实例.
### 2.3.2 实现网关转发案例
- 单独创建一个网关工程-luban-demo-gateway

- 提供项目的依赖
所有依赖包括4个.
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-loadbalancer
```
### 2.3.3 准备springboot启动类
```java
package com.tarena.luban.demo.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
@SpringBootConfiguration
public class GatewayMain {
public static void main(String[] args) {
SpringApplication.run(GatewayMain.class,args);
}
}
```
### 2.3.4 配置入门案例的application.yml
- 指定一个端口
- 指定一个spring应用应用名称
- **切换一下web应用底层容器类型**
- 微服务集群网关属性
```yaml
server:
port: 30000
spring:
application:
name: luban-demo-gateway
#切换一下底层web容器类型
main:
web-application-type: reactive
#配置网关 完成入门案例的需求
cloud:
gateway:
#网关的路由们
routes:
# 当前路由对象的id值
- id: demo01
# 网关转发的目标
uri: http://localhost:10000
# 转发判断路由的条件(判断断言)
predicates:
#Path断言 进如网关 匹配请求的path路径
- Path=/**
```
- 访问测试
| 请求测试网关 url | 响应结果 |
| ------------------------------- | ----------- |
| http://localhost:30000/doc.html | 获取doc页面 |
| http://localhost:30000/abc | 404没有资源 |
### 2.3.5 解析请求流程(重点)
重点关注 在网关中都经历了哪些环节.
- url结构

1. 访问协议
2. 域名和端口
3. url路径中path **value=/abc**
4. query字符串
- 请求进入到网关
只要域名和端口 对应的是访问网关的进程,请求一定能进入网关.
- 网关逻辑
- 判断 url中path在 网关中是否可以通过断言匹配至少一个路由
- 判断依据 /abc是否 满足 /**的范围 满足
- 找到了一个路由 id=demo01
- 经过网关内部过滤器 计算 最终 通过路由找到uri=http://localhost:10000
- 拼接请求进入的path(/abc)到uri(http://localhost:10000)
- 结果是http://localhost:10000/abc
- 网关调用代码发送请求http://localhost:10000/abc
- stock接收到请求 返回404 原路返回
http://localhost:30000/doc.html
- url
- path=/doc.html
- 请求进入网关
- 使用断言匹配这个请求 Path=/**
- 可以匹配 /doc.html
- 找到路由id demo01
- 找到uri目标地址
- uri和请求的path拼接 http://localhost:10000/doc.html
- stock接收到网关转发调用过来的请求
- 响应原路返回
### 2.3.6 web ant匹配规范
主要路径 url 地址 path匹配使用到的三种通用符号
| 规则符号 | 说明 | 案例 |
| -------- | ------------------ | ------------------------------------------ |
| ? | 匹配任意一个字符 | Path=/?, 可以匹配 /a /b,不可以匹配/ab /a/b |
| * | 匹配任意一个字符串 | Path=/*,可以匹配/a /ab,不可以匹配/a/b |
| ** | 匹配任意多级字符串 | Path=/**, |
组合ant规范 Path断言可以有很多种写法
- Path=/a/*/abc: path请求路径以a开始 中间任意字符串 以abc结束
- [x] /a/b/abc
- [x] /a/abc/abc
- [x] /a/abc
- [ ] /a/abc/abc/abc
- Path=/a/**/abc:
- [x] /a/b/abc
- [x] /a/abc/abc/abc
- Path=/a/?/abc
- [x] /a/b/abc
- [ ] /a/abc/abc
- [ ] /a/abc
### 2.3.7 Host断言
需求: 启动后端2个服务实例,通过网关入门案例配置的路由规则,实现2个路由的转发,转发目标不同
stock cart
- version1 使用path
```yaml
server:
port: 30000
spring:
application:
name: luban-demo-gateway
#切换一下底层web容器类型
main:
web-application-type: reactive
#配置网关 完成入门案例的需求
cloud:
gateway:
#网关的路由们
routes:
# 当前路由对象的id值
- id: demo01
# 网关转发的目标
uri: http://localhost:10000
# 转发判断路由的条件(判断断言)
predicates:
#Path断言 进如网关 匹配请求的path路径
- Path=/stock/**
- id: demo02
uri: http://localhost:11100
predicates:
- Path=/cart/**
```
测试一下请求url 观察结果:
http://localhost:30000/stock/doc.html
- [x] 404 后端服务报的
- [ ] 404 网关报的
- [ ] 看到资源页面
http://localhost:30000/cart/doc.html
- [x] 404 后端服务报的
- [ ] 404 网关报的
- [ ] 看到资源页面
http://localhost:30000/doc.html
- [ ] 404 后端服务报的
- [x] 404 网关报的
- [ ] 看到资源页面
http://localhost:30000/stock/doc.html
- url中path=/stock/doc.html
- 网关接收到请求
- 使用断言匹配寻找 网关的路由
- 找到Path=/stock/** 对应路由 demo01
- 找到uri http://localhost:10000
- 拼接uri 和请求path
- 调用转发
- stock 接收到 http://localhost:10000/stock/doc.html
- stock后端服务没有这个资源 返回404
http://localhost:30000/doc.html
- url中path /doc.html
- 进入到网关了
- 使用断言匹配寻找 网关的路由
- 找不到 网关直接返回404
**请求在网关中找不到对应匹配断言路由时,网关直接返回404**
上述案例完成后 Path断言规则基本就掌握了,但是并不符合当前情况.
如果使用Path匹配的规则配置 没有符合当前应用场景,断言不只有一种断言规则.所有的断言都在匹配Request中的数据.
- version2 添加Host断言
Host 匹配的是请求中的Host头.
```yaml
server:
port: 30000
spring:
application:
name: luban-demo-gateway
#切换一下底层web容器类型
main:
web-application-type: reactive
#配置网关 完成入门案例的需求
cloud:
gateway:
#网关的路由们
routes:
# 当前路由对象的id值
- id: demo01
# 网关转发的目标
uri: http://localhost:10000
# 转发判断路由的条件(判断断言)
predicates:
#Path断言 进如网关 匹配请求的path路径
- Path=/**
- Host=127.0.0.1:30000
- id: demo02
uri: http://localhost:11100
predicates:
- Path=/**
- Host=localhost:30000
```
http://localhost:30000/stock/doc.html
后端404
http://localhost:30000/cart/doc.html
后端404
http://localhost:30000/doc.html
找到资源
http://127.0.0.1:30000/stock/doc.html
后端404
http://127.0.0.1:30000/cart/doc.html
后端404
http://127.0.0.1:30000/doc.html
找到资源
- 分析其中一个请求流程
http://localhost:30000/stock/doc.html
1. Path=/stock/doc.html Host头=localhost:30000
2. 进入到网关
- 使用断言Path=/** Host=127.0.0.1:30000 | **localhost:30000**
- 找到了路由 demo02
- 找到uri http://localhost:11100
- 拼接path http://localhost:11100/stock/doc.html
3. cart服务实例 处理请求
4. 返回 404
## 2.4 网关路由断言规则
断言 predicates: 所有路由配置必备的一个属性.
作用: 实现一个进入网关请求的mapping映射计算的.
断言的方式,计算的功能,除了Path以外,有很多种.
多种断言可以独立配置,也可以组合使用. 基本所有的断言都匹配的是**请求**
### 2.4.1 After
官方案例
```yaml
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After = 2017-01-20T17:42:47.789-07:00[America/Denver]
```
所有请求进入网关满足After的判断的,都会匹配这个叫做after_route的路由.
在某个具体的时间点之后的请求.
### 2.4.2 Before
```yaml
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
```
所有请求在这个时间点之前的,都满足匹配这个路由.
### 2.4.3 Between
使用after+before也能实现between的功能.
```yaml
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-21T17:42:47.789-07:00[America/Denver]
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
```
```yaml
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
```
在两个时间点之间的请求满足断言匹配当前路由
以上的3断言和访问时间有关.
### 2.4.4 Cookie
```yaml
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, \\d+
```
要求请求必须携带一个cookie值,名字叫chocolate ,value值 必须满足 //d+正则表达
式. 如果断言的的值不是正则,就是一个固定值,就是判断相等.
### 2.4.5 Header
```yaml
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \\d+
```
匹配请求必须携带一个头 ,名字为X-Request-Id,头的值满足正则 \d+(一个或者多个数字);
### 2.4.6 Query
http://localhost:10000/abc?name
```yaml
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=green
```
请求中携带了green的query条件则满足 断言
```yaml
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=color,red
- Query=age,\\d+
```
请求中携带color并且值是red 携带age 并且值是多个数字 则满足断言
### 2.4.7 Host
```yaml
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org
```
请求Host头,如果是 www.somehost.org; order.somehost.org; cart.somehost.org;
主域名(一级域名): jd.com;taobao.com
二级域名: **passport**.jd.com;**login**.taobao.com
三级域名:**aa**.passport.jd.com;**bb**.passport.jd.com
### 2.4.8 Method
```yaml
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST
```
请求方式必须是GET或者POST其中一个,就能满足这个断言要求.
### 2.4.9 Path
```yaml
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/*,/blue/{segment}
```
请求的路径 满足/red/{任意},或者 /blue/{任意}
### 2.4.10 Remote
```yaml
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
```
在24个字节范围之内,子网掩码(255.255.255.0). 网络地址在此网段满足 断言的
- [x] 192.168.1.10
- [x] 192.168.1.11
- [ ] 192.168.2.10 不匹配
### 2.4.11 Weight
权重断言
```yaml
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Path=/**
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Path=/**
- Weight=group1, 2
```
分散的转发了 两个路由(路由匹配规则相同),一批高并发请求,会按照8:2的比例
转发调用到后端不同的服务,实例.
同一个weight分组的断言规则必定完全相同。
### 2.4.12(4.0.x) 新的断言
```yaml
spring:
cloud:
gateway:
routes:
- id: xforwarded_remoteaddr_route
uri: https://example.org
predicates:
- XForwardedRemoteAddr=192.168.1.1/24
```
具体含义 ,可以自行扩展。
## 2.5 断言在微服务应用场景
用的最多的微服务场景下 使用Path 或者Host
- Path
前后端 网关配置要约定好所有的规则。
```yaml
routes:
- id: stock
uri: stock.com
predicates:
- Path=/stock/**
- id: cart
uri: cart.com
predicates:
- Path=/cart/**
- id: order
uri: order.com
predicates:
- Path=/order/**
```
- Host
有的微服务集群每一个服务都独立注册使用了域名
cart: cart.luban.com
order: order.luban.com
stock: stock.luban.com
```yaml
routes:
- id: stock
uri: stock.com
predicates:
- Host=stock.luban.com
- id: cart
uri: cart.com
predicates:
- Host=cart.luban.com
- id: order
uri: order.com
predicates:
- Host=order.luban.com
```
## 2.6 网关运行原理

1. 请求进入网关
2. 映射器,通过请求各种条件 计算匹配路由配置的断言 找到路由 如果映射器找不到处理的路由 直接返回404.
3. 找到路由之后,会交给handler处理器 传给后面的过滤链。
4. 过滤链既包含网关内置的过滤器 也可以自定义添加**过滤器**.
5. 最终通过过滤器的各种计算 将访问后端的结果交给一个代理
6. proxy代理发送调用后端服务响应返回
问题: 有匹配映射,有路由过滤器,其它过滤器有哪些?
http://localhost:30000/stock/doc.html

拼接path和路由中uri地址之前 有过滤逻辑 自行更改了- StripPrefix=1
path路径 前1个路径值 删除 /stock/doc.html --> doc.html 在去拼接
http://localhost:10000/doc.html
和断言 同理, 内置过滤器可以在yaml中配置过滤器规则.从官网中 可以查看过滤器的所有详细案例.
# 附录
# 1 跨域的概念
动态数据是通过前端页面中 ajax访问的,ajax发起访问的域名 和访问的目标域名不同 就会出现跨域问题(CORS).
| origin原域名 | target目标域名 | 是否跨域 |
| ------------- | --------------- | -------- |
| www.jd.com | cart.jd.com | true |
| www.jd.com:80 | www.jd.com:8080 | true |
| www.id.com | 60.217.250.3 | true |
| www.jd.com | www.jd.com | false |
只要不一样 域名 ip 端口 就形成了跨域
# 1 GATEWAY
## 1.1 gateway负载均衡原理图

整个流程原理和 单纯的路由转发 区别在于:
1. 网关需要整合nacos客户端读取注册信息
2. 网关需要引用负载均衡计算器,在proxy发送请求之前计算具体的逻辑.
## 1.2 实现网关负载均衡
### 1.2.1 引用依赖
- nacos-discovery
- loadbalancer
### 1.2.2 修改yaml配置文件
- 体现连接nacos客户端
- 修改路由引用负载均衡计算器的内容
```yaml
server:
port: 30000
spring:
application:
name: luban-demo-gateway
#切换一下底层web容器类型
main:
web-application-type: reactive
#配置网关 完成入门案例的需求
cloud:
nacos:
discovery:
server-addr: localhost:8848
#namespace: 123456
#group: 1.0.0
gateway:
#网关的路由们
routes:
# 当前路由对象的id值
- id: demo01
# 网关转发的目标 如果需要使用服务集群实例 负载均衡
uri: lb://luban-demo-stock
# 转发判断路由的条件(判断断言)
predicates:
#Path断言 进如网关 匹配请求的path路径
- Path=/**
- Host=127.0.0.1:30000
- id: demo02
uri: lb://luban-demo-cart
# http://localhost:30000/doc.html lb://luban-demo-cart/doc.html localhost:11100/doc.html
predicates:
- Path=/**
- Host=localhost:30000
```
### 1.2.3 访问测试梳理流程
http://localhost:30000/doc.html
- 进入网关
- 寻找匹配路由 path=/doc.html Host=localhost:30000
- 找到demo02
- path拼接 uri形成 新的url **lb://luban-demo-cart/doc.html**
- 负载均衡器 解析服务名称 luban-demo-cart 从注册信息计算负载均衡 127.0.0.1:11100
- 重新替换url 127.0.0.1:11100/doc.html
http://127.0.0.1:11100/doc.html
### 1.2.4 问题提前看
- 路由配置断言不正确 导致请求没有匹配到路由
- 网关返回 404
- 网关报500 服务名称不存在
- 可以找到路由
- 但是lb负载均衡无法获取 服务信息
- 后端异常(问题出现的位置 不一定是网关)
- 网关路由 正确的
- 负载均衡 正确的
- 向后调用 正确的
- 后端出现异常
## 1.3 跨域配置
### 1.3.1 跨域问题介绍
跨域(Cross-Origin Resource sharing)
通过前端请求ajax,发起向后的访问,如果访问的origin原地址和目标地址
**域名** **ip** **端口** **协议** 有一个不一样的,就会形成跨域,默认web应用(springmvc spring)需要后端处理跨域问题的.
### 1.3.2 网关配置跨域
跨域的配置 一共就5个属性:
mapping: 匹配请求路径 ,匹配到的来按照定义的跨域实现访问,没匹配到的,不设置
- allowedOriginPatterns: 允许的origin原域值 "*" "www.jd.com" "**.jd.com"
- allowedHeaders: 跨域请求能够携带的头. "*" "X-REQEUST-A"
- allowedMethod: 跨域请求允许的方式 "*" "POST" "GET"
- allowCredentials: 跨域请求是否允许一些特殊携带的cookie值 设计安全 身份验证携带 true|false
- maxAge: 第一次允许的跨域请求对应的客户端 会保存一个有效时间,在有效时间之内,后续不需要再判断跨域条件.时间一旦超时,重新计算跨域条件 单位是秒
网关只需要按照需求 配置yaml属性,就能实现跨域.
```yaml
server:
port: 30000
spring:
application:
name: luban-demo-gateway
#切换一下底层web容器类型
main:
web-application-type: reactive
#配置网关 完成入门案例的需求
cloud:
nacos:
discovery:
server-addr: localhost:8848
#namespace: 123456
#group: 1.0.0
gateway:
#跨域配置
globalcors:
cors-configurations:
#对应代码配置类的 mapping
"[/**]":
#允许origin来源
allowed-origin-patterns: "*"
#- "**.jd.com"
#- "**.taobao.com"
#允许跨域访问的头 不允许的会被清空删除
allowed-headers: "*"
#- "Accept"
#- "Authorization"
allowed-methods: "*"
#- "GET"
#- "POST"
allow-credentials: true
#允许跨域之后 最长客户端保存的状态时间
max-age: 7200
#网关的路由们
routes:
# 当前路由对象的id值
- id: demo01
# 网关转发的目标 如果需要使用服务集群实例 负载均衡
uri: lb://luban-demo-stock
# 转发判断路由的条件(判断断言)
predicates:
#Path断言 进如网关 匹配请求的path路径
- Path=/**
- Host=127.0.0.1:30000
- id: demo02
uri: lb://luban-demo-cart
# http://localhost:30000/doc.html lb://luban-demo-cart/doc.html localhost:11100/doc.html
predicates:
- Path=/**
- Host=localhost:30000
```
## 1.4 网关过滤器
网关可以实现容易入口.可以做**身份认证 监控**等. 身份认证 监控等功能就需要使用网关的过滤器
### 1.4.1 过滤器介绍
每一个web应用,都存在**过滤器**和**拦截器**.
**过滤器**: 能实现过滤的计算,在请求进入到资源方法之前 对请求做处理的过程.
例如: 过滤器获取请求的参数 header头等信息 做身份认证.
**拦截器**: 拦截器一般都会结合过滤器 在过滤器中左的计算 向后传递一个标签|标志,在拦截器通过判断决定是否放行.
security安全框架 典型的过滤器和拦截器应用框架.
过滤器特点:
- 过滤器各司其职
- 顺序执行需要符合业务逻辑(粗粒度过滤在前 细粒度过滤在后)
- 特殊的情况,过滤器可以实现拦截的效果(操作响应返回)
### 1.4.2 过滤器入门案例
目标: 让网关中 一个过滤器代码生效,所有请求都会进入这个过滤器.
- 创建过滤器类,并且实现网关固定接口
- 让过滤器类 成为spring容器bean对象
当前案例过滤器的执行位置

```java
package com.tarena.luban.demo.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyFilter01 implements GlobalFilter {
//过滤器核心方法
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//打桩 表明过滤器生效了
System.out.println("myfilter01 生效了");
//让请求 在过滤链中继续向后执行
Mono mono = chain.filter(exchange);
return mono;
}
}
```
### 1.4.3 过滤器请求对象
在过滤器中 拿到请求对象 并且掌握部分相关的api方法.
```java
package com.tarena.luban.demo.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
@Component
public class MyFilter01 implements GlobalFilter {
//过滤器核心方法
/**
* @param exchange webflux底层封装的一个reactive容器对象 包含了请求和响应
* @param chain 过滤链,贯穿了web容器的一个过滤器集合的.从一个过滤器传入到另一个过滤器
* @return webflux框架的一个返回结果.
*/
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//拿到请求对象
ServerHttpRequest request = exchange.getRequest();
//获取相关请求的所有数据
//1.请求路径 第一种 uri拼接的路径 第二种 path
URI uri = request.getURI();
RequestPath path = request.getPath();
System.out.println("当前请求uri:"+uri);
System.out.println("当前请求path:"+path);
//2.请求方法 GET POST PUT DELETE OPTION HEAD
HttpMethod method = request.getMethod();
System.out.println("当前请求method:"+method);
//3.请求参数 localhost:30000/doc.html?name=王翠花&name=刘首付&age=18
//name={"王翠花","刘首付"} age={"18"}
MultiValueMap queryParams = request.getQueryParams();
List values = queryParams.get("name");
System.out.println("当前参数name的值:"+values);
//4.请求头获取
HttpHeaders headers = request.getHeaders();
List headerValues = headers.get("user-agent");
System.out.println("当前user-agent头的size:"+headerValues.size()+"值:"+headerValues);
//5.其它 都是通过get方法获取
InetSocketAddress remoteAddress = request.getRemoteAddress();//客户端ip地址
System.out.println("当前请求客户端ip:"+remoteAddress.getAddress());
System.out.println("当前请求客户端ip:"+remoteAddress.getHostName());
Mono mono = chain.filter(exchange);
return mono;
}
}
```
请求测试url:
http://localhost:30000/abc?name=王翠花&name=刘首付
### 1.4.4 过滤器响应对象
在网关通过过滤器拿到响应对象,然后进行响应直接输出,起到一个拦截的作用.
### 1.4.5 过滤器其它问题(例如顺序问题)
### 1.4.6 课堂练习
题目1:
需求: 模拟 JWT 网关**解析**. 解析如果不成功(签名错误,时间超时,格式错误) 过滤器返回一个认证失败的信息.
- 可以使用请求参数 模拟jwt 例如: localhost:30000/abc?jwt=123
- 利用过滤器 请求对象获取这个参数
- 如果参数jwt为空 或者 值为空(JWT解析失败)
- 解析失败的时候 请求不会向后流转 使用响应对象 返回一个自定义的json文本 明确提示jwt解析失败
- 如果正确携带了jwt,允许请求继续向后流转.
题目2(拔高扩展):
需求: 参考之前cart集群 服务配置 实现**cart**的负载均衡案例.
多次访问一个网关接口,响应结果可以明确观察到到底是哪个后端cart实例返回的 http://localhost:30000/abc

# JSD2305-DAY03-NIGHT
# Redis面试题串讲
## Redis持久化
### 相关引出的面试题
- redis用来作什么了/怎么用(缓存,存储要求写速度比较快的数据库)
- redis为什么快(1. 数据结构简单 空间复杂度低 key-value 2. 内存读写 3. redis线程 IO多路复用)
- 缓存丢失,缓存稳定性如何保证,雪崩(数据可靠性,redis持久化)
- redis 持久化需要不需要怎么做等.
### 由redis持久化引出的各种面试问题
- 问题1:Redis支持哪些持久化方式,并分别介绍它们的优缺点是什么?
- rdb: 快照持久化
- aof: 命令日志持久化
- rdb优点: 恢复速度快,可以支持文件迁移备份.
- rdb缺点: 持久化间隔有可能无法把控,导致 断点宕机 丢失数据量比较大
- aof优点: aof记录所有命令. 恢复实时数据的能力比较强
- aof缺点: 记录详尽命令,恢复数据速度慢
- 问题2:在Redis的RDB(Redis Database)持久化中,如何设置触发RDB快照的条件?
- 自动触发(趋势: 变动越频繁,触发时间间隔越短)
- 人为触发: save bgsave
- save 同步 redis服务端不会在同步持久化时接收客户端其它命令造成阻塞
- bgsave 异步 不会影响 接收其它命令(会不会新的写操作 影响后台的持久化)
- flushall 存储空持久化数据
- 主从复制时
- 问题3:Redis的持久化模式分为哪两种方式,并分别介绍它们的工作原理?
- rdb: fork 子进程 写入临时持久化rbd文件,替换旧文件. 父进程依然接收客户端命令,如果客户端修改了子进程即将写入临时文件的数据,写时复制策略(操作系统提供的)会复制一份数据,保证子进程持久化的是fork那一刻的数据集.
提示: 写时复制策略,产生一个新问题,子进程持久化,客户端大量的覆盖内存数据,操作系统将数据复制一份,内存数据量一旦超过上限,如何处理(开启磁盘缓冲).
- aof: 纯文本日志记录.所有客户端操作的命令,实时性比较强. 内容会在redis中进行重写
- 问题4:在Redis中同时启用RDB和AOF持久化有何优缺点?
- 加载顺序: 先加载rdb 后加载aof(缺点 aof中命令会讲rdb数据覆盖,redis性能会收到影响)
- 企业中保证数据可靠性,稳定性,都这么干. 虽然同时开启,但是影响可接受范围内
- 问题5:在Redis的AOF持久化中,如何处理AOF文件过大的问题?
- aof不管他,一定越来越大. 最重要的原因是内容重复冗余
- 可以配置redis重写aof机制
- 问题6:Redis持久化RDB方式中的手动触发和自动触发的区别是什么?
- save bgsave
- save 同步 redis服务端不会在同步持久化时接收客户端其它命令造成阻塞
- bgsave 异步 不会影响 接收其它命令(会不会新的写操作 影响后台的持久化)
- 配置触发的逻辑
```xml
save 900 1
save 300 10
save 60 10000
```
无论如何触发持久化,存储过程 流程没有变化(fork函数过程)
# 1 阶段性回顾
## 1.1 架构

## 1.2 网关回顾
### 1.2.1 网关作用
- 微服务集群统一入口
- 可以实现路由 监控 流量控制等
### 1.2.2 网关实现功能
- 请求转发: 固定访问后端一个服务实例
- 路由负载均衡:结合nacos客户端抓取的注册信息,实现负载均衡
- 过滤器: 网关有内置过滤器(通过配置yaml属性影响使用的内置过滤器). 自定义过滤器.
### 1.2.3 尚存的微服务架构问题
- 流量控制:
- 外部请求:
- 内部请求:
- 调用失败处理:
# 2 Sentinel熔断限流
## 2.1 熔断/限流/降级
### 2.1.1 熔断
微服务架构中,切分的服务越多,调用关系就越复杂.
如果在多个服务调用过程中,由于某个服务的实例故障,导致调用失败,延迟,等待,重试.
**需要不需要立刻解决这种调用失败的问题,需不需要对故障的实例做额外的处理**
如果不能及时处理这个问题,最终故障的服务实例也会被nacos剔除,或者记录健康状态.总是存在这种状态达到一段时间.这段时间 就有可能导致A服务中所有实例,访问B服务这个故障节点等待排队.访问的压力 调用的压力就积压在这里.积累到一定程度 A服务也有不可访问不可使用的风险.
这种问题 采用一种新的机制来解决--**熔断**
**熔断思想**: 牺牲局部 保存全局 (保险丝)
在熔断过程中,微服务中的落地实现方案,一般是需要引入熔断组件(sentinel).
熔断组件实现牺牲局部保存全局的熔断逻辑 sentinel 中存在一个断路器
断路器 会根据调用的资源的反馈 决定断路器中的状态: **闭合 断开 半开(有些类似开关)**
- 闭合状态 正常调用
- 打开状态: 根据设定的判断条件 打开断路器因为 异常 调用慢等原因.一旦打开 代码将不会在调用这个资源
- 半开: 当打开状态 持续了一段时间,要断路器半开连接故障资源,进行一次检测.
访问检测成功,半开的状态 切换到闭合,后续继续正常调用.
访问检测失败,半开的状态 切换会打开,禁止调用.
在业务访问,代码调用过程中,并不是所有的出问题的调用资源 都需要进行熔断. 要保证这个系统基本可用性.
**通过熔断可以保证系统 核心功能是基本可用的**.
例如: 抖音刷视频.核心数据是视频播放流畅. 评价数据可有可无.
### 2.1.2 限流
重点内容:
- [ ] 理解限流的概念和作用
- [ ] 掌握一些流量,压测数据指标的概念,和公式.避免面试时出现不合理的回答
- 前提:
服务器运行,每个服务器无论硬件还是软件都存在承受访问**并发的上限**.
- 目的:
限流的目的,防止服务接收超过上限的**请求**,导致崩溃.
**介绍一些基础的概念**: 压测数据指标,流量.
1. RT: reaction time 表示一次请求服务器到响应回到客户端的时长(服务实例的计算时间 和网络往返时间);
给一些常规的数值.
访问一个由redis读写功能的接口 rt时间,大概10ms-20ms.
访问一个由mysql数据库读写功能的 rt 30ms-70ms
访问一个包含远程调用的读写功能接口 同步调用还是异步调用. 一般公司要求 每一个写的接口不特殊的话 50ms-100ms,如果是读的接口 20ms-40ms.超过这些数值,接口不合格
简单估算一下: 当前下单案例的RT时间.
远程调用stock减库存 75ms,本地生成订单50ms 75ms 当前下单案例RT时间200ms.
2. QPS: queries per second 直译 每秒查询次数.也表示单个接口的每秒中请求数量.
3. TPS: transaction per second 直译 每秒的事务次数. 相对于QPS变得复杂了,因为很多接口一次请求进入服务器内部,并不只有一个接口功能. 一个对外的功能所包含的多个QPS 统计到一起 就是TPS.相对来讲TPS数值 比QPS小一些.

4. 并发: 服务器同时存在的请求数量. 同时存在处理的请求数量 就分配类相同数量的线程. 比如一个tomcat一般支持100并发.
压测时候 压测的数据是否准确 是否合理,不能完全依赖技术. 也许要人为验证.
每秒查询次数= 并发/一次请求时长
**公式1**: **QPS=并发/RT时间.**
例子1: 有个电商接口压测, 启动10个tomcat集群,接口的平均响应时间RT 20ms,最终QPS 10万QPS.这套数据合理么?
QPS=并发/RT时间(1000/20ms=5万)
10*10/20ms
例子2: 接口平均响应时间 20ms **估算**出来的用户量 QPS达到 1000,请问我需要多个少个tomcat搭建集群.
RT 20ms QPS 每秒查询次数 1000 1000/20/100=
QPS=并发/RT时间 并发=QPS*RT=1000*0.02=20 需要 tomcat一个足够
QPS=10万 tomcat需要20个的集群.
1000* 20 并发 一次请求时长 0.02 10万*0.02 /100
5. PV: page view 一个页面的请求. 一个接口的一次请求 就是一次pv. 一般我们所说的pv都指的是日pv量.
**公式2:** 使用日pv量可以计算系统的并发**QPS峰值**. **80%\*日pv量/20%\*一天**
因为存在非常多的参考数据,比如 A系统用户量50万,日活用户10万.每日下单 10万张订单.
架构师可以根据大量这种数据参考和经验累积. 估算出即将上线的系统的用户量和 pv量.
然后利用公式2计算QPS峰值 然后利用公式1估算出所申请的服务器资源.
例子1: 有一个系统 根据核心接口 下单操作 申请服务器资源. 架构师预估日pv量10个亿.请问这个架构师应该向公司申请多少个正常功能的tomcat(100并发)服务器运行集群?
- 计算服务器tomcat个数 就要知道总并发数
- QPS 和 RT时间(200ms)
- QPS峰值=(80%\*日pv量)/(20%\*一天)=46296
- 并发总数=QPS*RT=9259
- 总tomcat集群个数100个
上述案例完成后,可以实现 使用已知 **RT时间**(写代码的人估算的,或者压测的),一致**tomcat单台并发**(100),**日pv量**(架构师根据系统上线之前 估算用户量 日活用户量得到的结果)
**10亿的日pv量 100tomcat并发 200msRT时间 QPS峰值=46296 tomcat个数 100个**
例子2:
RT时间100ms--> tomcat集群个数 减半50个
日PV量 估算5个亿--> tomcat集群个数 减半50个
- 总结限流的概念
1. RT响应时间是可以获知的
2. 日pv量可以估算
3. tomcat并发是固定的
4. 使用以上3个已知条件 通过2个公式计算出峰值QPS 从而得到申请tomcat集群资源的个数.
### 2.1.3 降级
访问资源无论是熔断 还是 限流,总要有一个可以使用的数据返回.
**思路**: 在出现熔断(断路器打开),限流的情况前提之下,退而求其次的获取可用数据.
## 2.2 Sentinel组件使用
### 2.2.1 介绍
官网: http://sentinelguard.io/zh-cn/docs/introduction.html(官网文档比较乱)
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的**流量治理组件**,主要以流量为切入点,从流量路由、**流量控制**、流量整形、**熔断降级**、系统自适应过载保护、热点流量防护等多个维度来帮助开发者**保障微服务的稳定性**。
### 2.2.2 setinel核心概念
以下2个概念和sentinel工作原理有关.
**资源:** setinel中 可以把任意的代码 封装组织成sentinel的资源,进行保护和限制.
**规则:** 对于某一个资源 或者某几个资源 sentinel如何进行保护和限制,是通过加载和计算规则生效的,如果资源不绑定规则,sentinel将会对资源的控制无视.
围绕资源的实时状态设定的规则,可以包括**流量控制规则**、**熔断降级规则**以及系统保护规则。所有规则可以动态实时调整.
### 2.2.3 准备一个测试工程
- 创建一个项目工程(父工程)
luban-sentinel-study
- 添加依赖资源
luban-sentinel-study添加依赖 主要是 让子工程同一使用.
```xml
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
```
- 准备demo子项目
sentinel使用的方式 有区别.通过不同的demo 逐步从资源 开始 到规则进行学习和编码.
- 准备代码的基础案例
HelloController-->HelloService
接口文件:
| 要素 | 值 | 备注 |
| -------- | ----------- | ------------------------------ |
| 请求地址 | /hello | |
| 请求方式 | GET | |
| 请求参数 | String name | |
| 返回数据 | String | 业务层使用name参数拼接的一句话 |
### 2.2.4 定义资源|定义资源入口
对资源定义之后 ,实现资源的限流.
将controller中调用 service的方法sayHi看成是资源.
```java
package com.tarena.sentinel.study.demo01.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.tarena.sentinel.study.demo01.service.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class HelloController {
@Autowired
private HelloService helloService;
/**
* 接收测试请求的接口功能
* http://localhost:8080/hello?name=王翠花
* hello sentinel i am 王翠花
*/
@GetMapping("/hello")
public String sayHi(String name){
//准备一个sentinel资源入口 Entry
Entry entry=null;
String result=null;
//对entry赋值,赋值过程会进入到sentinel系统架构计算
//提供一个资源名称
try{
entry= SphU.entry("sayHi");
result=helloService.sayHi(name);
}catch (BlockException e){
//entry方法抛出异常 表示入口生成失败,失败原因是sentinel拦截了
log.info("当前资源sayHi受到了限制",e);
}finally {
//释放entry资源
if (entry!=null){
entry.exit();
}
}
return result;
}
}
```
### 2.2.5 定义规则
已经定义的资源 名字 sayHi,没有任何规则限制他.
所以需要一个规则---流控规则.
需求逻辑: 访问这个/hello接口 qps(每秒查询的次数)不能超过1. 如果超过QPS=1的限制,sentinel就拒绝访问 entry赋值失败 抛出一个BlockException.
明确2个目标:
1. 如何生成sentinel的规则数据(多种方式).
这里在启动类中 直接加载规则代码.
2. 如何填写规则数据的内容才能满足不同需求.
按照需求设置 qps不能超过1的限流规则,限制sayHi资源.
修改启动类代码.
```java
package com.tarena.sentinel.study.demo01;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.ArrayList;
import java.util.List;
@SpringBootApplication
public class SentinelDemo01 {
public static void main(String[] args) {
SpringApplication.run(SentinelDemo01.class,args);
//在容器启动之后 设置sentinel规则.
//定义一个规则 数组 流控规则FlowRule 熔断规则DegradeRule 系统规则SystemRule
List flowRules=new ArrayList<>();
//生成一个 规则对象
FlowRule flowRule=new FlowRule();
//使用这个规则绑定过一个资源
flowRule.setResource("sayHi");
//设定 限流 方式 0=并发限流 1=qps限流
flowRule.setGrade(1);
//限流阈值
flowRule.setCount(1);
//添加到规则数组中,
flowRules.add(flowRule);
//sentinel加载这个数组.
FlowRuleManager.loadRules(flowRules);
}
}
```
### 2.2.6 sentinel底层原理

- 每一个entry赋值,都要在内存经过一条责任链(sentinel组件提供)
- 在责任链中 主要经过2种不同的计算逻辑.
- 第一批是固定的计算 nodeselect clusterbuilder statistic
- 第二批对比自定义的规则 如果违反了,抛出异常 没有违反直接向后调用
- 从sentinel责任链中生成赋值entry 才能继续调用保护起来的资源.
NodeSelectSlot: 负责计算 entry树结构的.
ClusterBuilderSlot: 集群节点中 构建相互连接的.
StatisticSlot: 统计,能统计当前请求进入的各种数据 比如 并发数,qps,等待时间,异常数等等.
NodeSelectSlot 管理的entry结构. 一个程序中可以存在多个entry. 在NodeSelectSlot中记录了一个内存的树状结构.
通过这样的树结构, sentinel对资源做计算时,可以清晰的知道 整条调用过程. 从哪个Entry进入的,经过了哪些entry,邻居有谁?..
### 2.2.7 数据源配置规则
sentinel的规则数据,可以通过不同的数据源存储,比如 file文件 db数据库 nacos注册中心配置文件.
不同的数据源,实现是不同的. 可以自定义数据源实现类.
#### 2.2.7.1 JDK 的SPI机制
API和SPI的区别:
API(application programing interface)
即应用程序接口,服务方提供接口和实现,给出接口文档供调用方调用,调用方需要遵循服务方的接口文档规范。
SPI(service provider interface)
即服务提供方接口,服务方提供接口,定义好接口参数、返回等规范,但是实现交给调用方,相当于call back的思想。最终通过SPI机制 让调用方可以加载调用自己实现的代码.(**给提供方提供一个可以发现实现类的方案**)

- test实现demo案例
1. 定义一个接口,模拟提供方的接口
```java
package com.tarena.test.sentinel.spi.datasource;
/**
* 模拟提供者提供的接口
*/
public interface SentinelDatasource {
public void loadRules(String rule);
}
```
2. 模拟调用方 来完成接口实现
```java
package com.tarena.test.sentinel.luban;
import com.tarena.test.sentinel.spi.datasource.SentinelDatasource;
public class MyDatasource implements SentinelDatasource {
@Override
public void loadRules(String rule) {
System.out.println("我的实现加载了");
}
}
```
3. 准备一个文件
所在文件夹 META-INF/services **jdk提供的spi**就是固定这个文件夹.
准备一个文件 名称 是接口的全路径名称SentinelDatasource
文件里的值,就是调用方对接口的实现.
4. 模拟提供方可以加载调用方的实现
```java
package com.tarena.test.sentinel;
import com.tarena.test.sentinel.spi.datasource.SentinelDatasource;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SentinelRun {
public static void main(String[] args) {
ServiceLoader load = ServiceLoader.load(SentinelDatasource.class);
Iterator iterator = load.iterator();
//对这个加载的结果.做迭代循环可以执行代码
while(iterator.hasNext()){
SentinelDatasource implement = iterator.next();
System.out.println("当前实现类的名字"+implement.getClass().getName());
implement.loadRules("流控规则");
}
}
}
```
#### 2.2.7.2 自定义扩展 sentinel数据源
通过 自定义 sentinel的数据源接口,实现一批规则对象的读取和生成,加载到sentinel框架.
sentinel通过底层jdk spi机制 获取到实现类.从而让实现类生效.
- 创建luban-sentinel-demo02工程
- 准备好和demo01初始化一样的代码
HelloController-->HelloService-->SentinelDemo02启动类.
- 我们作为调用方 编写提供方(sentinel)定义的数据源接口
```java
package com.tarena.sentinel.study.demo02.datasource;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义的数据源
*/
public class MyDatasource implements InitFunc {
/**
* 把之前在启动类编写那套加载逻辑 放到这个数据源里呢
*/
@Override
public void init() throws Exception {
//在容器启动之后 设置sentinel规则.
//定义一个规则 数组 流控规则FlowRule 熔断规则DegradeRule 系统规则SystemRule
List flowRules=new ArrayList<>();
//生成一个 规则对象
FlowRule flowRule=new FlowRule();
//使用这个规则绑定过一个资源
flowRule.setResource("sayHi");
//设定 限流 方式 0=并发限流 1=qps限流
flowRule.setGrade(1);
//限流阈值
flowRule.setCount(1);
//添加到规则数组中,
flowRules.add(flowRule);
//sentinel加载这个数组.
FlowRuleManager.loadRules(flowRules);
}
}
```
- 准备SPI底层加载的文件 META-INF/services/com.alibaba.csp.sentinel.init.InitFunc
- 文件内容文本 是自定义的 MyDatasource的全路径名称.
- 使用硬编码的方式 在HelloController 将sayHi资源定义
```java
package com.tarena.sentinel.study.demo02.datasource;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义的数据源
*/
public class MyDatasource implements InitFunc {
/**
* 把之前在启动类编写那套加载逻辑 放到这个数据源里呢
*/
@Override
public void init() throws Exception {
System.out.println("当前自定义数据源初始化init加载了");
//在容器启动之后 设置sentinel规则.
//定义一个规则 数组 流控规则FlowRule 熔断规则DegradeRule 系统规则SystemRule
List flowRules=new ArrayList<>();
//生成一个 规则对象
FlowRule flowRule=new FlowRule();
//使用这个规则绑定过一个资源
flowRule.setResource("sayHi");
//设定 限流 方式 0=并发限流 1=qps限流
flowRule.setGrade(1);
//限流阈值
flowRule.setCount(1);
//添加到规则数组中,
flowRules.add(flowRule);
//sentinel加载这个数组.
FlowRuleManager.loadRules(flowRules);
}
}
```
### 2.2.8 课堂跟踪练习
需求描述:
请编写 一个repository 在业务层 service调用.
在业务代码里service 将repository的方法定义成一个新的资源.
使用任何一种规则加载方式(启动类写,数据源写),将respository的方法新资源做 限流规则控制.
为了区分限流效果,可以将sayHi资源规则 定义QPS=100以上.
目的: 使用已掌握的案例方法功能 实现**定义资源 定义规则**的练习.
### 2.2.9 配置file规则文件
sentinel支持文件的方式配置规则. 也提供了数据源的实现.
我们利用已有数据源实现MyDatasource,读取配置文件 flowRule.json.
- 文件格式 最终在内存 等同于 List\类型对象 放到resources文件夹
```json
[
{
"resource":"sayHi",
"grade":1,
"count":100
},
{
"resource":"sayHiRepo",
"grade":1,
"count":1
}
]
```
- 改造MyDatasource 将读取的flowRule.json 加载(模拟sentinel提供的fileDatasource)
```java
package com.tarena.sentinel.study.demo02.datasource;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import java.net.URL;
import java.sql.SQLOutput;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义的数据源
*/
public class MyDatasource implements InitFunc {
/**
* 把之前在启动类编写那套加载逻辑 放到这个数据源里呢
*/
@Override
public void init() throws Exception {
System.out.println("当前自定义数据源初始化init加载了");
//读取文件 选择使用类加载器读取文件 flowRules.json
ClassLoader classLoader = MyDatasource.class.getClassLoader();
URL resource = classLoader.getResource("flowRules.json");
String fileName = resource.getFile();// flowRules.json
System.out.println(fileName);
//sentinel提供的一套api方法 ,直接将文件交给sentinel读流 转化成List
ReadableDataSource> datasource=
new FileRefreshableDataSource>(fileName,
new Converter>() {
/**
* @param json fileName文件中的json格式的文本
* @return 使用 json转化成对象 (fastJson)
*/
@Override
public List convert(String json) {
return JSON.parseArray(json,FlowRule.class);
}
});
//将规则对象的内容 注册到 sentinel
FlowRuleManager.register2Property(datasource.getProperty());
}
}
```
# 3 今日内容总结
## 3.1 核心概念
- 熔断概念
- 限流概念
- 压测指标和概念
- 计算公式
- qps=并发/RT
- 日pv 28原则
- sentinel中的概念
- 资源: entry赋值,经过了一些列sentinel计算---责任链
- 规则:
- 不同的加载方式
# 1 SENTINEL
## 1.1 SENTINEL定义数据源
除了自定义数据源 sentinel已经实现的 可以支持nacos远程配置文件读取.
### 1.1.1 准备一个项目demo03
### 1.1.2 准备基础代码
HelloController-->HelloService
### 1.1.3 nacos数据源原理
### 1.1.4 添加nacos数据源依赖
```xml
com.alibaba.csp
sentinel-datasource-nacos
```
### 1.1.5 本地flowRules.json迁移到nacos配置中心
public 命名空间 DEFAULT_GROUP分组创建文件
### 1.1.6 配置yaml属性
```yaml
spring:
cloud:
sentinel:
datasource:
#每一个key值 每一个名字 都表示一个数据源的id
"nacos-flow-rules":
#数据源来源类型 redis nacos apollo cloud config...
nacos:
# nacos远程地址
server-addr: localhost:8848
# 当前sentinel1.8 在1.7以后得版本nacos必须提供权限
username: nacos
password: nacos
# 规则类型 flow degrade熔断 system系统保护 param-flow 热点流控等
rule-type: flow
# 文件名称 data-id
data-id: flowRules.json
# namespace nacos命名空间
# groupId 分组
```
## 1.2 注解定义资源
除了在代码中使用Entry直接编码入口,我们还可以利用sentinel提供的注解来实现资源的定义.
### 1.2.1 定义一个简单资源
将HelloService的sayHi方法看成一个资源.可以使用@SentinelResource.
```
package com.tarena.sentinel.study.demo02.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
@SentinelResource(value="sayHi")
public String sayHi(String name) {
return "hello sentinel i am "+name;
}
}
```
### 1.2.2 @SentinelResource底层原理
sentinel使用这个注解,底层实现是切面逻辑.
虽然不在写Entry entry相关代码了,但是sentinel依然存在这个代码,只是放到sentinel切面里了.

### 1.2.3 @SentinelResource属性详解
- value: 定义资源名称,会影响切面中entry声明使用的名字
- blockHandler: 有blockHandler 切面中就存在catch捕获BlockException逻辑. 没有这个属性,底层切面就没有捕获逻辑,一旦限制异常就直接向外抛.单独使用这个属性 必须在**目标类**中准备一个同名的方法,参数,返回值和**目标方法**一致.并且多接收一个参数BlockException.
- blockHandlerClass: 主要负责catch BlockException扩展,可以放到任意一个其它的类中去处理这个异常降级逻辑,需要准备和blockHandler同名方法 并且设置静态调用.
- fallback: 和blockHandler所有逻辑完全一样,唯独能处理blockHandler底层逻辑中不能捕获的异常.单独使用这个属性 必须在**目标类**中准备一个同名的方法,参数,返回值和**目标方法**一致.并且多接收一个参数Throwable.
- fallbackClass: 主要负责对应fallback的扩展逻辑.
### 1.2.4 单独使用blockHandler

```java
package com.tarena.sentinel.study.demo02.service;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class HelloService {
@SentinelResource(value="sayHi",blockHandler = "blockHandle")
public String sayHi(String name) {
return "hello sentinel i am "+name;
}
//处理的时熔断和限流的降级逻辑
public String blockHandle(String name, BlockException e){
log.info("当前访问的方法 正在处理降级逻辑",e);
return "sayHi方法 被sentinel限制流量了";
}
}
```
### 1.2.5 迁移降级逻辑blockHandlerClass
可以引用blockHandlerClass属性 将降级方法迁移到一个指定类.
然后在项目中准备这个类HelloServiceBlockHandler
按照图片配置这个指定类

**结论** blockHandler和blockHandlerClass 生效的时候 目标方法调用还是没调用? 没有调用.
通过blockHandler和blockHandlerClass的使用,延伸学习fallback fallbackClass.
fallbackClass自行尝试使用.
## 1.3 课堂跟踪练习
### 1.3.1 课堂练习需求
经过 sentinel的学习,掌握以下使用方式
- 定义一个项目调用方法为资源
- 使用sentinel整合nacos配置中心 定义资源绑定的规则
目标: 下单操作 做qps=500流控限制.
- order-main 添加相关依赖 2个 sentinel cloud |sentinel nacos数据源
- 定义规则文件 配置yaml读取规则生成数据源加载规则
- 选择目标方法 声明定义资源 编写注解和属性 配置降级逻辑
## 1.4 规则详解
### 1.4.1 限流规则属性详解
```json
[
{
"resource":"app",
"count":1,
"grade":1,
"strategy":1,
"limitApp":"limitApp",
"controlBehavior":0
}
]
```
- resource: 绑定已经定义好的资源名称
- count: 限流阈值,至于阈值表示的何种含义 取决于grade grade=1 qps数量 grade=0 并发数量(web应用并发表示同时存在请求个数)
- grade:
1 qps 流控类型
0 并发 流控类型
- strategry: 限流的模式
0 直接限流: 针对resource的资源做限流
1 关联限流, limitApp 属性值也表示一个资源,如果app的资源访问超过阈值了,限制limitApp.
例如: 双11,所有资源都需要给下单让路.
2 链路 limitApp 指定的资源,查看当前资源的链路是否是通过limitApp进入的,如果不是,不限制流量,如果是则限制流量.(限制的是来源)

- limitApp: 当前资源关联的其它的某个资源,是否生效取决于strategy的值 1 2有用,而且作用不同.
- controlBehavior: 限流效果,当访问已经违反了限流规则时,表现状态
0: 直接拒绝抛异常
1: Warm up 慢慢的拒绝抛异常
2: 排队 后续请求不拒绝 排队到一个queue队列(有上限)
### 1.4.2 熔断规则属性详解
```json
[
{
"resource": "app",
"count": 100,
"grade": 0,
"timeWindow": 10,
"minRequestAmount": 10,
"slowRatioThreshold": 0.5,
"statIntervalMs": 10000
}
]
```
- resource: 绑定已经定义好的资源名称
- count: 熔断触发的阈值(不再调用这个资源,而是访问降级策略),如果grade=0 count表示慢调用临界RT(响应时间单位毫秒),超过这个数字,就记录一次慢调用.grade是1,count值应该是>0小于1的小数,表示异常比例,grade=2 count配置整数,表示异常出现的次数
- grade: 熔断类型 0 默认值 慢调用比例 1 异常比例 2 异常数
- timeWindow: 如果触发熔断,持续时间,单位秒
- minRequestAmount: 最少统计请求数量,如果没达到,即使超过count的阈值,也不熔断
- slowRatioThreshold: 慢调用比例,只有在grade=0的时候才有用.
- statIntervalMs: 统计时长,计算判断熔断规则是否违反的逻辑中,有很多都需要统计时间段 单位是毫秒数.
### 1.4.3 熔断案例
通过这个案例 使用熔断规则. 选择测试案例 demo03来实现.
- 定义一个资源 HelloController方法sayHi定义为资源
```java
package com.tarena.sentinel.study.demo02.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.tarena.sentinel.study.demo02.service.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class HelloController {
@Autowired
private HelloService helloService;
/**
* 接收测试请求的接口功能
* http://localhost:8080/hello?name=王翠花
* hello sentinel i am 王翠花
*/
@GetMapping("/hello")
@SentinelResource(value="app",blockHandler="appBlock")
public String sayHi(String name){
System.out.println(helloService.getClass().getName());
String result=null;
result=helloService.sayHi(name);
return result;
}
public String appBlock(String name,BlockException e){
log.info("当前app资源 被熔断了,持续10秒");
return "请求资源被熔断处理了";
}
}
```
- 定义一个nacos新规则文件degradeRules.json
- yaml属性配置
```yaml
spring:
cloud:
sentinel:
datasource:
"nacos-degrade-rules":
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
data-id: degradeRules.json
rule-type: degrade
```
- 定义一个Thread.sleep方法

- 熔断效果
根据当前规则设置 至少在10秒钟只能访问7次 才能触发熔断规则(**断路器-断开**).
资源被熔断限制访问长达10秒(**断路器的断开状态持续时间**).
10秒钟之后,允许访问1次(**断路器是半开**),但是第二次继续按照熔断处理(**断路器从半开 切换回断开**).
### 1.4.4 sengtinel熔断整合demo业务
以下单为例,将熔断的逻辑,整合到下单操作中.
- 什么样的资源 可以配置熔断(前面介绍熔断概念的例子)
- 非核心业务
- 下单操作过程中哪些业务符合这样的特点
- 减库存: 生成订单前提是 必须减库存,不管减库存因为什么原因失败,异常,卡主,都不能进行熔断处理.
- 生成订单: 如果生成订单没有成功,超时,异常等,想办法,把库存会还,也不能进行熔断.
- 删除购物车: 下单中最不重要的,甚至可有可无的业务.
### 1.4.5 购物车删除的熔断配置
- 给删除购物车的代码片段 定义资源-cartDelete
准备一个单独封装删除购物车方法的bean对象
```java
package com.tarena.demo.luban.all.main.component;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.tarena.demo.luban.protocol.order.param.OrderAddParam;
import com.tarena.luban.demo.cart.api.CartRpcApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CartRpcDeleteComponent {
@Autowired
private CartRpcApi cartRpcApi;
@SentinelResource(value="cartDelete",blockHandler = "cartDeleteBlock")
public void cartDelete(OrderAddParam param){
cartRpcApi.cartDelete(param.getUserId(),param.getProductCode());
}
public void cartDeleteBlock(OrderAddParam param, BlockException e){
//删除购物车失败 ,调用失败 时间过长 熔断降级
System.out.println("删除购物车失败,当前购物车参数 userId"+param.getUserId()+
"productCode"+param.getProductCode());
}
}
```
- 给cartDelete定义熔断规则.
- 慢调用: 10秒统计时间 最少访问删除购物车10次 每次超过150记录一次慢调用,总体慢调用比例超过50% 将对cartDelete熔断处理.
```json
[
{
"resource": "cartDelete",
"count": 150,
"grade": 0,
"timeWindow": 10,
"minRequestAmount": 10,
"slowRatioThreshold": 0.5,
"statIntervalMs": 10000
}
]
```
- 异常比例|**异常数**: 10秒统计时间内,最少0次删除购物车访问,只要异常达到1次,对资源进行10秒的熔断处理.
```json
[
{
"resource": "cartDelete",
"count": 150,
"grade": 0,
"timeWindow": 10,
"minRequestAmount": 10,
"slowRatioThreshold": 0.5,
"statIntervalMs": 10000
},
{
"resource": "cartDelete",
"count": 1,
"grade": 2,
"timeWindow": 10,
"minRequestAmount": 0,
"slowRatioThreshold": 0.5,
"statIntervalMs": 10000
}
]
```


### 1.4.6 案例问题解析
没有违反 慢调用比例的规则.
没满足 最小请求数量10.
# 2 面试真题
背景: 某位同学面试复试的问题,一道题
题目:
一个老师 教一个班级的学生们四门课.数学,英语,音乐和自然.
要求对于在上这些课程的学生们满足以下条件:
1. 每节课只有3个学生
2. 每个班任意每两个学生至少一起上一门课程
要求:
编写一段代码程序: 计算该班最多可以有多少学生并生成所有符合上述条件的分组可能.(可以只提供解题思路)
# 3 消息队列-rocketmq
## 3.1 rocketmq环境
目标:
- [ ] 成功启动rocketmq的2个核心进程 nameserver broker
- [ ] 最好能够启动第三个进程,辅助学习的进程 rocketmq的 仪表盘dashboard
### 3.1.1 找到压缩包解压
解压到没有中文,没有空格的路径中


### 3.1.2 配置环境变量ROCKETMQ_HOME
环境变量名称ROCKETMQ_HOME
环境变量的值 上边截图的家目录 路径
配置结束之后,重新打开一个CMD验证
```shell
echo %ROCKETMQ_HOME%
```
此电脑-->属性-->高级系统设置-->高级标签-->环境变量
### 3.1.3 确定是否存在JAVA_HOME环境变量并且版本JDK1.8

### 3.1.4 启动ROCKETMQ核心2个进程
在家目录有bin文件夹(binarry缩写,一般存放脚本命令文件)
使用cmd进入到这个文件夹.

- 启动nameserver(类似nacos)
1. windows
```shell
bin>mqnamesrv.cmd
```

2. mac
下面2个命令 执行方式都试试.
```shell
bin>mqnamesrv
```
```shell
bin>./mqnamesrv
```
- 启动broker
在打开一个cmd 在bin目录中执行命令脚本
1. windows(localhost:9876就是namesrv)
```shell
bin>mqbroker.cmd -n localhost:9876
```

2. mac
```shell
bin>mqbroker -n localhost:9876
```
```shell
bin>./mqbroker -n localhost:9876
```
### 3.1.5 启动辅助工具--仪表盘
利用这个辅助工具 检查观察rocketmq中一些数据展示页面 图形等.
- 找到jar包
- jar放到没有中文没有空格的文件夹
- 在jar包路径中打开cmd运行java命令

```shell
java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar --server.port=9999 --rocketmq.config.namesrvAddr=localhost:9876
```

- 访问对应页面
http://localhost:9999

# 1 Rocketmq
## 1.1 启动进程
- mqnamesrv.cmd (nameserver)
- mqbroker.cmd (broker)
- 启动dashboard.jar包 (仪表盘)
## 1.2 入门案例
### 1.2.1 消息队列介绍
RocketMQ是消息队列的一种.
MQ:消息队列 message queue,是一种可以进行**储存**消息,**发送**消息,**消费**消息的队列中间件. **Message指的不是字符串**,而是一个消息对象数据.
- 基本结构

producer: 开发编写的代码 生成消息对象数据 发送消息
consumer: 开发编写的代码 接收消息对象 消息消费
队列: 负责消息的存储
- 类似的软件
rabbitmq: 分布式支持的不是特别好.吞吐不大的时候可以使用.
kafka: 大数据领域使用,负责数据收集通道.
JMS: java 中的队列存储 性能一般
RocketMQ: 仿照kafka的结构 支持高吞吐 支持分布式
- Rocketmq
官方网址: https://rocketmq.apache.org/

### 1.2.2 案例代码
需求: 实现一个入门级的消息生产和消息消费逻辑. 一发一接
- 创建一个测试rocketmq工程
- 添加相关依赖
```xml
junit
junit
test
org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.2
```
- 实现代码
按照需求,按照基本结构编写 生产者 发送消息 编写消费者 消费消息.
MyProducer:
```java
package com.tarena.rocketmq.study;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
/**
* 组织消息
* 发送消息
*/
public class MyProducer {
@Test
public void send() throws Exception {
//1. 连接nameserver生产者对象
DefaultMQProducer producer=new DefaultMQProducer();
producer.setNamesrvAddr("localhost:9876");
//生产者 和消费者 都是集群结构
producer.setProducerGroup("test-producer01");
//生产者可以启动了.
producer.start();
//2. 组织创建一个消息
//创建一个消息对象
Message message=new Message();
//消息数据 message body
String msgTxt="你好呀,这是第一条消息";
message.setBody(msgTxt.getBytes(StandardCharsets.UTF_8));
//提供一个主题名称,在消息中间件中保存消息的目的地 当前消息队列没有主题名称 但是代码可以自动创建
message.setTopic("test-01-topic");
//3. 将消息发送
SendResult send = producer.send(message);
System.out.println(send.getSendStatus());//表示发送的状态
while(true);
}
}
```
错误:

解决:


MyConsumer:
```java
package com.tarena.rocketmq.study;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListener;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.sql.SQLOutput;
import java.util.List;
/**
* 监听一个主题
* 拿到消息进行消费
*/
public class MyConsumer {
@Test
public void consume() throws Exception{
//1. 创建一个消费者对象
DefaultMQPushConsumer consumer=new DefaultMQPushConsumer();
//连接nameserver
consumer.setNamesrvAddr("localhost:9876");
//消费者分组
consumer.setConsumerGroup("test-consumer01");
//设置主题 监听这个主题
consumer.subscribe("test-01-topic","*");
//2. 创建监听器 实现消费逻辑
MessageListenerConcurrently messageListener=
new MessageListenerConcurrently() {
/**
* 每拿到一条消息,方法就调用一次
* @param msgs 接收到的消息
* @param consumeConcurrentlyContext 消费者信息
* @return CONSUME_SUCCESS 表示消费成功 RECONSUME_LATER 表示过一段时间再重新消费
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//解析消息
MessageExt messageExt = msgs.get(0);
byte[] body = messageExt.getBody();
String msg=new String(body, StandardCharsets.UTF_8);
System.out.println("消费者消费:"+msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
};
//给消费者装配这个监听器
consumer.setMessageListener(messageListener);
//3. 建立连接开启消费者
consumer.start();
while(true);
}
}
```
### 1.2.3 课堂跟踪练习
参考入门案例 实现 一个一发一消费的结构.
换第二个主题名称尝试
# 2 RocketMQ核心概念和运行原理
## 2.1 nameserver
启动的一个核心进程. mqnamesrv.cmd启动的进程.
NameServer是一个简单的 Topic 路由**注册中心**,支持 Topic、Broker 的动态**注册与发现。**
谁来**注册**: broker将自身信息,定时的向nameserver同步注册,包括broker ip 端口 管理包含的主题 主题队列信息等都注册在nameserver

谁来**发现**: 代码客户端(producer consumer). 代码启动之后会建立和nameserver连接.定时更新抓取的.根据抓取的信息,代码客户端(4.X)需要计算负载均衡,某个消息发给谁,当前消费绑定谁都是固定计算结果.

## 2.2 broker
**Broker主要负责消息的存储、投递和查询以及服务高可用保证。**
**高可用保证:**
broker支持Master-slave结构的.Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。

**高并发高吞吐保证**

上述架构中唯一的问题:nameserver 单机故障问题

了解了nameserver和broker之后 rocketmq的运行架构 基本结束.
问题: broker负责存储消息,broker存储数据结构 什么样.
## 2.3 主题 Topic
一个rocketmq的集群中可以有多个主题.

同一个主题,表示的是同一类消息的集合.
例如:
- order-topic: 存储的都是 订单消息
- cart-topic: 存储的都是购物车消息
主题最终由broker管理的,在注册的时候,在nameserver中注册主题的**路由关系**(某个主题 在某个broker管理)
主题创建之后 就存储在对应的broker节点,随着注册 在nameserver保存broker详细地址和主题的路由信息.生产者或者消费者 从nameserver拿到注册信息,计算完成才实现消息发送和消费监听.
分布式结构的主题和broker的关系

问题: 为什么同一个主题可以分散到不同的broker存储呢?
## 2.4 队列queue
存储消息的物理实体(最小单位)。一个Topic中可以包含**多个Queue**(分布式体现的关键),每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(partition).
- 单机broker

- 分布式brokers

消息发送的整体流程:
1. 创建生成主题.broker会将主题和队列的路由信息 注册在nameserver
2. producer 生产者抓取注册信息,获取目标主题和broker和队列的所有信息.
3. producer组织消息数据,进行负载均衡计算,计算确切结果输出.
4. 创建连接 连接到具体broker将消息发送.
## 2.5 生产者组
在微服务 分布式高可用集群中(微服务实例非常多的).
producer生产者的角色也很多.RocketMQ为了方便管理,将每一个生产者都列入一个生产者组.
生产者---1:n---生产者组.

从技术角度讲: 一个生产者组可以向多个主题发送消息,同一个主题也可以接收多个不同生产者组发送的消息.
从业务角度: 只要能保证发送给不同主题的消息,是同一种类型的消息,就可以组合.否则会出现消费失败,消费故障. (一般情况下 一个分组的生产者 只给一个主题发送消息,如果需要发送给多个主题,多创建一个生产者分组).
## 2.6 消费者组
和生产者一样,消费者也必须要指定分组.
消费者进程通过连接nameserver获取注册信息,根据主题中的队列个数,根据主题中记录的消费者组的信息.确定直接绑定的队列.
- 只有一个消费者

- 一个分组有多个消费者
多个消费者进程会将主题中的所有的队列平均分配.

所以消费者进程和生产者进程个数 不一样(没关系),生产者个数不受队列的约束,但是消费者最大数量就是这个主题中的队列数量.
问题: 如果多个消费者分组 监听同一个主题,会发生什么事?
## 2.7 消费位点
- 每一个在队列中存储的消息,rocketmq主题都会记录偏移量.


- 主题针对同一个分组.还会记录消费位点(消费者消费了多少条消息,消费偏移量)

- 同一个消费者组的消费者共享消费偏移|位点

- 不同消费者组消费位点记录 相互隔离

## 2.8 keys和tags
- KEYS
每个消息,keys绑定到消息的一个**业务数据**,用来做查询统计使用的.
例如: 组织一个订单消息,可以绑定过一个key值 可以是消息编码orderSn.
如果没有这个key值,想要查询一条消息,需要使用消息的id,而消息id是rocketmq自动生成的没有任何业务含义.
- tags
每个消息允许绑定标签tags. 可以在消费端进程中使用标签进行过滤.

## 2.9 系统作业
Rocketmq 无论是发送消息还是监听消费消息,还有很多不同的场景可以尝试编写案例.
rocketmq还支持很多消息发送和消费的特性.比如:
push消费(已做,关注消费速度)
pull消费(主动拉取消息,不拉取,消息不会达到消费者进程. 速度处理慢)
生产者 发送顺序消息 延迟消息等等.
系统作业要求:
请阅读官方相关文档,将其它的生产者案例代码和消费者案例代码,自行测试一遍.
# 3 SpringBoot应用整合RocketMQ
## 3.1目标
- [ ] 掌握使用整合rocketmq实现消息发送
- [ ] 掌握使用整合rocketmq实现消息消费
## 3.2 整合rocketmq步骤
- 添加相关依赖
```xml
org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.2
org.springframework.boot
spring-boot-starter-web
```
- yaml
```yaml
rocketmq:
# nameserver 地址
name-server: localhost:9876
# 满足producer的创建 必须提供的属性
producer:
group: test-producer-group
# 满足pullConsumer的创建 必须提供的属性
consumer:
group: test-consumer-group
```
## 3.3 实现消息的生产发送
### 3.3.1 准备测试案例基座
SendController. 接收一个请求. 携带一个请求参数 message=王翠花. 将这个参数作为消息数据发送到队列.
接口文件:
| 要素 | 值 | 备注 |
| -------- | ---------- | ------------- |
| 请求地址 | /send | |
| 请求方式 | get | |
| 请求参数 | String msg | |
| 返回值 | String | 固定"success" |
```java
package com.tarena.rocketmq.study.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SendController {
//localhost:8080/send?msg=消息内容
@GetMapping("/send")
public String send(String msg){
//TODO 使用api发送消息
return "success";
}
}
```
### 3.3.2 启动类
```java
package com.tarena.rocketmq.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RocketMain {
public static void main(String[] args) {
SpringApplication.run(RocketMain.class,args);
}
}
```
### 3.3.3 注入RocketMQTemplate实现消息发送
- syncSend
- 发送普通消息
- 发送延迟消息: 消息在发送之后 ,会延迟一段时间才能出现在主题队列中.
官方给定义了18个延迟级别 0表示不延迟 1-18 延迟时间不断增加
- asyncSend
- 发送普通消息
- 发送延迟消息
直接将RocketMQTemplate注入到当前controller中.
测试异步发送消息
```java
package com.tarena.rocketmq.study.controller;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SendController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
//localhost:8080/send?msg=哈哈
@GetMapping("/send")
public String send(String msg){
if (StringUtils.isEmpty(msg)){
return "msg is null";
}
Message message = MessageBuilder.withPayload(msg).build();
//1.同步发送: 发送消息的生产者,必须等待rocketmq消息队列反馈 才继续执行代码
//1.1 普通消息
SendResult sendResult = rocketMQTemplate.syncSend(
"test-01-topic",
message);
System.out.println(sendResult.getSendStatus());
//1.2 延迟消息 send方法多了2个参数 timeout delayLevel
//timeout表示发送链接超时时间 网络通常可以定义超时时间较短,如果波动常见 可以定义稍长的时间
/*SendResult sendResult = rocketMQTemplate.syncSend(
"test-01-topic",
message,500,4);
System.out.println(sendResult.getSendStatus());*/
//2.异步发送: 发送消息的生产者,发完消息 线程立刻向下执行代码. 通过回调获取反馈结果
//2.1 异步普通消息
//2.2 异步延迟消息
//构造一个消息,可以在消息中携带任意类型的消息体数据 springboot整合对象会进行自动序列化
/*//异步发送参数 destination 目标 message消息对象 SendCallback异步方法回调
rocketMQTemplate.asyncSend("test-01-topic", message, new SendCallback() {
//异步调用 接收rocketmq反馈
//发送成功 调用onSuccess
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
//发送失败 调用onException
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败");
}
});*/
return "success";
}
}
```
### 3.3.4消息push消费
底层肯定创建了监听器.
- 准备一个消费类,称为容器的bean对象
- 实现一个接口,实现接口方法onMessage
- 使用rocketmq整合的注解@RocketMQMessageListener 定义监听参数.
每当主题中出现一条消息,接口实现的onMessage将会被调用一次.
```java
package com.tarena.rocketmq.study.consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "test-01-topic",
consumerGroup = "${rocketmq.consumer.group}",
selectorExpression = "*"
)
//接口的泛型 对应发送时 消息封装payLoad的类型
public class MyConsumer01 implements RocketMQListener {
/**
* @param message 是payLoad反序列化结果
* 底层是一个MessageListenerCurrently监听器 需要返回值
* CONSUME_SUCCESS RECONSUME_LATER
* 如果onMessage没有向外抛出异常,底层返回结果就是 CONSUME_SUCCESS
* 如果onMessage 底层返回结果就是 RECONSUME_LATER(消息会被重复消费 retry)
*/
@Override
public void onMessage(String message) {
System.out.println("消费者01框架消费监听 拿到消息:"+message);
}
}
```
# 附录
# 1 清空rocketmq数据
## 1.1 步骤
- 停止 nameserver和broker
- 找到当前默认的rocketmq的持久化文件夹{user.home}/store
- 重启nameserver和broker
# 2 调整rocketmq启动内存
默认情况下 broker占用2G内存 nameserver占用2G内存.
## 2.1 进入启动脚本的文件夹
mqnamesrv.cmd mqbroker.cmd bin文件夹.
使用编辑器软件 ever-edit notepad++. 打开2个文件
runserver.cmd runbroker.cmd
## 2.2 修改runserver.cmd|runserver.sh
## 2.3 修改runbroker.cmd|runbroker.sh


# 附录
# 1 清空rocketmq数据
## 1.1 步骤
- 停止 nameserver和broker
- 找到当前默认的rocketmq的持久化文件夹{user.home}/store
- 重启nameserver和broker
# 1 回顾ROCKETMQ核心概念和原理
https://www.processon.com/v/6542f846d4825870f59173d2
# 2 RocketMQ结合业务应用
## 2.1 消息队列的应用场景
短信|微信:
producer: 手机当中的短信软件|微信软件
consumer: 手机当中的短信软件|微信软件
message: 软件根据用户输入的内容(目标地址,信息内容) 组织的消息对象
邮件:
producer: 发送邮件的进程
consumer: 接收邮件的进程
message: 编辑的邮件产生的消息对象
抢红包:
producer: 发送红包的客户端软件
consumer: 抢红包的客户端软件
message: 发送红包之前 组织的红包个数,一个红包就是一个message对象

### 2.1.1 异步解耦
将同步的代码功能 变成异步的调用,形成解耦.

### 2.1.2 削峰填谷
由于访问的处理 大部分是同步处理,可以处理的并发量取决于 线程 服务器性能.
为了提高服务器响应效率,会尽可能引入提升性能的软件技术,比如redis.
削峰填谷,本质上也是可以尽快处理请求的一种方式.
处理高并发时,使用队列,做削峰(超出服务器处理能力的请求 放到队列)填谷(延迟一段时间处理这些超出的请求).
## 2.2 消息队列的业务应用
### 2.2.1 需求(削峰填谷)
如果下单操作 是同步处理的. 进程没有处理完业务流程,就不会有相应.
通过引入消息队列实现**削峰填谷**,提升系统的并发访问能力.
引入消息队列.让系统资源暂时没有空闲处理能力的时候,业务在队列中排队等待.

### 2.2.2 实现削峰填谷案例
- order-main 添加rocketmq依赖资源
```xml
org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.2
```
- yaml添加rocketmq属性
```xml
rocketmq:
name-server: localhost:9876
producer:
group: luban-demo-order-producer
consumer:
group: luban-demo-order-consumer
```
- 消息生产者
业务场景要满足一下2点消息组织原则:
1. 满足消费业务需要.
2. 精简.
```java
package com.tarena.demo.luban.all.main.controller;
import com.tarena.demo.luban.all.main.service.OrderService;
import com.tarena.demo.luban.commons.restful.JsonResult;
import com.tarena.demo.luban.protocol.order.param.OrderAddParam;
import com.tarena.luban.demo.cart.api.DubboTestRpcApi;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/order")
@Api(tags = "订单")
public class OrderController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@PostMapping("/add")
@ApiOperation("新增订单的功能")
public JsonResult addOrder(OrderAddParam orderAddParam){
//组织精简准确的消息对象
Message message =
MessageBuilder.withPayload(orderAddParam).build();
//将消息对象发送 到目标主题 order-add-topic
rocketMQTemplate.asyncSend("order-add-topic", message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
}
@Override
public void onException(Throwable throwable) {
//处理失败信息存储 达到2秒钟
}
});
return JsonResult.ok("新增订单完成!");
}
}
```
- 消费端
框架整合 创建编写消费逻辑 三步.
1. 准备消费类 容器bean管理
2. 实现接口,接口泛型就是消息数据类型
3. 指定消费的参数属性 使用注解
```java
package com.tarena.demo.luban.all.main.consumer;
import com.tarena.demo.luban.all.main.service.OrderService;
import com.tarena.demo.luban.protocol.order.param.OrderAddParam;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "order-add-topic",
consumerGroup = "${rocketmq.consumer.group}")
public class OrderAddConsumer implements RocketMQListener {
@Autowired
private OrderService orderService;
@Override
public void onMessage(OrderAddParam message) {
//调用业务
orderService.addOrder(message);
}
}
```
- 启动测试
1. 软件工具 启动 nacos nameserver broker
2. 启动order cart stock 进行测试
3. 新增购物车 新增订单 检验数据
## 2.3 rocketmq 消息队列相关面试题
### 2.3.1 面试题描述
1. **消息丢失**怎么处理的|怎么保证**消息**不**丢失**|你这个系统消息有没有可能**丢**
2. 消息**重复消费**怎么处理|**重复消费**的处理逻辑|哪个业务消息**重复消费**处理了
### 2.3.2 消息丢失
- 明确消息丢失的位置
- 生产者 发送消息丢失
- broker 存储消息丢失
- 消费者 消费逻辑中丢失
- 要不要关注消息丢失,或者要不要保证消息不丢失
- 订单消息: 不能丢
- 弹幕: 可以丢
- 处理订单丢失:
- 发丢了
- 原因: 代码没有关注 或者没有处理rocketmq反馈消息发送失败的逻辑
- 解决思路: 在发送消息之后,无论是同步 还是异步 rocketmq一定会有反馈 需要根据反馈的内容 执行后续的操作
- 记录日志,等待用户投诉反馈,人工处理
- 接丢了
- 原因: 消费逻辑处理不正确.在消费失败的情况下 也返回CONSUME_SUCCESS.
- 解决思路: 正确理解业务顺序 在正确的位置 返回正确状态 成功了就返回CONSUME_SUCCESS 没成功 RECONSUMER_LATER
- 存丢了
- 原因: 消息存储在队列的,队列是在broker服务器进程中管理.rocketmq为了提供一个高可用的方案,采用持久化的逻辑.没来得及持久化,进程就奔溃了. 单机架构.
- 解决方案:
- 单机架构 升级成 MS主从架构
- 主从中落盘持久化的方式,采用 MASTER **同步落盘**,SLAVE **异步落盘**
同步落盘|同步刷盘|异步落盘|异步刷盘
异步落盘 指的是,在rocketmq接收到生产者的消息,开始执行持久化,但是持久化的结果还没有结束,就将成功接收消息的反馈返回给生产者,所以只有异步落盘,**是不能保证消息持久化结果的**.但是速度快.
只要rocketmq没有持久化这条消息成功,就不会反馈给生产者 成功的信息.同步落盘能保证消息存储的可靠性.
但是单独的broker处理同步落盘 依然存在单机故障. 必定概要引入ms主从.
主节点也是同步落盘,从节点也是同步落盘,这种架构可以最高级别的保证消息存储不丢失.因为生产者发送的消息,只要有一个节点没有完成持久化,生产者都不会接受到反馈成功的信息.
这种架构会影响rocketmq的吞吐量.所以平衡问题,既能保证吞吐量,又能保证一定的可靠性.
最终采用 **master做同步落盘 slave异步落盘**
### 2.3.3 消息重复消费
#### 2.3.3.1 充足消费的原因
消费者监听绑定主题后,在rocketmq中,投递机制是 **at least once**(至少投递一次).除此以外的投递机制(at-most-once最多一次,exactly-once只能一次 )
这个投递机制导致消费消息可能出现重复消费.
这里介绍2个具体原因.
- RETRY机制
consumer 返回结果是 RECONSUME_LATER.
每一个consumer分组除了绑定目标主题以外,rocketmq还会给消费者组绑定过一个%RETRY%{consumerGroup}.
当某个消息 在消费结束后 返回RECONSUME_LATER.这个消息在原主题 中位点就移动了.但是由于没有消费成功 rocketmq会将这个消息当成一个新的消息发送到RETRY主题里.
由于consumer也绑定这个retry的主题了,所以会重新消费这条消息.
随着重新retry消息的次数 消费等待时间(内部延迟处理) 逐步递增.
**当消息重复触发 18次之后(延迟级别delayLevel 18 2小时)**. 依然消费失败. 消息队列,将会把消息发送到死信队列(Dead Letter Queue). 必定需要人工
- REBALANCE(队列消息重平衡)
结论: 出现REBALANCE概率是极低的,因为很少会在一个上线系统做扩容或者缩容的处理.
REBALANCE: rocketmq对消息重平衡. 当rocketmq集群broker做扩容(集群规模变大)缩容(集群规模变小),都会出现重平衡的情况. 分散的队列,重新排布分赛的结果.
1. broker集群做了扩容,队列做了扩容
2. 没有在扩容之前正确返回消费成功的信息这些消息,会在重平衡后 重新下发投递
出现重平衡的时候,rocketmq服务端 会对扩展的队列做消息重平衡,没有反馈成功消费的消息,可能在重平衡之后 绑定到同一个消费者分组中不同的消费对象里,和原来消费对象产生了同一个消息 重复消费的现象.
#### 2.3.3.2 解决方案
重复消费,是否需要解决?
对于当前 减库存 生订单 删除购物车操作 ,如果重复消费同一个消息,会造成影响. 多减库存,多生成冗余订单.
采用业务消费方法**幂等**的方案.
**幂等方法**: 如果一个方法是幂等,**同一时刻调用这个方法1次,和调用N次结果是相同的.**
只需要在调用的位置,编写一个验证的for循环,多次调用.如果结果相同,这个方法就是幂等的.
#### 2.3.3.3 方法幂等的设计思路
- 查询方法: 查询的操作 天生是幂等的,不需要单独设计.
- 删除方法: 删除方法和查询一样 天生幂等,不需要单独设计.
- 新增方法: 新增方法,一般也不是天生幂等的.需要单独考虑
insert into user (id,name) values ("1","王翠花"); id自增 指定主键新增 幂等生效
insert into user (id,name) values (null,"王翠花"); id自增 不指定主键值 不幂等
- 更新方法: 更新方法,一般也不是天生幂等的.需要单独考虑
update user set age=age+1 where id=1; 不是幂等
update user set age=18 where id=1; 是幂等的
新增订单 设计到多少个底层操作.
- 减库存
- 扣减库存: 不幂等
- 新增日志记录: 没有唯一字段校验,不幂等
- 新增订单
- 新增订单: 不幂等
- 删除购物车
- 删除购物车: 幂等
有任何一个底层逻辑不是幂等的,整个新增订单就不是幂等的.
思路1: 将addOrder方法中涉及到的所有方法都涉及成幂等.
reduceCount减库存: 使用orderSn查询库存减少日志,如果存在日志,说明已经减过了,不允许再次执行减库存逻辑.如果没有库存记录,说明是第一次减库存.
新增订单insertOrder:
1. 先查询当前订单orderSn存在不存在,存在则不再新增,不存在则新增.
2. orderSn是唯一值.可以在数据库设置唯一性校验. 处理数据库异常
#### 2.3.3.4 新增订单幂等的业务逻辑

添加订单业务的 查询功能.
```java
@Override public void addOrder(OrderAddParam param) {
//利用参数orderSn 查询订单,如果订单已经存在了
String orderSn=param.getOrderSn();
//select count(*) from order_tbl where order_sn=#{orderSn}
Integer count=orderMapper.countOrderByOrderSn(orderSn);
if (count>0){
log.info("当前订单{},已经存在了",orderSn);
return;
}
stockApi.reduceCount(param.getOrderSn(),param.getProductCode(),param.getCount());
OrderDO orderDO=new OrderDO();
BeanUtils.copyProperties(param,orderDO);
orderMapper.insertOrder(orderDO);
cartRpcApi.cartDelete(param.getUserId(), param.getProductCode());
}
```
OrderMapper
```java
package com.tarena.demo.luban.all.main.mapper;
import com.tarena.demo.luban.protocol.order.dos.OrderDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderMapper {
// 新增订单的方法
@Insert("insert into order_tbl(user_id,product_code,count,total_money,order_sn) values" +
"(#{userId},#{productCode},#{count},#{totalMoney},#{orderSn})")
int insertOrder(OrderDO order);
@Select("select count(*) from order_tbl where order_sn=#{orderSn}")
Integer countOrderByOrderSn(String orderSn);
}
```
除了消费者调用新增订单.提交订单的方法,是否应该也做幂等涉及.消息对于同一个订单只发送一次.
#### 2.3.3.5 订单重复提交的幂等设计
- 订单重复提交的问题: 用户停留在订单提交页面,多次点击页面中提交订单按钮.如果没有处理这个问题,会导致 对同一个订单发送多个消息.
- 解决方案: 使用orderSn做发送消息的限制. 每次请求新增订单 在controller方法中使用redis对订单编号orderSn做自增设计. 返回结果=1 则表示当前订单是第一次提交,如果返回结果>1则表示当前订单是多次提交.不发送消息.
**第一步:** 依赖
```xml
org.springframework.boot
spring-boot-starter-data-redis
```
**第二步**: yaml配置
```yaml
spring.redis.host=localhost
spring.redis.port=6379
```
**第三步**: 直接注入对象调用api操作redis的各种命令
RedisTemplate