# 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和名字一致 image-20231020093406875 - 对照自己的settings.xml 修改id和name然后替换到 ${user.home}\\.m2 文件夹里.粘贴替换 - 替换之后 maven的对于仓库资源的工作逻辑 image-20231020095150375 ![image-20231020151609483](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310201516552.png) ## 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自带插件 image-20231020101852576 - 自定义命令 image-20231020101946745 image-20231020102056438 ### 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流程时序图 image-20231020105242793 ## 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 缺点 前提: 业务壮大,在发展,在变复杂.(**项目代码的接口多了**) - 代码在大型系统中臃肿.维护更新很困难. - 并发流量的木桶原则. image-20231020115851812 ### 4.2.3 应用场景 互联网公司项目,面对用户群体比较庞大. **功能多,扩展快**. 不合适单体架构 传统项目,用户体量不大.功能更新扩展不迅速. 例如: 早期公司oa,财务, 养殖,国企项目. # 5 项目的纵向拆分 ## 5.1 纵向拆分的概念 系统结构演变过程 单体架构---分布式架构 纵向拆分是这种演变必要过渡阶段. 将一个系统中 按照**功能**,**逻辑**,划分成多个**独立运行**的单体系统---纵向拆分的过程. image-20231020141103910 将这个项目中依赖关系,根据应用功能,进行纵向拆分. image-20231020141612760 必定导致2个直接的问题出现: - 拆分的独立系统之间 需要引入解决方案解决沟通问题. - 如果需要沟通,相互应该传递应用信息. 结果: 在拆分之后,要解决更多新的问题,否则架构不能使用. ## 5.2 纵向拆分练习 ### 5.2.1 目标|步骤 - 步骤 - 课上带领拆分出来order业务 - 练习将剩下的cart业务 和stock业务拆分 - 目标: - 成功拆分 - 成功运行 - 拆分的maven应用模块结构 image-20231020142752621 ### 5.2.2 如何创建maven项目 - 右键项目任何项目标题 new project image-20231020143006262 - 选中左侧maven项目 next 注意 jdk版本(1.8) image-20231020143147831 - 填写详情信息 注意继承 和项目名称 image-20231020143416613 ### 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 image-20231020145441284 ### 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 现象 image-20231020143857693 ### 1.1.2 原因 当前idea版本 对与删除的(remove-module)maven项目 做了自动忽略ignore操作. ### 1.1.3 解决办法 file-->settings-->搜索maven-->ignore files --> 勾掉忽略的文件. # 提前准备好课前资料 image-20231021090400639 # 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 解压文件 - 课前资料 image-20231021101535776 - nacos-server压缩包 **解压 没有中文 没有空格的路径 image-20231021101700988 ### 4.2.2 检查JAVA_HOME环境变量 在当前系统中 检查是否存在JAVA_HOME环境变量. windows cmd终端 ```shell echo %JAVA_HOME% ``` mac 终端 ```shell echo $JAVA_HOME ``` image-20231021102039217 必须配置 并且要求 使用**jdk1.8** 要是路径**没有空格.** 环境变量配置(windows) 此电脑--> 右键属性-->高级系统设置-->高级-->环境变量 image-20231021102348857 image-20231021102409906 image-20231021102438206 image-20231021102819763 image-20231021103708449 ### 4.2.3 运行nacos启动脚本 - 找到nacos家目录中的bin文件夹 image-20231021104116439 - 使用终端 进入这个文件夹bin image-20231021104305916 windows ```shell startup.cmd -m standalone ``` mac ```shell startup -m standalone ``` ```shell ./startup -m standalone ``` image-20231021104648953 windows cmd 控制台运行输入日志时,有可能会卡主.多敲击几次回车,反之这种情况出现. - 访问页面控制台 http://localhost:8848/nacos 用户名 \ 密码: nacos \ nacos image-20231021105149093 ### 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应用的一个子进程. image-20231021113046928 ### 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 image-20231021115847428 order-main 启动 之后可以和nacos-server进行通信和交互. 访问 localhost:8848/nacos 控制台 左侧静态菜单--服务管理 image-20231021120116041 ### 4.3.3 课堂跟踪练习 实现 cart 和stock 整合nacos-client ![image-20231021131358841](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310211313920.png) 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 同一个服务多实例配置 如下步骤 **第一步**: 准备一个可以启动的项目. **第二步:** 修改启动配置项 ![image-20231021151944253](doc/imgs/image-20231021151944253.png) ![image-20231021152148355](doc/imgs/image-20231021152148355.png) ![image-20231021152229771](doc/imgs/image-20231021152229771.png) ![image-20231021152558875](doc/imgs/image-20231021152558875.png) ### 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服务端记录每次上报的时间戳. ![image-20231021161940441](doc/imgs/image-20231021161940441.png) 超时剔除 - 永久实例: 永不剔除 - 临时实例: 每个一段时间,发送一次心跳上报请求,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. 数据需要隔离. image-20231021164142297 默认命名空间 叫做public. 我们可以手动创建.比如当前开发环境dev.可以创建命名空间 dev. ![image-20231021164626133](doc/imgs/image-20231021164626133.png) 可以通过实例进程yaml属性配置指定命名空间得id值. ```yaml spring: cloud: nacos: discovery: namespace: ``` - 分组策略(灰度发布) 在同一个命名空间,可以通过分组的配置,使得服务之间进行隔离, 分级管理的第二层----分组配置 分组的应用功能---**灰度发布**. 灰度发布的意思就是在同一个环境中 存在多个不同版本的服务进程. 例如: 电商 互联网项目(美团) 持续发布的项目: 同时存在很多版本 版本发布项目: 只能存在一个固定版本 image-20231021171850099 通过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 课堂跟踪练习 根据下图,完成多实例微服务集群的启动. ![image-20230921161907785](doc/imgs/image-20230921161907785.png) ![image-20231022225241406](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310222252504.png) ![image-20231022225342698](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310222253763.png) ![image-20231022225405322](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310222254385.png) # 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在启动应用程序之前 先读取远端配置文件. ![image-20231023092536155](doc/imgs/image-20231023092536155.png) 而且 不一定把所有当前集群管理不同服务管理的公用配置都放到一个远端文件. ![image-20231023092814424](doc/imgs/image-20231023092814424.png) ## 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 image-20231023094550482 2. bootstrap.yaml加载流程 目的: 理解bootstrap.yaml和application.yaml得关系. ![image-20231023095637036](doc/imgs/image-20231023095637036.png) 结论: 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 ``` ![image-20231023102207617](doc/imgs/image-20231023102207617.png) **第三步**: 启动检查日志 发现nacos-config-client已经生效了,而且在读取3个默认的文件. ![image-20231023103246710](doc/imgs/image-20231023103246710.png) 1. 证明nacos-config-client 正常工作 2. 默认读取一批配置文件,虽然这个文件在nacos-config-server不存在. 名字规律: {spring.application.name} {spring.application.name}.{fileExtension} {spring.application.name}-{spring.profiles.active}.{fileExtension} **第四步:**根据默认读取的配置文件 准备远程配置文件. 准备默认读取的第一个文件 ![image-20231023103939844](doc/imgs/image-20231023103939844.png) ![image-20231023104206833](doc/imgs/image-20231023104206833.png) 结论: 将本地 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 综合练习 - 需求目标 image-20231023140841559 - 练习操作步骤 **第一步**: 依赖 (参考整合入门案例) **第二步:** 准备一个bootstrap.yaml配置 image-20231023141139055 1. 开启的环境配置名称 2. 按照格式配置的某一个环境 3. 隔离不同环境的符号 --- image-20231023141908637 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(检查对应关系和内容) ![image-20231023142722444](doc/imgs/image-20231023142722444.png) 2. mybatis.yaml ![image-20231023143638597](doc/imgs/image-20231023143638597.png) 通过案例 理解 真实场景中 能够扩展和应用的结构 ![image-20231023144313719](doc/imgs/image-20231023144313719.png) ### 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跟服务端进行请求响应交互的. image-20231023153605241 每次客户端启动 都会根据我们在应用配置文件中提供的属性(有一部分有默认值).拼接一个请求http. 向指定的nacos服务端发送这个请求. 服务接收请求之后 处理服务信息注册申请,在内存中保存一份注册信息. ![image-20231023154653739](doc/imgs/image-20231023154653739.png) ![image-20231023155925555](doc/imgs/image-20231023155925555.png) 官方网址 接口文档:https://nacos.io/zh-cn/docs/open-api.html ## 3.2 配置中心 和注册中心结构相同 CLIENT SERVER 主要客户端功能获取需要的配置 接口 ![image-20231023162608372](doc/imgs/image-20231023162608372.png) ![image-20231023162934208](doc/imgs/image-20231023162934208.png) # 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 阶段性架构图 ![image-20231024090644347](doc/imgs/image-20231024090644347.png) 纵向拆分,提出了2个比较明显的问题 1. 集群规模变大 ,相互之间 无法寻找(没有注册信息) 2. 无法实现相互调用(RPC) # 2 DUBBO框架 ## 2.1 RPC通信协议 ### 2.1.1 计算机网络通信协议 http tcp/ip udp 通信协议 相当于网络中 端到端 通信的途径 手段. ![image-20231024091437532](doc/imgs/image-20231024091437532.png) ### 2.1.2 通信序列化协议 ![image-20231024091929461](doc/imgs/image-20231024091929461.png) 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数据传递 流的关系 ![image-20231024094257940](doc/imgs/image-20231024094257940.png) 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: 服务调用者 - 服务协调器: 注册中心 image-20231024105732510 ## 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添加以下依赖关系 ![image-20231024114023517](doc/imgs/image-20231024114023517.png) - 对外暴露接口的实现 在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依赖. ![image-20231024114857925](doc/imgs/image-20231024114857925.png) - 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创建的具备底层连接对象的那个代理. ![image-20231024142117209](doc/imgs/image-20231024142117209.png) - 缺少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信息 ![image-20231024144626860](doc/imgs/image-20231024144626860.png) ### 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配置文件. ![image-20231024152907280](doc/imgs/image-20231024152907280.png) dubbo.xml 内容主要作用,就是配置dubbo的provider和consumer 代替了之前编写的dubbo api. 程序加载这个xml之后 ,会在内存中创建dubbo整合spring相关配置类. ``` ``` 配合dubbo:service引用注入的对象,实现类需要添加注解 ![image-20231024160328957](doc/imgs/image-20231024160328957.png) - web应用加载xml 在启动类中 添加一个注解@ImportResource. ![image-20231024162309404](doc/imgs/image-20231024162309404.png) - 启动springboot 在注册中心 这个web应用 整合了2个组件 nacos-client dubbo。所以会出现2个注册信息。 ![image-20231024162756996](doc/imgs/image-20231024162756996.png) #### 2.4.1.2 CONSUMER luban-demo-order - 依赖(略) 和provider一样 - 配置dubbo.xml ``` ``` - web应用加载xml luban-demo-order-main 启动类 添加@ImortResource注解 ![image-20231024165142511](doc/imgs/image-20231024165142511.png) - 业务代码注入接口对象调用方法 ![image-20231024165848530](doc/imgs/image-20231024165848530.png) ### 2.4.2 功能扩展 - 方法扩展(dubbo框架中不需要做额外配置) ![image-20231024171837082](doc/imgs/image-20231024171837082.png) ![image-20231024172016497](doc/imgs/image-20231024172016497.png) - 接口扩展 1. provider ![image-20231024172405333](doc/imgs/image-20231024172405333.png) ![image-20231024172745886](doc/imgs/image-20231024172745886.png) ![image-20231024173044859](doc/imgs/image-20231024173044859.png) 2. consumer ![image-20231024173609397](doc/imgs/image-20231024173609397.png) ![image-20231024173846516](doc/imgs/image-20231024173846516.png) # 3 dubbo总结回顾 ![image-20231024175112179](doc/imgs/image-20231024175112179.png) # 附录 # 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 ![image-20231024163156075](doc/imgs/image-20231024163156075.png) ![image-20231024163220664](doc/imgs/image-20231024163220664.png) ![image-20231024163330620](doc/imgs/image-20231024163330620.png) - 执行maven clean 在顶级父工程 执行mvn clean。重新运行。 # REDIS 哨兵集群 # 1 REDIS SENTINEL ## 1.1 REDIS结构变化 ### 1.1.1 单机版 image-20231024190519034 - 存在的问题 1. 读写瓶颈有上限(3万-5万上限,但是1万是保证稳定的运行 1万条/S) 2. **单机故障** - 应用场景 1. 开发环境 只需要走通业务逻辑,不需要关心故障,关心并发 哨兵集群 就是为了解决单机故障存在的一个结构. ## 1.2 哨兵(sentinel)集群 ### 1.2.1 主从结构 redis 支持主从结构. 在数据存储容器技术中,主从是解决单机故障的必要方案. image-20231024191703274 ### 1.2.2 演示redis主从 redis支持一主多从,支持多级主从.但是考虑同步数据成本问题,一般 1主 1从 image-20231024191934563 演示的结构 一主 二从 总结: 主从演示完毕 可以实现主从数据同步 但是,能否实现**高可用(HA)** ### 1.2.3 哨兵进程 是一种特殊的redis进程,不负责读写数据(单独启动的这个哨兵有点浪费) 主要负责监听监控主从集群 image-20231024195628020 启动哨兵集群,监听一个主从,集群哨兵中选取一个leader负责心跳检测. leader 连接主节点和从节点. - 从节点宕机: ping/pong 失败, 通知其它哨兵 连接 防止单个哨兵判断失误.最终投票做结论.如果判断结果是宕机 记录从节点宕机状态. - 主节点宕机: ping/pong失败,通知其它节点哨兵判断,如果最终投票失败,将会进行新一轮投票.从2个从节点选举一个新的节点作为主节点使用. - 回复的主节点在恢复的一瞬间 角色还是master.哨兵leader 发现回复.哨兵会通知这个主节点,你已经不是主节点了,重新发送命令挂接新的主节点 主从结构中,永远都是master做写操作. 读操作主从都能实现. 提升读的效率,总体来讲,并发和读写瓶颈还是比较高的. 无法解决高并发访问. 比如 1秒钟搜索50万条商品数据. ### 1.2.4 redis五种经典数据类型 String类型 image-20231024192740786 - set - get - incr/decr 自增和自减1 - incrby/decrby 自增自减自定义部署 - append - setnx 在不存在的key的情况下可以set数据 Hash类型 image-20231024193048176 - hset: 生成一个hash数据 - hget: 获取一个hash的field - hkeys: 拿到所有field - hvals: 拿到所有value - hincrby: 对某一个field数字类型数据 做增加或者减少 - hmset: 批量写 - hmget: 批量读 List image-20231024193518867 - lpush/rpush - lrange - rpop/lpop - lset - linsert Set image-20231024193656973 - sadd - srem - scard - sismember ZSet image-20231024193925187 - zadd - zcard - zrank - zrange - zrangebyscore # 1 DUBBO ## 1.1 阶段性结构 image-20231025090458191 ## 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一般都是一个多实例的集群. image-20231025101016621 **负载**: 访问就是负载.基本可以理解为流量 和访问 请求 **均衡**: 按照逻辑 进行不同算法的分配负载. - 负载均衡的算法 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注册信息 ![image-20231025104158611](doc/imgs/image-20231025104158611.png) 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... image-20231025105449295 ### 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约束头 ![image-20231025113743273](doc/imgs/image-20231025113743273.png) ``` ``` **第二步**: 配置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(); } } ``` **第二步:** 修改导入方式(代码略) image-20231025150154537 ### 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 image-20231025152528513 **第二步:** 使用注解读取文件 在加载的配置类中 使用这个@PropertySource注解 image-20231025152836027 ## 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入口配置类 扫描了新的配置类所在包 ![image-20231025154651442](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202310260858755.png) 新的条件配置类 是否加载取决于条件逻辑 返回是否是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. image-20231026091658093 提供自动配置类的依赖资源 必定需要满足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文件 按照上述的格式,填写内容. image-20231026093406744 通过这个案例 检测是否可以将当前环境 所有 spring.factories一起读取 image-20231026093507977 上述代码流程逻辑 能够实现 第三方完成spring-boot-starter 一般这种第三方名称**-spring-boot-starter. 例如: mybatis-spring-boot-starter 一定包含两部分内容 自动配置类 META-INF/spring.factories ![image-20231026095504966](doc/imgs/image-20231026095504966.png) ### 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 ![image-20231026102942353](doc/imgs/image-20231026102942353.png) 文件夹 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 阶段性架构 ![image-20231026104903355](doc/imgs/image-20231026104903355.png) nacos管理治理集群实例 dubbo负责集群内部的相互调用 问题: 外部调用怎么进入? ![image-20231026105558659](doc/imgs/image-20231026105558659.png) ## 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作为转发测试的目标实例. image-20231026114136783 ### 2.3.2 实现网关转发案例 - 单独创建一个网关工程-luban-demo-gateway ![image-20231026114450186](doc/imgs/image-20231026114450186.png) - 提供项目的依赖 所有依赖包括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结构 ![image-20231026143238973](doc/imgs/image-20231026143238973.png) 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 image-20231026153436903 - 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 网关运行原理 ![image-20231026173326344](doc/imgs/image-20231026173326344.png) 1. 请求进入网关 2. 映射器,通过请求各种条件 计算匹配路由配置的断言 找到路由 如果映射器找不到处理的路由 直接返回404. 3. 找到路由之后,会交给handler处理器 传给后面的过滤链。 4. 过滤链既包含网关内置的过滤器 也可以自定义添加**过滤器**. 5. 最终通过过滤器的各种计算 将访问后端的结果交给一个代理 6. proxy代理发送调用后端服务响应返回 问题: 有匹配映射,有路由过滤器,其它过滤器有哪些? image-20231026174447280 http://localhost:30000/stock/doc.html ![image-20231026174200656](doc/imgs/image-20231026174200656.png) 拼接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负载均衡原理图 ![image-20231027091333263](doc/imgs/image-20231027091333263.png) 整个流程原理和 单纯的路由转发 区别在于: 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安全框架 典型的过滤器和拦截器应用框架. image-20231027103532387 过滤器特点: - 过滤器各司其职 - 顺序执行需要符合业务逻辑(粗粒度过滤在前 细粒度过滤在后) - 特殊的情况,过滤器可以实现拦截的效果(操作响应返回) ### 1.4.2 过滤器入门案例 目标: 让网关中 一个过滤器代码生效,所有请求都会进入这个过滤器. - 创建过滤器类,并且实现网关固定接口 - 让过滤器类 成为spring容器bean对象 当前案例过滤器的执行位置 ![image-20231027105406116](doc/imgs/image-20231027105406116.png) ```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 ![image-20231027145624552](doc/imgs/image-20231027145624552.png) # 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 架构 ![image-20231030090352169](doc/imgs/image-20231030090352169.png) ## 1.2 网关回顾 ### 1.2.1 网关作用 - 微服务集群统一入口 - 可以实现路由 监控 流量控制等 ### 1.2.2 网关实现功能 - 请求转发: 固定访问后端一个服务实例 - 路由负载均衡:结合nacos客户端抓取的注册信息,实现负载均衡 - 过滤器: 网关有内置过滤器(通过配置yaml属性影响使用的内置过滤器). 自定义过滤器. ### 1.2.3 尚存的微服务架构问题 - 流量控制: - 外部请求: - 内部请求: - 调用失败处理: # 2 Sentinel熔断限流 ## 2.1 熔断/限流/降级 ### 2.1.1 熔断 微服务架构中,切分的服务越多,调用关系就越复杂. 如果在多个服务调用过程中,由于某个服务的实例故障,导致调用失败,延迟,等待,重试. **需要不需要立刻解决这种调用失败的问题,需不需要对故障的实例做额外的处理** image-20231030091900387 如果不能及时处理这个问题,最终故障的服务实例也会被nacos剔除,或者记录健康状态.总是存在这种状态达到一段时间.这段时间 就有可能导致A服务中所有实例,访问B服务这个故障节点等待排队.访问的压力 调用的压力就积压在这里.积累到一定程度 A服务也有不可访问不可使用的风险. image-20231030092427309 这种问题 采用一种新的机制来解决--**熔断** **熔断思想**: 牺牲局部 保存全局 (保险丝) 在熔断过程中,微服务中的落地实现方案,一般是需要引入熔断组件(sentinel). 熔断组件实现牺牲局部保存全局的熔断逻辑 sentinel 中存在一个断路器 断路器 会根据调用的资源的反馈 决定断路器中的状态: **闭合 断开 半开(有些类似开关)** - 闭合状态 正常调用 image-20231030093344271 - 打开状态: 根据设定的判断条件 打开断路器因为 异常 调用慢等原因.一旦打开 代码将不会在调用这个资源 image-20231030093506240 - 半开: 当打开状态 持续了一段时间,要断路器半开连接故障资源,进行一次检测. image-20231030093734551 访问检测成功,半开的状态 切换到闭合,后续继续正常调用. 访问检测失败,半开的状态 切换会打开,禁止调用. 在业务访问,代码调用过程中,并不是所有的出问题的调用资源 都需要进行熔断. 要保证这个系统基本可用性. **通过熔断可以保证系统 核心功能是基本可用的**. 例如: 抖音刷视频.核心数据是视频播放流畅. 评价数据可有可无. ### 2.1.2 限流 重点内容: - [ ] 理解限流的概念和作用 - [ ] 掌握一些流量,压测数据指标的概念,和公式.避免面试时出现不合理的回答 - 前提: 服务器运行,每个服务器无论硬件还是软件都存在承受访问**并发的上限**. - 目的: 限流的目的,防止服务接收超过上限的**请求**,导致崩溃. **介绍一些基础的概念**: 压测数据指标,流量. image-20231030095001007 1. RT: reaction time 表示一次请求服务器到响应回到客户端的时长(服务实例的计算时间 和网络往返时间); image-20231030095115092给一些常规的数值. 访问一个由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小一些. ![image-20231030102532210](doc/imgs/image-20231030102532210.png) 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 降级 访问资源无论是熔断 还是 限流,总要有一个可以使用的数据返回. **思路**: 在出现熔断(断路器打开),限流的情况前提之下,退而求其次的获取可用数据. image-20231030112247740 ## 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 image-20231030113410900 - 添加依赖资源 luban-sentinel-study添加依赖 主要是 让子工程同一使用. ```xml org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` - 准备demo子项目 sentinel使用的方式 有区别.通过不同的demo 逐步从资源 开始 到规则进行学习和编码. image-20231030114135727 - 准备代码的基础案例 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底层原理 ![image-20231030145034032](doc/imgs/image-20231030145034032.png) - 每一个entry赋值,都要在内存经过一条责任链(sentinel组件提供) - 在责任链中 主要经过2种不同的计算逻辑. - 第一批是固定的计算 nodeselect clusterbuilder statistic - 第二批对比自定义的规则 如果违反了,抛出异常 没有违反直接向后调用 - 从sentinel责任链中生成赋值entry 才能继续调用保护起来的资源. NodeSelectSlot: 负责计算 entry树结构的. ClusterBuilderSlot: 集群节点中 构建相互连接的. StatisticSlot: 统计,能统计当前请求进入的各种数据 比如 并发数,qps,等待时间,异常数等等. NodeSelectSlot 管理的entry结构. 一个程序中可以存在多个entry. 在NodeSelectSlot中记录了一个内存的树状结构. image-20231030145929067 通过这样的树结构, sentinel对资源做计算时,可以清晰的知道 整条调用过程. 从哪个Entry进入的,经过了哪些entry,邻居有谁?.. ### 2.2.7 数据源配置规则 sentinel的规则数据,可以通过不同的数据源存储,比如 file文件 db数据库 nacos注册中心配置文件. 不同的数据源,实现是不同的. 可以自定义数据源实现类. #### 2.2.7.1 JDK 的SPI机制 API和SPI的区别: API(application programing interface) 即应用程序接口,服务方提供接口和实现,给出接口文档供调用方调用,调用方需要遵循服务方的接口文档规范。 image-20231030152510221 SPI(service provider interface) 即服务提供方接口,服务方提供接口,定义好接口参数、返回等规范,但是实现交给调用方,相当于call back的思想。最终通过SPI机制 让调用方可以加载调用自己实现的代码.(**给提供方提供一个可以发现实现类的方案**) ![image-20231030152813388](doc/imgs/image-20231030152813388.png) - 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 文件里的值,就是调用方对接口的实现. image-20231030153654249 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工程 image-20231030154951060 - 准备好和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 image-20231030162419484 - 文件内容文本 是自定义的 MyDatasource的全路径名称. image-20231030162610333 - 使用硬编码的方式 在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 } ] ``` image-20231030173326782 - 改造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 image-20231031090953650 ### 1.1.2 准备基础代码 HelloController-->HelloService image-20231031091312907 ### 1.1.3 nacos数据源原理 image-20231031091714668 ### 1.1.4 添加nacos数据源依赖 ```xml com.alibaba.csp sentinel-datasource-nacos ``` ### 1.1.5 本地flowRules.json迁移到nacos配置中心 public 命名空间 DEFAULT_GROUP分组创建文件 image-20231031092422349 ### 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. image-20231031094423585 ``` 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切面里了. ![image-20231031100829772](doc/imgs/image-20231031100829772.png) ### 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 ![image-20231031102437948](doc/imgs/image-20231031102437948.png) ```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属性 将降级方法迁移到一个指定类. image-20231031103349549 然后在项目中准备这个类HelloServiceBlockHandler image-20231031103456381 按照图片配置这个指定类 ![image-20231031104020662](doc/imgs/image-20231031104020662.png) **结论** blockHandler和blockHandlerClass 生效的时候 目标方法调用还是没调用? 没有调用. 通过blockHandler和blockHandlerClass的使用,延伸学习fallback fallbackClass. image-20231031104726241 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,所有资源都需要给下单让路. image-20231031140813846 2 链路 limitApp 指定的资源,查看当前资源的链路是否是通过limitApp进入的,如果不是,不限制流量,如果是则限制流量.(限制的是来源) ![image-20231031141432718](doc/imgs/image-20231031141432718.png) - 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 image-20231031151207322 - 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方法 ![image-20231031151819521](doc/imgs/image-20231031151819521.png) - 熔断效果 根据当前规则设置 至少在10秒钟只能访问7次 才能触发熔断规则(**断路器-断开**). 资源被熔断限制访问长达10秒(**断路器的断开状态持续时间**). 10秒钟之后,允许访问1次(**断路器是半开**),但是第二次继续按照熔断处理(**断路器从半开 切换回断开**). image-20231031152528945 ### 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 } ] ``` ![image-20231031161422480](doc/imgs/image-20231031161422480.png) ![image-20231031161714105](doc/imgs/image-20231031161714105.png) ### 1.4.6 案例问题解析 没有违反 慢调用比例的规则. 没满足 最小请求数量10. # 2 面试真题 背景: 某位同学面试复试的问题,一道题 题目: 一个老师 教一个班级的学生们四门课.数学,英语,音乐和自然. 要求对于在上这些课程的学生们满足以下条件: 1. 每节课只有3个学生 2. 每个班任意每两个学生至少一起上一门课程 要求: 编写一段代码程序: 计算该班最多可以有多少学生并生成所有符合上述条件的分组可能.(可以只提供解题思路) # 3 消息队列-rocketmq ## 3.1 rocketmq环境 目标: - [ ] 成功启动rocketmq的2个核心进程 nameserver broker - [ ] 最好能够启动第三个进程,辅助学习的进程 rocketmq的 仪表盘dashboard ### 3.1.1 找到压缩包解压 解压到没有中文,没有空格的路径中 ![image-20231007153300141](doc/imgs/image-20231007153300141.png) ![image-20231031172202489](doc/imgs/image-20231031172202489.png) ### 3.1.2 配置环境变量ROCKETMQ_HOME 环境变量名称ROCKETMQ_HOME 环境变量的值 上边截图的家目录 路径 配置结束之后,重新打开一个CMD验证 ```shell echo %ROCKETMQ_HOME% ``` 此电脑-->属性-->高级系统设置-->高级标签-->环境变量 image-20231031172815473 ### 3.1.3 确定是否存在JAVA_HOME环境变量并且版本JDK1.8 ![image-20231031172922667](doc/imgs/image-20231031172922667.png) ### 3.1.4 启动ROCKETMQ核心2个进程 在家目录有bin文件夹(binarry缩写,一般存放脚本命令文件) 使用cmd进入到这个文件夹. ![image-20231031173116338](doc/imgs/image-20231031173116338.png) - 启动nameserver(类似nacos) 1. windows ```shell bin>mqnamesrv.cmd ``` ![image-20231031173219010](doc/imgs/image-20231031173219010.png) 2. mac 下面2个命令 执行方式都试试. ```shell bin>mqnamesrv ``` ```shell bin>./mqnamesrv ``` - 启动broker 在打开一个cmd 在bin目录中执行命令脚本 1. windows(localhost:9876就是namesrv) ```shell bin>mqbroker.cmd -n localhost:9876 ``` ![image-20231031173805592](doc/imgs/image-20231031173805592.png) 2. mac ```shell bin>mqbroker -n localhost:9876 ``` ```shell bin>./mqbroker -n localhost:9876 ``` ### 3.1.5 启动辅助工具--仪表盘 利用这个辅助工具 检查观察rocketmq中一些数据展示页面 图形等. - 找到jar包 image-20231031174132466 - jar放到没有中文没有空格的文件夹 image-20231031174225514 - 在jar包路径中打开cmd运行java命令 ![image-20231031174314675](doc/imgs/image-20231031174314675.png) ```shell java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar --server.port=9999 --rocketmq.config.namesrvAddr=localhost:9876 ``` ![image-20231031174435783](doc/imgs/image-20231031174435783.png) - 访问对应页面 http://localhost:9999 ![image-20231031174615415](doc/imgs/image-20231031174615415.png) # 1 Rocketmq ## 1.1 启动进程 - mqnamesrv.cmd (nameserver) - mqbroker.cmd (broker) - 启动dashboard.jar包 (仪表盘) ## 1.2 入门案例 ### 1.2.1 消息队列介绍 RocketMQ是消息队列的一种. MQ:消息队列 message queue,是一种可以进行**储存**消息,**发送**消息,**消费**消息的队列中间件. **Message指的不是字符串**,而是一个消息对象数据. - 基本结构 ![image-20231101091302328](doc/imgs/image-20231101091302328.png) producer: 开发编写的代码 生成消息对象数据 发送消息 consumer: 开发编写的代码 接收消息对象 消息消费 队列: 负责消息的存储 - 类似的软件 rabbitmq: 分布式支持的不是特别好.吞吐不大的时候可以使用. kafka: 大数据领域使用,负责数据收集通道. JMS: java 中的队列存储 性能一般 RocketMQ: 仿照kafka的结构 支持高吞吐 支持分布式 - Rocketmq 官方网址: https://rocketmq.apache.org/ ![image-20231007170443996](doc/imgs/image-20231007170443996.png) ### 1.2.2 案例代码 需求: 实现一个入门级的消息生产和消息消费逻辑. 一发一接 - 创建一个测试rocketmq工程 image-20231101092341692 - 添加相关依赖 ```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); } } ``` 错误: ![image-20231101191428936](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202311011914513.png) 解决: ![image-20231101191522529](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202311011915577.png) ​ ![image-20231101191528804](https://gitee.com/dengxing1277/blogimage/raw/master/imgs/202311011915136.png) 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 ![image-20231101110529029](doc/imgs/image-20231101110529029.png) 谁来**发现**: 代码客户端(producer consumer). 代码启动之后会建立和nameserver连接.定时更新抓取的.根据抓取的信息,代码客户端(4.X)需要计算负载均衡,某个消息发给谁,当前消费绑定谁都是固定计算结果. ![image-20231101111637646](doc/imgs/image-20231101111637646.png) ## 2.2 broker **Broker主要负责消息的存储、投递和查询以及服务高可用保证。** **高可用保证:** broker支持Master-slave结构的.Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。 ![image-20231101112256150](doc/imgs/image-20231101112256150.png) **高并发高吞吐保证** ![image-20231101112603889](doc/imgs/image-20231101112603889.png) 上述架构中唯一的问题:nameserver 单机故障问题 ![image-20231101113024202](doc/imgs/image-20231101113024202.png) 了解了nameserver和broker之后 rocketmq的运行架构 基本结束. 问题: broker负责存储消息,broker存储数据结构 什么样. ## 2.3 主题 Topic 一个rocketmq的集群中可以有多个主题. ![image-20231101113340903](doc/imgs/image-20231101113340903.png) 同一个主题,表示的是同一类消息的集合. 例如: - order-topic: 存储的都是 订单消息 - cart-topic: 存储的都是购物车消息 主题最终由broker管理的,在注册的时候,在nameserver中注册主题的**路由关系**(某个主题 在某个broker管理) image-20231101113902541 主题创建之后 就存储在对应的broker节点,随着注册 在nameserver保存broker详细地址和主题的路由信息.生产者或者消费者 从nameserver拿到注册信息,计算完成才实现消息发送和消费监听. 分布式结构的主题和broker的关系 ![image-20231101114331551](doc/imgs/image-20231101114331551.png) 问题: 为什么同一个主题可以分散到不同的broker存储呢? ## 2.4 队列queue 存储消息的物理实体(最小单位)。一个Topic中可以包含**多个Queue**(分布式体现的关键),每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(partition). - 单机broker ![image-20231101114901222](doc/imgs/image-20231101114901222.png) - 分布式brokers ![image-20231101115141463](doc/imgs/image-20231101115141463.png) 消息发送的整体流程: 1. 创建生成主题.broker会将主题和队列的路由信息 注册在nameserver 2. producer 生产者抓取注册信息,获取目标主题和broker和队列的所有信息. 3. producer组织消息数据,进行负载均衡计算,计算确切结果输出. 4. 创建连接 连接到具体broker将消息发送. ## 2.5 生产者组 在微服务 分布式高可用集群中(微服务实例非常多的). producer生产者的角色也很多.RocketMQ为了方便管理,将每一个生产者都列入一个生产者组. 生产者---1:n---生产者组. ![image-20231101140445410](doc/imgs/image-20231101140445410.png) 从技术角度讲: 一个生产者组可以向多个主题发送消息,同一个主题也可以接收多个不同生产者组发送的消息. 从业务角度: 只要能保证发送给不同主题的消息,是同一种类型的消息,就可以组合.否则会出现消费失败,消费故障. (一般情况下 一个分组的生产者 只给一个主题发送消息,如果需要发送给多个主题,多创建一个生产者分组). ## 2.6 消费者组 和生产者一样,消费者也必须要指定分组. 消费者进程通过连接nameserver获取注册信息,根据主题中的队列个数,根据主题中记录的消费者组的信息.确定直接绑定的队列. - 只有一个消费者 ![image-20231101141249674](doc/imgs/image-20231101141249674.png) - 一个分组有多个消费者 多个消费者进程会将主题中的所有的队列平均分配. ![image-20231101142147639](doc/imgs/image-20231101142147639.png) 所以消费者进程和生产者进程个数 不一样(没关系),生产者个数不受队列的约束,但是消费者最大数量就是这个主题中的队列数量. 问题: 如果多个消费者分组 监听同一个主题,会发生什么事? ## 2.7 消费位点 - 每一个在队列中存储的消息,rocketmq主题都会记录偏移量. ![image-20231101142940798](doc/imgs/image-20231101142940798.png) ![image-20231101143029062](doc/imgs/image-20231101143029062.png) image-20231101143354790 - 主题针对同一个分组.还会记录消费位点(消费者消费了多少条消息,消费偏移量) ![image-20231101143521777](doc/imgs/image-20231101143521777.png) image-20231101143849661 image-20231101144045457 - 同一个消费者组的消费者共享消费偏移|位点 ![image-20231101144322649](doc/imgs/image-20231101144322649.png) - 不同消费者组消费位点记录 相互隔离 ![image-20231101144457380](doc/imgs/image-20231101144457380.png) ## 2.8 keys和tags - KEYS 每个消息,keys绑定到消息的一个**业务数据**,用来做查询统计使用的. 例如: 组织一个订单消息,可以绑定过一个key值 可以是消息编码orderSn. 如果没有这个key值,想要查询一条消息,需要使用消息的id,而消息id是rocketmq自动生成的没有任何业务含义. - tags 每个消息允许绑定标签tags. 可以在消费端进程中使用标签进行过滤. ![image-20231101152248085](doc/imgs/image-20231101152248085.png) ## 2.9 系统作业 Rocketmq 无论是发送消息还是监听消费消息,还有很多不同的场景可以尝试编写案例. rocketmq还支持很多消息发送和消费的特性.比如: push消费(已做,关注消费速度) pull消费(主动拉取消息,不拉取,消息不会达到消费者进程. 速度处理慢) 生产者 发送顺序消息 延迟消息等等. 系统作业要求: 请阅读官方相关文档,将其它的生产者案例代码和消费者案例代码,自行测试一遍. image-20231101153308041 # 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 延迟时间不断增加 image-20231101170412490 - 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 image-20231101174207456 image-20231101174348244 ## 2.3 修改runbroker.cmd|runbroker.sh ![image-20231101174527086](doc/imgs/image-20231101174527086.png) ![image-20231101174704310](doc/imgs/image-20231101174704310.png) # 附录 # 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对象 ![image-20231102092655297](doc/imgs/image-20231102092655297.png) ### 2.1.1 异步解耦 将同步的代码功能 变成异步的调用,形成解耦. ![image-20231102093336989](doc/imgs/image-20231102093336989.png) ### 2.1.2 削峰填谷 由于访问的处理 大部分是同步处理,可以处理的并发量取决于 线程 服务器性能. 为了提高服务器响应效率,会尽可能引入提升性能的软件技术,比如redis. 削峰填谷,本质上也是可以尽快处理请求的一种方式. 处理高并发时,使用队列,做削峰(超出服务器处理能力的请求 放到队列)填谷(延迟一段时间处理这些超出的请求). ## 2.2 消息队列的业务应用 ### 2.2.1 需求(削峰填谷) 如果下单操作 是同步处理的. 进程没有处理完业务流程,就不会有相应. 通过引入消息队列实现**削峰填谷**,提升系统的并发访问能力. image-20231102100743276 引入消息队列.让系统资源暂时没有空闲处理能力的时候,业务在队列中排队等待. ![image-20231102101205898](doc/imgs/image-20231102101205898.png) ### 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 **异步落盘** ​ 同步落盘|同步刷盘|异步落盘|异步刷盘 ​ image-20231102112404211 异步落盘 指的是,在rocketmq接收到生产者的消息,开始执行持久化,但是持久化的结果还没有结束,就将成功接收消息的反馈返回给生产者,所以只有异步落盘,**是不能保证消息持久化结果的**.但是速度快. image-20231102112728312 只要rocketmq没有持久化这条消息成功,就不会反馈给生产者 成功的信息.同步落盘能保证消息存储的可靠性. 但是单独的broker处理同步落盘 依然存在单机故障. 必定概要引入ms主从. image-20231102113222169 主节点也是同步落盘,从节点也是同步落盘,这种架构可以最高级别的保证消息存储不丢失.因为生产者发送的消息,只要有一个节点没有完成持久化,生产者都不会接受到反馈成功的信息. 这种架构会影响rocketmq的吞吐量.所以平衡问题,既能保证吞吐量,又能保证一定的可靠性. 最终采用 **master做同步落盘 slave异步落盘** image-20231102113445171 ### 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集群做了扩容,队列做了扩容 image-20231102141157514 2. 没有在扩容之前正确返回消费成功的信息这些消息,会在重平衡后 重新下发投递 image-20231102141601919 出现重平衡的时候,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 新增订单幂等的业务逻辑 ![image-20231102150358598](doc/imgs/image-20231102150358598.png) 添加订单业务的 查询功能. ```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 调用set方法 set("name","王老师") set ^&#&vds78dsname #*%^&$F&R*&%^&$*& StringRedisTempalte incr orderSn ```java package com.tarena.demo.luban.all.main.controller; import com.tarena.demo.luban.all.main.service.OrderService; import com.tarena.demo.luban.commons.exception.BusinessDemoException; import com.tarena.demo.luban.commons.restful.JsonResult; import com.tarena.demo.luban.commons.restful.ResponseCode; 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.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; 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; @Autowired private StringRedisTemplate redisTemplate; @PostMapping("/add") @ApiOperation("新增订单的功能") public JsonResult addOrder(OrderAddParam orderAddParam){ //判断当前请求是否重复提交 orderSn=5 利用redis incr orderSn //incr 命令属于String类型的命令 hget 命令属于hash类型命令 ValueOperations opsForValue = redisTemplate.opsForValue(); String orderKey="order:sn:"+orderAddParam.getOrderSn(); Long increment = opsForValue.increment(orderKey); if (increment>1){ return JsonResult.failed(new BusinessDemoException(ResponseCode.BAD_REQUEST,"重复提交订单")); } //组织精简准确的消息对象 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("新增订单完成!"); } } ``` # 3 总结 核心思路: rocketmq的应用场景, 应用的问题解决 - 消息队列应用: - 异步解耦: 把同步执行的代码利用队列发送消息消费消息 变成异步的关系. - 削峰填谷: 提升系统 高并发场景处理请求的上限. - 应用场景: - 新增订单: - 同步新增订单: - 好处: 发送下单的客户端 知道订单是否新增成功. - 坏处: 时间执行比较长,无法应对高并发场景. - 异步下单: - 好处: 提升系统性能 降低下单的RT时间,增加并发量. - 坏处: 和前端交互逻辑需要变化,否则无法让用户支付订单. - 面试问题: - 消息丢失 - 在哪丢失,在哪解决,怎么解决 - 重复消费 - 本质原因: rocketmq投递消息机制 at least once - 直接原因: - retry机制(理解) - rebalance重平衡(了解): 概率极低 - 方法幂等涉及: 1次调用和N次调用结果一样. ## 3.1 课堂练习 ### 3.1.1 删除购物车的异步解耦 减库存 增订单 (**订单服务**)发送一条删除购物车消息: 不在和减库存和增订单同步执行在一个方法. (**购物车服务消费者**)监听消息 删除消息中指定的购物车数据. ### 3.1.2 将今日课上的功能实现逻辑 - 下单 从同步变成异步 - 业务方法新增订单 幂等设计(防止消息重复消费) - 提交订单接口方法 幂等设计(防止用户重复提交) # 1 练习讲解 ## 1.1 题目描述 将 减库存 生成订单 删除购物车中 最后一步删除购物车 异步解耦. ## 1.2 异步解耦结构 同步结构 image-20231103090542686 异步调用 image-20231103090810539 # 2 Redis分布式锁 ## 2.1 分析业务代码 重复消费-->方法幂等-->新增订单做幂等涉及. image-20231103091753627 存在线程并发安全的隐患(概率极低):满足条件 1. 多个线程同时调用addOrder方法生成同一张订单orderSn相同 2. 同时查询数据库,结果是0 3. 同时执行减库存和新增订单 回顾线程并发安全问题影响因素: - 多线程并发并行: 多个消费者进程 - 写操作: 新增订单 删除购物车 生成订单新增 更新库存 - 共享数据: 同时执行新增订单 针对同一个orderSn image-20231103092639053 ## 2.2 解决方案 如果放任不管,出现的结果: **设计的addOrder幂等无效了. 会出现同一个订单 减库存多次,生成多张一样的订单.** 解决方案1: - 减库存设计成幂等: 依然存在线程安全问题. 减库存先查,后改. 解决方案2: - addOrder方法上添加synchronized同步锁. image-20231103094142037 只能锁住同一个进程中多线程执行调用 串行顺序,无法决定 多个进程的集群中,最终出现同时访问数据库的情况. 解决方案3: redis分布式锁(互斥锁,排它锁) ## 2.3 分布式锁设计思路 ### 2.3.1 Redis的setnx命令 Redis中可以使用String类型的命令: - set: 写数据 覆盖数据 - get: 读数据 - incr/decr: 自增 自减 一个数字的redis字符串纯数字数据 - **setnx**: 写数据,有前提key值不存在,才能写成功 多个客户端同时调用setnx 写入同一个key值,只有一个人能够写成功,其它人都失败. 我们可以利用这个命令在代码中 实现抢锁的逻辑. 谁(线程客户端) 能够成功的写入这个数据,谁就抢到了可以执行资源的**锁** ### 2.3.2 工具--业务流程图 开发代码之前,尤其是业务逻辑,一般先要画好业务流程图,如果没有任何前提,业务存在,代码是不可能短时间高效完成的. 开始和结束: 在一个合理的业务流程图中,必须存在开始和结束,一般开始有一个,结束可有很多. ![image-20231103101529223](doc/imgs/image-20231103101529223.png) 流程步骤: 开始之后,要经历的所有流程步骤. ![image-20231103101613437](doc/imgs/image-20231103101613437.png) 判断条件: 在流程向下完成的某个步骤,使用判断产生分支. ![image-20231103101722147](doc/imgs/image-20231103101722147.png) ### 2.3.3 分布式抢锁业务流程 抢锁流程核心原则: 想要执行addOrder新增订单的消费线程.必须要先抢锁.抢到锁的按照抢到的逻辑执行 没抢到锁的按照抢失败的逻辑执行. - 设计方案version1 image-20231103102544070 上述流程,可能存在的问题是 当前线程对于没抢到锁的逻辑 处理比较**消极**. 可以使当前抢锁线程在没有抢到锁的时候 持续抢锁. - 设计方案version2 ![image-20231103103203154](doc/imgs/image-20231103103203154.png) 消费逻辑的线程 应该为自己拿到的消息负责.想办法执行业务方法.所以没抢到锁的线程中,等待5秒钟,重新抢锁. 有当前线程第一次抢锁没有成功,说明别的线程已经set这个key完成了,当前线程再次等待5秒钟,抢锁奕然会失败.不断在等待5秒--抢锁失败循环,就出现**死锁**的问题 - 设计方案version3 ![image-20231103104116367](doc/imgs/image-20231103104116367.png) 手动释放锁的隐患问题,是当出现网络波动的时候,手动释放未必成功. 依然出现死锁. - 设计方案version4 image-20231103104451228 同时提供手动释放和 自动释放,手动释放是业务流程,自动释放是防止死锁的保底方案. 当前版本业务流程 还有一个问题就是 释放的锁未必是当前线程 抢到的锁. ![image-20231103111025529](doc/imgs/image-20231103111025529.png) - 设计方案version5 ![image-20231103111538966](doc/imgs/image-20231103111538966.png) 在抢锁的同时 将当前线程生成的一个随机数作为value值 set到redis保存.手动释放锁的时候,读取redis中的value值和当前线程生成的rand随机值做比较判断,只有相等的时候才释放锁. ### 2.3.4 实现分布式锁 - 环境要求 - 具备redis代码的开发能力的. - 代码落地 - 按照上述设计的流程图完成代码功能 ```java package com.tarena.demo.luban.all.main.consumer; import com.tarena.demo.luban.all.main.service.OrderService; import com.tarena.demo.luban.commons.exception.BusinessDemoException; import com.tarena.demo.luban.commons.restful.ResponseCode; 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.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.Random; import java.util.concurrent.TimeUnit; @Component @RocketMQMessageListener( topic = "order-add-topic", consumerGroup = "${rocketmq.consumer.group}") public class OrderAddConsumer implements RocketMQListener { @Autowired private OrderService orderService; @Autowired private StringRedisTemplate redisTemplate; @Override public void onMessage(OrderAddParam message) { //抢锁的所有参数都准备好 lockKey 同一个订单orderSn 锁相同的 randCode String lockKey="order:add:"+message.getOrderSn()+".lock"; String randCode=new Random().nextInt(9000)+1000+""; //使用String类型操作对象 调用setnx命令 ValueOperations opsForValue = redisTemplate.opsForValue(); //抢锁 setnx {lockKey} {randCode} EX 10 Boolean tryLock = opsForValue.setIfAbsent(lockKey,randCode,10,TimeUnit.SECONDS); try{ while(!tryLock){ //只要tryLock是false 就是没抢到 等待5秒继续抢 //TODO 可以在while中添加一些代码逻辑细节,比如: 最多允许一个线程抢三次 //TODO 每次抢都比之前少等一会,让等待次数多的人更可能抢到锁 Thread.sleep(5000); tryLock = opsForValue.setIfAbsent(lockKey,randCode,10,TimeUnit.SECONDS); } //抢到锁 执行业务逻辑 orderService.addOrder(message); }catch (BusinessDemoException e){ //记录打印日志 结束 }catch (Throwable e){ //连接数据库 dubbo调用等问题 throw new BusinessDemoException(ResponseCode.BAD_REQUEST,"系统异常"); }finally { //先读取 锁的value String getValue=opsForValue.get(lockKey); if (getValue==randCode){ redisTemplate.delete(lockKey); } } } } ``` ### 2.3.5 redis缓存面试题(鲁班项目缓存方案) - 缓存方案 - 缓存雪崩 - 缓存穿透 - 缓存击穿 - 缓存一致性 # 3 鲁班项目环境准备 ## 3.1 克隆项目 luban-admin-front: 前端管理平台(运行) luban-front: 手机APP前端(运行) luban-project: image-20231103140700802 ## 3.2 maven私服 当前idea环境使用的maven插件 所加载的settings.xml 必须是连接达内私服的xml. ![image-20231103141018894](doc/imgs/image-20231103141018894.png) ![image-20231103141134810](doc/imgs/image-20231103141134810.png) ## 3.3 前端运行 两个前端项目,保证可以正常启动运行 访问没有功能页面. - 确定node npm版本 node.js版本16.X npm 8.X 不会有太大问题的. image-20231103141441222 - luban-admin-front 1. 安装依赖资源 npm ```shell npm install ``` 2. 运行dev环境 ```shell npm run dev ``` 3. 访问地址9096 ![image-20231103141752204](doc/imgs/image-20231103141752204.png) 4. npm出现install问题 ![image-20231103142055895](doc/imgs/image-20231103142055895.png) 将这个压缩包中的内容 保证一定要解压到该项目家目录 node_modules文件夹. ![image-20231103142956796](doc/imgs/image-20231103142956796.png) - luban-front 1. 下载依赖 参考npm命令 luban-admin-front ```shell npm install ``` 2. 运行启动 ```shell npm run dev:h5 ``` 3. 访问8989 ![image-20231103142446570](doc/imgs/image-20231103142446570.png) ## 3.4 后端检查maven编译 打开luban-project 在自定义的maven命令窗口 运行maven命令. ```shell mvn clean compile -X ``` ![image-20231103144012968](doc/imgs/image-20231103144012968.png) ## 3.5 导入sql文件 必要导入的sql文件是7个 image-20231103144546707 ## 3.6 登录注册 登录注册功能,直接实现的jar打包.可以在鲁班开发过程直接运行jar包 - passport.jar包位置 image-20231103144852217 配置idea运行passport jar包的启动项.要求passport 端口 必须是8099,和前端直接对应兼容的,如果不运行在8099需要修改前端项目配置文件. - 配置启动项启动passport的jar包 1. 打开idea启动配置界面 ![image-20231103145123496](doc/imgs/image-20231103145123496.png) 2. 添加一个shell script ![image-20231103145307376](doc/imgs/image-20231103145307376.png) 3. 配置脚本命令 加载java命令,启动一个jar包. 使用一些选项将源码application.yaml文件属性替换掉. version1 ```java java -jar passport-provider-1.0-SNAPSHOT.jar --spring.profiles.active=local --server.port=8099 --spring.datasource.username=root --spring.datasource.password=root ``` 问题: localhost:3306 url地址不一样怎么办. ![image-20231103150047023](doc/imgs/image-20231103150047023.png) version2: jar启动的时候 占用内存更小 ```shell java -jar passport-provider-1.0-SNAPSHOT.jar -Xmx128M -Xms128M -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=128M --server.port=8099 --spring.profiles.active=local --spring.datasource.username=root --spring.datasource.password=root ``` -Xmx: 最大堆内存 -Xms: 最小堆内存 -XX:MaxMetaspaceSize: 最大的元数据内存空间(.class,.xml,.properties,.yml) -XX:MetaspaceSize: 最小元数据内存空间 # 附录 # 1 maven版本高于3.8.1的问题 ## 1.1 原因 3.8.1开始 对于访问私服http协议进行了拦截,所以如果版本高于3.8.1会造成达内私服的拦截 ## 1.2 解决方法 - 找到拦截的settings设置 ![image-20231103154701204](doc/imgs/image-20231103154701204.png) - 打开文件删除mirror 将global的settings文件打开,高于3.8.1的版本中会存在一个mirror拦截 将其删除,重启idea 重新编译项目. 具体删除的mirror内容如图所示: ![image-20231103154810193](doc/imgs/image-20231103154810193.png) # 1 问题解析 ## 1.1 MAVEN版本过高问题 ### 1.1.1 现象 - maven使用idea插件 FILE-SETTINGS-搜索maven查看到的. image-20231104090432479 - 拦截配置在了idea中一个全局settings.xml ${user.home}/.m2/settings.xml 相当于是用户的settings配置. idea存在一个 global settings. 只要找到这个文件修改拦截. - 从maven输出的**日志**中搜索global settings image-20231104091027991 ctrl + f image-20231104091301050 - 删除拦截 ![image-20231104091443283](doc/imgs/image-20231104091443283.png) ## 1.2 passport登录和注册 ### 1.2.1 登录和鲁班其它接口功能流转过程 前端项目家目录 代理配置文件 vue.config.js - 前端代理 image-20231104092209202 1. 页面请求path路径 前缀匹配 页面请求是/api/aaa /api/a/b/c 这种都会由这个代理来处理 2. 根据代理的配置 向后跳转. '^/api/' 过滤掉path中/api/ 在1中过滤掉后 /aaa /a/b/c. 拼接 http://localhost:8097 页面中 请求路径 http://localhost:8989/api/a/b/c---代理处理--> http://localhost:8097/a/b/c - 登录代理 http://localhost:8989/to_passport/login image-20231104092756441 1. 匹配请求中前缀是to_passport的请求 2. 重写path将to_passport过滤掉 /login 3. 拼接target http://localhost:8099/login ### 1.2.2 passport登录和注册 - luban-front才有注册 luban-admin-front 后台管理只有登录 没有注册. - 注册不要重名 重名的数据会导致登录失败. - 总结 nacos启动 luban-front启动 common-passport.sql导入 passport启动 # 2 整洁架构分层开发 ## 2.1 目标 - [x] 了解整洁架构思想 - [x] 理解分层开发的含义 - [x] 熟练掌握分层开发中每一层的作用和功能. - [x] 熟练掌握分层开发中出参入参的约定 ## 2.2 分层开发 使用maven管理java项目,支持多模块开发的.我们通过maven的依赖关系,maven的多模块管理,可以将一个项目分成多层,最终依赖到一起整合到一起运行---这种开发方式就是分层开发. ![image-20231104101738202](doc/imgs/image-20231104101738202.png) 说白了,就是利用maven依赖特性,将代码拆分的更零散. 问题: 分层最终分成几层,如果代码功能特别**复杂**,包括持久层 业务层 仓储层,rpc http接口 es整合 redis缓存.应该**如何划分层次**? 为什么要进行分层开发? ## 2.3 整洁架构思想 <<代码整洁之道>> Bob(罗伯特马丁)大叔曾经说过 程序架构总是一样的. 让程序运行很简单. 让程序"正确"很难. 让程序**维护简单,扩展简单**就是正确. 但是程序项目又不断地在扩展 和维护. 如果我们发现项目的扩展和维护很艰难. **实现整洁架构通过分层开发,让层与层之间尽可能松耦合,并且要保证稳定的,坚实的,核心的内容在底层依赖出现.** ## 2.4 整洁架构落地 可以将整个项目开发过程 拆分成7个层级关系,通过依赖 实现层与层的连接. - 依赖倒置: 一种开发原则(开闭原则,**依赖倒置**原则,单一职责原则,接口隔离原则,迪米特原则 SOLID) ```txt 程序要依赖于抽象接口,不要依赖于具体实现 ``` ![image-20231104114240823](doc/imgs/image-20231104114240823.png) 分层结构特点: 1. 稳定的分层,依赖关系靠下.代码不太变化. 2. 不稳定的分层依赖关系靠外. 通过依赖导致的原则来处理的. ## 2.5 分层开发整洁架构--数据出参入参约定 ![image-20231104115255197](doc/imgs/image-20231104115255197.png) 如果分层开发的结构按上述实现. 约定的出参和入参. - adapter层: - 入参: - 读数据: query后缀 - 写数据: param后缀 - 出参: - 读数据: VO后缀 - 写数据: 写入生成的ID值 - domain: - 入参: - 读数据:query后缀 - 写数据:param后缀 - 出参: - 读数据: BO后缀 - 写数据: 写入生成的ID值 - infrastructure: - 入参: - 读数据:query后缀 - 写数据:param后缀 - 出参: - 读数据: BO后缀 - 写数据: 写入生成的ID值 - dao持久层 - 入参: - 读数据: - 普通读: query - 分页读: 定义分页的DBQuery(拼接limit分页条件) - 写数据: do|entity - 出参: - 读数据: - 普通读: do|entity - 分页读: 单独封装的分页对象 - 写数据: id值 # 3 开发鲁班上门师傅前台项目 ## 3.1 目的 鲁班上门,是一个微服务开发的集群.其中师傅前台是一个服务. **利用这个服务 单独创建项目 从0-1开发.将整洁架构运用落地.** ## 3.2 实现步骤 ### 3.2.1 创建一个新工程 Worker-Server-JSD2306 image-20231104143715842 主要负责子工程的依赖版本控制.必须提供相互兼容的jar包资源. ### 3.2.2 配置父工程的pom文件 继承 私服中的父工程 tarena-mall-bom. 约定了一堆资源版本. ```xml tarena-mall-bom com.tarena.mall 1.0.0-SNAPSHOT ``` ### 3.2.3 创建整洁架构 管理依赖关系 #### 3.2.3.1 worker-server-start 在 Worker-Server-2306在创建一个子工程.使用第三级maven模块 完成 整洁架构. image-20231104150213505 最终目标: image-20231104144658865 #### 3.2.3.2 worker-server-protocol 协议层: 是在项目设计的时候,到项目落地开发 出现的各种数据接口类型. DTO QUERY PARAM. image-20231104150645199 暂时不需要依赖分层中的其它层. #### 3.2.3.3 worker-server-domain 领域层: 核心业务层,包括 BO业务对象,service业务代码 定义仓储层接口. image-20231104150953167 在domain中 准备好和protocol的依赖. ```xml org.example worker-server-protocol 1.0-SNAPSHOT ``` #### 3.2.3.4 worker-server-client-api 对外暴露的RPC接口工程: 如果有需求,才会编写这里的接口. 在adapter的内部实现的. image-20231104151844832 和domain一样,依赖protocol. ```xml org.example worker-server-protocol 1.0-SNAPSHOT ``` #### 3.2.3.5 worker-server-dao-api 定义持久层接口.让实现去选择具体的框架. image-20231104152142044 依赖protocol ```xml org.example worker-server-protocol 1.0-SNAPSHOT ``` #### 3.2.3.6 worker-server-adapter 适配层: 包含http协议请求接口 controller rpc协议通信接口rpc实现 异步消息通信相关 生产端和消费端 image-20231104152344406 依赖 client-api和domain ```xml org.example worker-server-client-api 1.0-SNAPSHOT org.example worker-server-domain 1.0-SNAPSHOT ``` #### 3.2.3.7 worker-server-infrastructure 基础设施层: 包含的代码基本上都围绕着仓储层完成repository.可以实现缓存逻辑. image-20231104152949116 ```xml org.example worker-server-domain 1.0-SNAPSHOT org.example worker-server-dao-api 1.0-SNAPSHOT ``` #### 3.2.3.8 worker-server-dao-impl 持久层实现: 主要根据依赖的持久层框架 完成应用,如果依赖的是mybatis,主要使用xml映射文件绑定dao-api中接口和方法. image-20231104153457231 ```xml org.example worker-server-dao-api 1.0-SNAPSHOT ``` #### 3.2.3.9 worker-server-main 整合所有分层的工程项目: 包含了当前分层的所有代码,包含运行相关的环境,比如springboot 配置类等. image-20231104154014930 依赖三个分层项目 ```xml org.example worker-server-adapter 1.0-SNAPSHOT org.example worker-server-infrastucture 1.0-SNAPSHOT org.example worker-server-dao-impl 1.0-SNAPSHOT ``` ## 3.3 接口测试 测试跑通一个流程,准备一个http接口. TestController-->TestService-->TestRepo-->TestRepoImpl-->TestDao-->TestDao.xml 接口功能: 传递一个id值 21 | 要素 | 值 | 备注说明 | | -------- | --------------------------- | -------- | | 请求地址 | /test/worker | | | 请求方式 | GET | | | 请求参数 | TestWorkerQuery workerQuery | | | 返回数据 | TestWorkerVO | | 请求参数 http://localhost:8080/test/worker?userId=21 接收参数TestWorkerQuery 只有一个属性Long userId 返回值VO: ``` { "id": 师傅id, "userId": 绑定师傅的用户id, "realName": 师傅真实姓名 } ``` ### 3.3.1 TestController 每次编写代码考虑2个问题: 1. 在哪写: worker-server-adapter 2. 需要什么资源(依赖,其它代码): - spring-webmvc - TestWorkerVO(adapter) - lombok(父工程) - TestWorkerQuery(protocol) - TestService TestWorkerBO(domain) image-20231104162759208 ### 3.3.2 TestService 1. 在哪写: worker-server-domain 2. 需要什么资源: - TestService TestWorkerBo - spring-context(依赖) - TestRepository接口(domain) image-20231104163645054 ### 3.3.3 TestRepoImpl 1. 在哪写: worker-server-infrastructure 2. 需要什么依赖: - TestDao|TestMapper 接口(dao-api) - TestWorkerDO(dao-api) image-20231104164519570 ### 3.3.4 TestDao 1. 在哪写(dao-api) 2. 缺少什么依赖 - TestDao,TestWorkerDO(dao-api) image-20231104170720709 ### 3.3.5 TestDao.xml 1. 在哪写(dao-impl/main/java/resource/mapper文件夹) 2. 需要的依赖资源: - mybatis-plus依赖(扫描全局的classpath但是mybatis不配置 无法扫描) - 数据库驱动mysql - 映射文件内容: - namespace 指向实现的接口TestDao - 标签select id和接口抽象方法名称一致 ```xml ``` ### 3.3.6 springboot运行环境配置 1. 在哪写(main) 2. 需要什么资源 - 启动类TestMain - web容器(spring-boot-starter-web) - 数据源属性(spring.datasource.url...): application.yaml - mybatis-plus扫描映射文件包: application.yaml ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/tarena_tp_luban_worker?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # mybatis-plus属性 mybatis-plus: #classpath main模块启动只会扫描当前项目的类路径 dao-impl dao-api...都不扫描 #classpath* main所有依赖的 类加载路径都会扫描 mapper-locations: classpath*:/mapper/*.xml #日志打印 sql执行语句打印控制台 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` 测试访问 http://localhost:8080/test/worker?userId=21 ![image-20231104173952562](doc/imgs/image-20231104173952562.png) # 1 鲁班上门 ## 1.1 鲁班上门项目介绍|项目描述 1. 目前市场很多小家电品牌没有完善的售后安装服务体系,为用户最终使用带来不便; 2. 我们日常生活中也遇到棘手问题需要劳务上门服务,比如手机、电脑等电子设备设备维修,家务清洁等,苦于找不到合适的师傅。 鲁班上门平台主要解决以上痛点。为小众家电品牌如冰箱洗衣机等提供上门安装服务。接收上游用户(包括电商平台,个人及小众家电公司)订单(需求单),并为下游(安装师傅)分发订单; 后续业务扩展可以支持其他上门劳务项目,比如手机维护、电脑维护、家政清洁等。第一期仅支持电器上门安装业务。地域可能局限在某个城市. 项目愿景 鲁班平台最终会帮助小众家电品牌建立完善的售后服务体系,添加售后服务缺失环节,为百姓生活提供快捷、便利的上门服务;同时为下沉劳务市场提供大量就业机会。 前台: http://dev.front.luban.p.yufeiworld.com 后台: http://dev.admin.front.luban.p.yufeiworld.com ## 1.2 鲁班上门服务架构 image-20231106093039444 ## 1.3 鲁班上门师傅ER图 ### 1.3.1 ER图介绍 数据库表格是如何创建出来的. 项目产生,项目创建经历的环节 - 立项(甲方的需求,产品经历的需求) - 谈需求 产出**需求文档**(原形) **接口文档**,**测试用例**等 - 根据接口功能需求 设计数据库. - **需求分析阶段: 理解需求 分析需求,定义需求内部联系.** - **概念设计阶段: 使用ER图工具,设计实体,确定实体关系** - 逻辑设计阶段: 将ER图 字段属性 合并优化整理 - 物理设计阶段: 创建针对具体数据库的sql语句 - 数据库实施阶段: 安装数据库软件,启动,生成初始化数据. 配合开发使用. - 数据库运维阶段: 伴随项目运行,长期的工作之一. ER图定义的是需求描述中,所有实体之间的联系,要落地一张大的**ER图表.** ### 1.3.2 ER练习案例 需求描述: 请设计一个教务系统,面向**老师**和**学生**.学生可以查询自己的选则**课程**,可以查询自己的某个课程的**成绩**.老师可以在教务系统选择授课课程,一个老师只能选择一个课程. 1. 在需求中 确定实体内容(**项目系统中的数据载体**). 老师 学生 课程 成绩 2. 建立ER(entity relationship) 关系(1:1,1:n,n:n) image-20231106100749035 基于上述ER图可以常见数据库表格. - 实体必定对应一个表格: teacher student coures transcripts - 实体关系表n:n: student_coures ### 1.3.3 ER实体关系小练习 练习内容: 根据需求描述 画ER图 需求描述: 请开发一个论坛系统,**用户**登录可以申请成为**版主**,也可以以普通用户浏览论坛,论坛分为多个**板块**,论坛管理员可以新增创建板块.用户可以在不同的板块浏览 **帖子**或者发帖,也可以**回帖**.版主可以删除板块的帖子和回帖 1. 确定实体 用户 版主 板块 帖子 回帖 2. 确定实体关系 image-20231106103711862 表格设计有多少张: - 实体表格: user banzhu fatie huitie bankuai - 关联表: user_banzhu ### 1.3.4 鲁班ER图关系--师傅核心业务 需求描述: **用户**需要注册和登录,访问鲁班上门.登陆之后,用户可以提交资质申请,成为鲁班上门平台的**师傅**之一.鲁班上门在申请师傅的同时,需要提交资质证件**照片**(身份证 线下培训成绩 专业技能证书),需要绑定服务**区域**和服务**种类**.一个师傅可以同时绑定多个区域和多个种类.平台接收到师傅的入驻申请,对申请进行审核操作,并且记录**审核信息**.一旦审核通过,平台会给这个师傅绑定一个**平台账号**,并且师傅可以在手机客户端 查看 上游厂商推送的**需求单**数据.如果满足条件,满足要求,师傅可以对需求单进行抢单操作.抢单成功之后 平台给师傅创建**订单**,师傅可以手机端查看自己的所有订单信息.对未完成订单进行 签到 施工 完成的点击操作.一旦点击完成.平台会处理记录订单的**账单**信息,根据账单,结算完毕后给师傅账户打款,最终订单完成. 1. 确认实体 用户 师傅 附件 区域 种类 平台账号 师傅审核信息 需求单 订单 账单 2. 确定实体关系 image-20231106111136642 从中可以看出 对应的表格设计,与师傅有关的表格: user worker worker_area worker_category attach worker_audit_log account order bill. **师傅服务管理的表格四张: worker worker_area worker_category audit_log** 数据库 tarena_tp_luban_worker. ## 1.4 鲁班上门师傅前台-师傅入驻接口开发 ### 1.4.1 接口文件 | 要素 | 值 | 备注 | | -------- | ----------------------------------------------- | ---- | | 请求地址 | /worker/create | | | 请求方式 | post | | | 请求参数 | 请求体携带的JSON @RequestBody WorkerCreateParam | | | 返回值 | Long workerId | | ```json { "code":"0", "message":"ok", "data":"携带数据,一般查询的时候赋值data" } ``` code: 0表示成功 其它一切数字都表示失败. ### 1.4 2 准备一下开发所需的环境 Worker-Server-JSD2306 把common po(entiry do) protocol 粘贴复制. image-20231106112931479 worker-common: 开发worker-server师傅前台系统的公用代码,包含一些工具,和一些状态码的枚举类 worker-po: 相当于是师傅服务中的表给对应的entity. 请将 luban-project 对应的2个项目src和pom文件依赖全部复制过来. image-20231106113304279 - po和common在已经创建项目中依赖关系 protocol协议层,依赖 common dao-api持久层接口,依赖po ### 1.4.3 师傅入驻时序图 ![image-20231106115725907](doc/imgs/image-20231106115725907.png) ### 1.4.4 完成师傅入驻的业务流程 WorderController ->WorkerService **-->WorkerRepository->WorkerRepositoryImpl->WorkerDao->WorkerDao.xml** -->WorkerAreaRepository->WorkerAreaRepositoryImpl->WorkerAreaDao->WorkerAreaDao.xml -->WorkerCategoryRepository->WorkerCateRepositoryImpl->WorkerCateDao->WorkerCateDao.xml 1. 在哪写 2. 缺什么资源 3. 什么业务逻辑 ![image-20231106152930719](doc/imgs/image-20231106152930719.png) parameterType: 新增数据参数类型 useGeneratedKeys: 是否需要为自增的字段回填数据,true是 false不需要 keyColumn: 自增的字段名称 keyProperty: 自增回填的属性 也就是Wokrer中的属性 save 师傅的映射文件 ```xml insert into `worker` ( `user_id`, `birthday`, `phone`, `real_name`, `id_card`, `status`, `audit_status`, `cert_status`, `create_user_name`, `create_user_id`, `modified_user_id`, `modified_user_name`, `gmt_modified`, `gmt_create` ) values (#{userId}, #{birthday}, #{phone}, #{realName}, #{idCard}, #{status}, #{auditStatus}, #{certStatus}, #{createUserName}, #{createUserId}, #{modifiedUserId}, #{modifiedUserName}, #{gmtModified}, #{gmtCreate}) ``` param新增参数和do对象转化问题. 创建仓储层转化器Converter(Mock版本) ```java public WorkerArea param2po(WorkerAreaParam param) { WorkerArea workerArea = new WorkerArea(); BeanUtils.copyProperties(param, workerArea); //LoginUser loginUser = SecurityContext.getLoginToken(); workerArea.setGmtCreate(System.currentTimeMillis()); workerArea.setGmtModified(workerArea.getGmtCreate()); workerArea.setCreateUserId(100L); workerArea.setModifiedUserId(100L); workerArea.setCreateUserName("mock"); workerArea.setModifiedUserName("mock"); workerArea.setStatus(1); return workerArea; } ``` WorkerAreaRepoImpl循环写入和批量写入. 一定要使用批量处理,不能循环处理. WorkerAreaDao.xml 批量新增的insert语句 ```xml insert into `tarena_tp_luban_worker`.`worker_area`( `user_id`, `area_id`, `create_user_name`, `create_user_id`, `modified_user_name`, `modified_user_id`, `gmt_create`, `gmt_modified`, `area_detail` ) values (#{area.userId}, #{area.areaId}, #{area.createUserName}, #{area.createUserId}, #{area.modifiedUserName}, #{area.modifiedUserId}, #{area.gmtCreate}, #{area.gmtModified}, #{area.areaDetail}) ``` **自行练习完成 category相关新增的方法后续所有代码** WorkerCategoryRepoImpl循环写入和批量写入. 一定要使用批量处理,不能循环处理. WorkerCategoryDao.xml 批量新增的insert语句 ```xml insert into `tarena_tp_luban_worker`.`worker_category`( `user_id`, `category_id`, `status`, `create_user_name`, `create_user_id`, `modified_user_id`, `modified_user_name`, `gmt_modified`, `gmt_create`, `category_detail`) values (#{category.userId}, #{category.categoryId}, #{category.status}, #{category.createUserName}, #{category.createUserId}, #{category.modifiedUserId}, #{category.modifiedUserName}, #{category.gmtModified}, #{category.gmtCreate}, #{category.categoryDetail}) ``` ## 1.5 微服务组件 ### 1.5.1 当前项目存在的问题 - 启动是否没有报错 - 缺少和前端访问进行测试的能力(luban-front) - 代码userId 登录用户信息是写死的 ### 1.5.2 微服务访问结构 ![image-20231106171710219](doc/imgs/image-20231106171710219.png) ### 1.5.3 worker-server 配置nacos - 依赖nacos(worker-server-main模块) ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - yaml属性 ```yaml spring: application: name: luban-worker-server cloud: nacos: discovery: server-addr: localhost:8848 ``` ### 1.5.4 启动多个进程 提示启动顺序 - [x] nacos 服务端进程 - [x] luban-front: npm run dev:h5 - [x] passport(8099): jar配置的idea启动项 - [x] luban-attach-server: luban-project 的luban-attach中 找到启动类运行 确定数据库 数据源属性 image-20231106173459974 可以配置启动项,添加jvm选项参数,减低内存占用 ```shell -Xmx128M -Xms128M -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=128M ``` image-20231106173319321 - [x] worker-server: 启动类运行 - [x] 网关: 启动类运行,也可以添加jvm选项 从 注册登录开始,测试师傅入驻写入数据是否可以正常访问. # 1 鲁班上门-师傅前台入驻 ## 1.1 师傅入驻遗留的问题 ### 1.1.1 动态获取userId - 现有JWT解析流程 ![image-20231107091100891](doc/imgs/image-20231107091100891.png) 只要后端能够获取这个请求头Authorization 转化成LoginUser对象,就可以知道当前登录状态. ```json { "userId": 14, "nickName": "123", "userName": "XXW001", "avatar": null, "deviceId": null, "activate": true, "days": null, "expireAt": null } ``` - ThreadLocal 可以在一个java程序中,通过当前方法执行的**线程** 做成一个key值,来进行value值的传递. image-20231107092603203 - domain添加一个依赖 ```xml com.tarena.passport passport-sdk 1.0-SNAPSHOT javax.servlet servlet-api ``` 这个依赖资源中,会创建一个**JSON**解析器,通过一个过滤器机制,将网关透传的Authorization头 解析成LoginUser对象,并且通过工具SecurityContext存放到ThreadLocal对象.这样后端所有的执行代码,都可以直接获取LoginUser. - worker-server-main添加属性 JSON解析器能否创建,取决于一个属性条件 ```yaml json: provider: fastJson ``` - 使用SecurityContext(可以调整代码 动态获取userId) 在代码**任意位置**,都可以通过这个类调用方法 过去登录对象. ```java private Long getUserIdFromJwt() throws BusinessException { //无法获取真正的登录userId LoginUser loginToken = SecurityContext.getLoginToken(); //判断是否为空 if (loginToken==null || loginToken.getUserId()==null){ //抛自定义异常 throw new BusinessException("-999","user没登录"); } return loginToken.getUserId(); } ``` - 所有转化器 **WorkerConverter** ![image-20231107100435653](doc/imgs/image-20231107100435653.png) **WorkerAreaConverter** ![image-20231107100708016](doc/imgs/image-20231107100708016.png) **WorkerCategoryConverter** ![image-20231107100905710](doc/imgs/image-20231107100905710.png) ### 1.1.2 dubbo远程绑定图片 - 问题描述 当前入驻的师傅,暂时没有绑定上传的资质图片. - 图片服务功能 1. 上传的图片 通过接收接收,验证是否是图片资源,存储到磁盘 2. 整理图片信息 比如宽高,名称,后缀,等 ,写入数据库attach标签 - businessType: 业务类型(师傅图片 type=100 施工图片 type=200 用户头像 type=300) - businessId: 业务id(当业务type=100 当前这个图片保定businessId=张三Id) 两个属性同时生效,才能绑定具体的业务数据,到底是师傅的资质,还是某个用户的头像. 上传图片的操作,一般是在业务绑定之前就要执行的. 所以刚刚上传的图片数据库businessType和businessId都是0 - dubbo远程调用 image-20231107102904904 provider不需要我们做任何操作.已经暴露接口了.也已经实现了,也已经注册到nacos consumer需要我们进行如下操作: 1. 依赖provider暴露的接口 2. 本地配置dubbo的consumer xml内容 3. 在代码中直接使用接口的api进行远程调用了 **第一步**: 依赖provider暴露的接口模块 添加到worker-server-domain,workerService中调用图片api进行绑定操作 ```xml com.tarena.tp.attach luban-attach-server-client-api 1.0.0-SNAPSHOT ``` **第二步**: 准备dubbo依赖资源 dubbo 2.7.8 添加到worker-server-main. ```xml org.apache.dubbo dubbo 2.7.8 ``` **第三步**:准备dubbo 的xml配置文件 ``` com.tarena.tp.attach.server.client.AttachApi ``` **第四步**:导入xml到当前web应用 ![image-20231107110722630](doc/imgs/image-20231107110722630.png) **第五步**: 实现业务代码 在新增worker成功之后 ,使用worker的userId 绑定图片 定义: 1. businessType=100L 2. businessId=${userId} ![image-20231107112406231](doc/imgs/image-20231107112406231.png) 问题: 冗余图片如何产生的,上传图片了,但是没有新增业务数据. 如何处理,定时任务,扫描attach表格 条件 bizType=0 bizId=0 gmt_create | ## 2.3 编写代码 WorkerController -> WorkerService --> WorkerRepository(缓存)--> dao --> dao.xml --> WorkerAreaRepository(缓存)--> dao --> dao.xml --> WorkerCategoryRepository(缓存)--> dao --> dao.xml --> AttachApi --> AccountApi-->**TODO** 1. 在哪写 2. 缺什么资源 ### 2.3.1 缺少adapter层VO对象 image-20231107140330760 ### 2.3.2 WorkerBO 业务层数据对象 image-20231107140738667 ### 2.3.3 adapter层转化器assemble image-20231107141657809 ### 2.3.4 仓储层converter image-20231107144440083 ### 2.3.5 WorkerDao.xml 查询相关标签 ```xml `worker`.`id`, `worker`.`user_id`, `worker`.`birthday`, `worker`.`phone`, `worker`.`real_name`, `worker`.`id_card`, `worker`.`status`, `worker`.`audit_status`, `worker`.`create_user_name`, `worker`.`create_user_id`, `worker`.`modified_user_id`, `worker`.`modified_user_name`, `worker`.`gmt_modified`, `worker`.`gmt_create` ``` ### 2.3.6 WorkerAreaDao.xml 标签内容 resultMap sql select ```xml `worker_area`.`id`, `worker_area`.`user_id`, `worker_area`.`area_id`, `worker_area`.`status`, `worker_area`.`create_user_name`, `worker_area`.`create_user_id`, `worker_area`.`modified_user_id`, `worker_area`.`modified_user_name`, `worker_area`.`gmt_modified`, `worker_area`.`gmt_create`, `worker_area`.`area_detail` ``` ### 2.3.7 WorkerCategoryDao.xml标签内容 ```xml `worker_category`.`id`, `worker_category`.`user_id`, `worker_category`.`category_id`, `worker_category`.`status`, `worker_category`.`create_user_name`, `worker_category`.`create_user_id`, `worker_category`.`modified_user_id`, `worker_category`.`modified_user_name`, `worker_category`.`gmt_modified`, `worker_category`.`gmt_create`, `worker_category`.`category_detail` ``` ## 2.4 缓存方案 鲁班上门 用户登录之后 ,频繁的查询师傅详情,不管是入驻申请了还是没有入驻. 可以通过缓存引入 提升系统性能 ### 2.4.1 为什么要引入缓存 **所有的解决方案 和技术引入---需求驱动的.** **因为访问并发量比较高,通过减低系统RT响应时间,提升系统的并发性能.** 师傅详情查询接口,响应时间 计算 估算 影响因素 - 查询worker表格: 30ms左右 | redis 10ms - 查询worker_area表格:30ms左右 | redis 10ms - 查询worker_category表格: 30ms左右 | redis 10ms - 远程调用attach服务: 50ms | redis 25ms - 远程调用account服务: 50ms | redis 25ms 没有缓存的情况下,估算这个接口RT=190ms. 并发=100. qps=100/0.19S=526/s 引入redis缓存后,RT=80ms qps=100/0.085S=1176/s 性能提升了123%. ### 2.4.2 缓存方案和流程 作为鲁班上门 师傅有入住申请(写操作) 师傅登录之后详情查询(读操作).读多写少(一般电商基本都符合这个特点) 读写同时存在 读多写少 的环境---**Cache-aside(旁路缓存)** - 读流程 image-20231107163632231 - 写流程 写数据时,重点关注在缓存的数据 是否和数据库数据一致.如果不能保证一致,系统数据的一致性比较低,甚至不能用. image-20231107164735482 可以讨论的方法: 不做先写数据库在删缓存,有没有别的流程. 1. 先更新数据库,在更新缓存 2. 删除缓存,再更新数据库 3. 先更新数据库,再删除缓存(当前使用的流程) 4. 延迟双删. ### 2.4.3 数据一致性讨论 - 先更新数据库在更新缓存 image-20231107171701820 - 删除缓存,再更新数据库 image-20231107172131699 - 先更新数据库,在删除缓存(极端场景下,也会出现不一致,相比前两种,可能性极低) image-20231107172643304 仔细考虑。最后一种不一致的情况,出现在更新数据库update操作快于缓存set操作,实际情况几乎不可能出现的. 但是如果这种极端情况也需要防止的话,写流程可以采用 **双删/ 延迟双删** 延迟双删: image-20231107173210922 ### 2.4.4 缓存相关面试题 1. 你使用的缓存方案是什么? 2. 数据一致性如何保证? 3. 缓存雪崩 4. 缓存穿透 5. 缓存击穿 ### 2.4.5 师傅前台缓存方案落地 #### 2.4.5.1 读流程(详情查询) - redis整合到当前项目中 - 依赖添加(在哪加 repository仓储层) ```xml org.springframework.boot spring-boot-starter-data-redis ``` - yaml(如果本地localhost:6379可以忽略配置) ```yaml spring: redis: host: localhost port: 6379 password: ``` - 自定义配置类(自定义创建RedisTemplate,定义key和value的序列化) 选择使用第四阶段的redisConfiguration配置类 image-20231107174634687 - 代码编写. #### 2.4.5.2 写流程(师傅入驻) # 1 师傅前台缓存 ## 1.1 读流程 - 缓存在哪层: infruastructure 中的repository仓储层 - 缓存存放什么: - worker数据 - area数据 - category - 缓存数据结构是什么: - WorkerBO 整个对象放到缓存 String set(key,workerBO) - WorkerBO 存到redis,Hash hset(key,worker,area,category) - **WorkerBO 使用String key-value List\ List key-list List\ key-list** - key值如何设计: - 常见key设计方案: 前缀+业务数据{userId}+后缀 ```java @Autowired private RedisTemplate redisTemplate; @Override public WorkerBO findWorker(Long userId) { //1.查询缓存 判断缓存是否命中 //设计key值 String key="luban:worker:"+userId; //hasKey 等同于 redis命令exists Boolean hasKey = redisTemplate.hasKey(key); if (!hasKey){ //1.1 没命中 查询数据库 将数据库数据填补到缓存 Worker worker=workerDao.selectWorkerByUserId(userId); WorkerBO workerBO = workerConverter.po2bo(worker); //存储到缓存 供后续访问使用 ValueOperations opsForValue = redisTemplate.opsForValue(); opsForValue.set(key,workerBO); return workerBO; }else { //1.2 命中 直接返回缓存数据 ValueOperations opsForValue = redisTemplate.opsForValue(); return (WorkerBO) opsForValue.get(key); } } ``` 在仓储层明确的使用了redis作为缓存. 耦合性太强,可以将redis的实现拆开. 单独设计缓存的逻辑. 从接口 到抽象类. image-20231108094211601 问题1: 缓存一旦存储,是否需要存储永久数据? worker数据一旦发生变动,删除缓存,以保证数据一致性.redis数据存储内存里,为了节省内存,一致性兜底方案 自动删除,worker都不能存储成永久数据应该是临时,有超时时间的数据. 在RedisEntry代码里扩展重载方法. ## 1.2 写流程 - 缓存数据: - worker - worker_area - worker_category - 在哪里做了写操作: - saveWorker|saveCategory|saveArea 暂时不考虑缓存(save方法没有缓存一致性问题) - **deleteWorker|deleteCategory|deleteArea** ## 1.3 redis|缓存八股文 - **缓存雪崩**怎么解决 - 是什么: 缓存雪崩 是缓存数据出现大量未命中,导致请求涌向数据库. - 为什么: - 数据设置超时时间,时间超时同时达到,数据出现大量未命中. - redis出现宕机. - 怎么解决: - 针对第一种问题,可以设置所有数据都永久不过期(redis内存容量,数据一致性),设置超时时间变成随机. - redis宕机,如果需要代码处理,需要引入熔断机制(降级处理).一般情况下redis宕机不需要代码处理.因为集群redis存在主从替换的功能. - **缓存穿透**怎么解决 - 是什么: 访问的数据在缓存不存在,在数据库也不存在 - 为什么: - 恶意访问 - 业务数据组织错误 - 怎么解决 - **布隆过滤器**,提前预热所有需要防止穿透的数据(是否要在布隆存储所有数据).也可以做增量的补充,但是无法做修改删除. 架构: image-20231108142109022 过滤器方案1: redis的set集合来实现过滤器.如果数据量庞大起来,性能无法保证. 过滤器方案2: 布隆过滤器. 无论数据量多大,多小,都可以用相同时间判断存在,缺点无法删除. image-20231108143158407 布隆过滤器的特点: 1. 可以快速存储大量数据,因为根本不需要存储原数据 2. 判断存在,可能出现误判的,可以通过调整参数 调整2二进制越长误判越小,散列取余计算方法多几个. 3. 一旦判断不存在 100% 4. 无法删除数据. 要解决无法删除的问题,一般是挑一个没人用的时间,将数据重刷 - **缓存击穿**怎么解决 - 是什么:**高并发**访问同一个数据(缓存相同,数据相同),缓存未命中,同时执行cache-aside读流程. 造成数据库压力的上升. 资源的浪费,还可能出现读写的冲突.如果同时出现大量key值都击穿(雪崩了) - 为什么: - 数据是热点数据 - 怎么解决 - 目标是: 让这一批并发线程 只有一个线程 去查数据库 引入 redis分布式锁(排它锁,互斥锁). image-20230904104150360 - 缓存数据一致性怎么解决 - 参考思路: 回答cache-aside 缓存策略的写流程(延迟双删) 保证缓存和数据库一致. 根据延伸的问题. # 2 师傅后台审核 ## 2.1 师傅审核流程相关 ### 2.1.1 需求描述 当前台师傅APP手机端,提交了师傅入驻申请.暂时还没有进行审核.需要后台人员(同一套登录和权限判断逻辑)登录到后台,查询所有未审核和驳回审核的师傅列表(**师傅审核列表查询**).后台管理员可以点击列表中某一个申请的师傅查询申请详情(**审核详情查询**),需要看到师傅信息,工作区域,工作种类,上传图片数据.根据详情所展示的信息,做出评判,驳回申请,或者通过申请(**提交审核 worker_audit_log**). ### 2.1.2 师傅后台审核时序图 鲁班全景时序图: https://sparrowzoo.feishu.cn/docx/My4mdlLuMovmQixHko4c6qXbnYc?from=from_copylink image-20231108151643904 ## 2.2 准备项目模块环境 ### 2.2.1 使用worker-admin项目 清空这个项目,使用项目框架,完成luban师傅后台的核心代码开发. 保留原则(不满足保留原则的,全部删除): - 数据封装类: BO(domain) VO(adapter) DTO(protocol) PO DO ENTITY PAGEQUERY(dao-api) PARAM(protocol) QUERY(protocol) - 转化类: Converter Assemble Assembler - 所有接口: client-api \*\*Api domain \*\*Repository dao-api \*\*Dao ... - 映射文件: \*\*.xml ## 2.3 开发师傅后台-师傅审核列表 描述需求: 后台人员关注 审核列表的时候 ,列表中的师傅数据 审核状态审核通过就不再里面展示.只展示 审核驳回 未审核的状态. 并且允许管理员通过限定条件,查询审核列表师傅信息. ### 2.3.1 接口文件 | 要素 | 值 | 备注 | | -------- | ------------------------------------ | ----------------------- | | 请求地址 | /admin/worker/aduit | 分页查询 | | 请求方式 | Post | | | 请求参数 | @RequestBody WorkerQuery workerQuery | 查询条件,json请求体数据 | | 返回数据 | PagerResult\ | 包含分页数据的对象 | 一个前端和后端交互 分页的数据主要包含5个属性: - currentPage: 当前也(页面显示当前页是哪页) - pageSize: 每页数据条数,要和返回数组元素一致 - total: 分页查询总条数 - totalPage: 总页数(前端要展示 最后一页) - List: 分页的核心数据 ### 2.3.2 编码顺序 WorkerAdminController--> WorkerAdminService-->所有的repository实现-->阅读一下**.xml 查询分页列表的时候 阅读xml映射文件 #{} ${} 应用场景?: # 1 师傅后台开发 ## 1.1 功能-审核详情查询 ### 1.1.1 需求描述 通过审核列表 看到所有分页的未审核通过的师傅信息,列表的操作列,可以对某一个师傅进行审核操作,先查询师傅所有详情,包括师傅信息,区域信息,种类信息,审核信息,图片资质. ### 1.1.2 接口文档 | 要素 | 值 | 备注 | | -------- | ------------------------------------------ | ----------------------------- | | 请求路径 | /admin/worker/audit/detail | | | 请求方式 | POST | | | 请求参数 | @RequestParam(name="workerId") Long userId | 值是userId workerId只是业务名 | | 返回数据 | AuditDetailVO | | image-20231109090739043 ### 1.1.3 代码开发 WorkerAdminController-->WorkerAdminService -->WorkerRepository -->WorkerAreaRepository -->WorkerCategoryRepository -->AuditLogRepository -->AttachApi ## 1.2 功能-审核提交 ### 1.2.1 需求描述 后台前端页面,管理员进入审核详情,查看到师傅详细审核信息,根据规则.判断是否审核通过,提交请求,到后端代码写入审核记录.如果是驳回操作,需要提交驳回详细描述,原因,如果是通过操作,记录通过日志之后,还要对该师傅绑定的用户 在平台调用账户服务创建一个独立账户信息. ### 1.2.2 接口文档 | 要素 | 值 | 备注 | | -------- | ---------------------------------- | ---- | | 请求路径 | /admin/worker/audit/save | | | 请求方式 | POST | | | 请求参数 | @RequestBody AuditParam auditParam | | | 返回数据 | Long auditId | | ![image-20231109103724832](doc/imgs/image-20231109103724832.png) ### 1.2.3 代码开发 WorkerAdminController-->WorkerAdminService -->WorkerRepository(通过 驳回 审核状态 update) -->AuditLogRepository -->AccountApi ### 1.2.4(重要) 测试功能 出现图片绑定幂等设计问题,在worker-server模块中解决. 详情参考代码. # 2 账户服务开发 ## 2.1 RPC暴露接口类 luban-account-server-client-api - 创建账号 Long create(AccountParam accountCreateParam); 参数: AccountParam 三个属性在创建账号时 写入账号表格,做绑定 ``` @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class AccountParam implements Serializable { Long userId; String userName; String userPhone; } ``` 所以师傅审核提交,如果是通过,rpc调用使用create方法 - 查询账号信息 AccountDTO getAccountByUserId(Long userId); 参数: userId 返回值: AccountDTO ``` @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class AccountDTO implements Serializable { Long userId; String userName; String userPhone; Long totalAmount; } ``` userId,userName,userPhone 绑定用户信息 totalAmount: 当前账户所剩余额. 师傅登录APP手机端的时候,做师傅详情查询,需要展示账户中的totalAmount - 模拟打款 Long mockPayment(PaymentParam param); 参数: PaymentParam 模拟打款的时候,提交的账户相关信息. 返回值: 更新成功的数据行数 ```java @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class PaymentParam implements Serializable { Long userId; String orderNo; Long totalAmount; } ``` 给对应的用户账户,记录打款的来源订单,更新总金额. 师傅点击完成订单之后,通过异步调用的. ## 2.2 准备Account服务 ### 2.2.1 保留不删除的框架代码 - 数据封装类 - 接口类 - 转化类 - 映射文件 luban-account-server ## 2.3 代码开发 ### 2.3.1 顺序 AccountRpcImpl(AccountController)-->AccountService-->AccountRepository ## 2.4 远程调用 ### 2.4.1 dubbo架构 - 创建账户 - 获取账户信息 image-20231109152704465 ## 2.5 项目描述的真实性 核心概念: - 业务**描述** - 业务**闭环**: 数据有起始,有结束,流转完整 # 3 鲁班上门订单功能 ## 3.1 订单需求功能 平台运营过程中,会有**厂商**(provider),运营商等用户到平台入驻(省略了这个流程).厂商需要和鲁班平台签订合同,提供账号(**需求单**录入工作),提供接口,允许厂商提交需求单,发送需求单. **师傅**可以查询厂商推送的需求单(查询条件根据师傅自身入驻的时候服务区域和种类确定的),进行抢单操作,一旦争抢成功,平台将会给这个师傅单独创建一个**订单**的数据.记录师傅施工,完成等过程. 当订单完结后,系统要给订单,做**结算账单**,调用账号结算打钱,远程调用订单状态完结,需求单完结, ## 3.2 ER图补充 image-20231109173445790 ## 3.3 订单业务时序图 https://www.processon.com/v/654cac1a2a499a6f61daa89a # JSD2306-DAY18 # 1 订单功能 ## 1.1 订单时序图 https://www.processon.com/v/654cac1a2a499a6f61daa89a ## 1.2 需求单服务-查询需求单列表 ### 1.2.1 准备环境 保留不删除: - 数据封装类 - 接口类 - 转化类 - xml映射文件 ### 1.2.2 查询需求单列表 - 描述需求: 师傅登录平台,查看手机APP客户端,点击抢单按钮,展示符合师傅条件(区域和种类)的所有可抢需求单. 查询是分页展示的.使用时间做排序.而且要保证 返回的需求单是未抢状态. - 接口文档 | 要素 | 值 | 备注 | | -------- | ----------------------------------------------- | ---- | | 请求地址 | /demand/order/search | | | 请求方式 | POST | | | 请求参数 | @RquestBody RequestOrderQuery requestOrderQuery | | | 返回数据 | PagerResult\ | | 请求参数格式: 前端一个bug 没有携带工作区域和工作种类. ``` { "areaIds": [], 登录师傅,抢单师傅的工作区域 "orderCategoryIds": [], 登录师傅抢单师傅的工作种类 "pageNo": 0, "pageSize": 0 } ``` ### 1.2.3 代码开发 DemandController ->DemandService -->**WorkerApi** -->DemandRepository->Dao ### 1.2.4 展示价格和分润 厂商和运营商(上游用户)和平台会签订协议,原单价钱,分润(厂商,**平台**分一部分) 师傅. 分润的比例会记录在**厂商的入驻信息**中. profitScale 分润比例. 利用厂商分润比例 展示的师傅价格: 原价*((100-profitScale)/100) ### 1.2.5 定义和worker-server的rpc接口 需求功能: 根据userId 查询一个师傅的信息,包含的必要数据 有areaIds categoryIds ```java public Interface WorkerApi{ public WorkerDTO queryWorker(Long userId); } ``` public WorkerDTO queryWorker(Long userId); ```java @Data public class WorkerDTO implements Serializable{ List areaIds; List categoryIds; } ``` - provider:(worker-server) - 定义好的新接口信息 对外提供暴露 - 对接口做实现 - dubbo相关配置(xml) - dubbo:service - consumer:(demand-server) - 依赖对方暴露的接口(**大问题:依赖不到**) - 在业务层直接调用接口方法 - dubbo相关配置(xml) - dubbo:reference # 2 虚拟机安装和使用 参考笔记另外的一个文档 虚拟机virtualbox安装使用手册