这个是自己给公司做的一套微服务框架,其中部分的组件开发手册
技术 | 说明 | 官网地址 |
---|---|---|
Spring Boot | 容器+MVC框架 | https//spring.io/projects/spring-boot |
Spring Security | 认证和授权框架 | https//spring.io/projects/spring-security |
MyBatis-Plus | ORM框架 | https://mp.baomidou.com/gu |
MyBatisPlusGenerator | 数据层代码生成 | https://mp.baomidou.com/guide/generator.html#%E4%BD%BF%E7%94%A8%E6%95% |
PageHelper | MyBatis物理分页插件 | http//git.oschina.net/free/Mybatis_PageHelper |
Swagger-UI (Knife4j) | 文档生产工具 | https://doc.xiaominfo.com/knife4j/documentation/ |
RabbitMq | 消息队列 | https//www.rabbitmq.com/ |
Redis | 分布式缓存 | https//redis.io/ |
MongoDb | NoSql数据库 | https//www.mongodb.com/ |
Lombok | 简化对象封装工具 | https//github.com/rzwitserloot/lombo |
Seata | 全局事务管理框架 | https//github.com/seata/seata |
JWT | JWT登录支持 | https//github.com/jwt |
HikariCP | 数据库连接池 | https://github.com/brettwooldridge/HikariCP |
Docker | 应用容器引擎 | https//www.docker.com/ |
Spring Cloud Alibaba | 微服务架构解决方案 | https://spring.io/projects/spring-cloud-alibaba/ |
Oracle | 关系数据 | https://www.oracle.com/database/ |
mysql | 关系型数据库 | https://www.mysql.com/ |
Nacos | 服务注册&发现和配置管理 | https://nacos.io/zh-cn/docs/what-is-nacos.html |
Sentines-dashboard | 流量控制、熔断降级、系统负载保护 | https://github.com/alibaba/Sentinel/wiki |
工具 | 版本号 | 下载地址 |
---|---|---|
JDK | 1.8 (64) | https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html |
MYSQL | 8.0 | https://dev.mysql.com/downloads/ |
Oracle | 11g | https://www.oracle.com/cn/database/technologies/instant-client/downloads.html |
RabbitMq | 3.8.14 | https://www.rabbitmq.com/download.html |
Redis | 5.0.12 | https://download.redis.io/releases/redis-5.0.12.tar.gz |
MongoDb | 4.4 | https://www.mongodb.com/try/download/community |
Spring Boot | 2.2.6.RELEASE | https://spring.io/projects/spring-boot#learn |
Spring Cloud Alibaba | 2.2.1.RELEASE | https://spring.io/projects/spring-cloud-alibaba/#learn |
Nginx | nginx-1.19.10 | http://nginx.org/download/nginx-1.19.10.tar.gz |
Nacos | 1.3.1 | https://github.com/alibaba/nacos/releases |
Sentines-dashboard | 1.3.0 | https://github.com/alibaba/Sentinel/wiki |
简易环境搭建流程:
1. 安装IDEA,JDK,Maven.mysql ,oracle等工具,并配置好相关的环境变量
2. 拉取项目导入到导入到idea中
3. 安装好项目需要使用的服务,redis,rabbitmq,nacos,Mognodb等环境
4. 进行项目编译启动。
微服务项目需要使用到注册中心和服务发现组件,我们必须要先安装启动好nacos-serve
下载地址:https://github.com/alibaba/nacos/releases
1.解压我们的 nacos
cd conf
进入conf路径下,修改配置, vim application.proproties
修改配置数据库修改完成后 需要进行保存。 nacos默认是将数据持久化到本地文件,生产服务建议使用数据库配置。目前nacos只支持mysql数据库。注意如果是集群部署我们要确保每个服务都要链接 master节点。需要在数据库新建一个数据库 将 conf 目录下的 nacos-mysql.sql
导入到数据库
4.启动nacos cd bin
目录下,进入到bin目录下 执行命令(启动单机) sh startup.sh -m standalone
5.检查nacos启动的端口 lsof -i:8848
6.浏览器输入:http://192.168.163.129:8848/nacos 默认账号密码 : nacos/nacos
7.停止nocas 在nocas/bin目录下 执行 sh shutdown.sh
其他操作修改配置同理 linux步骤 ,修改成功后 ,启动nacos,进入 bin 目录下,单机启动可以将startup.cmd文件的启动模式修改为单机模式:
保存 ,直接运行 startup.cmd 启动nacos
三板斧之一,依赖引入
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
三板斧之二: 第二板斧写注解(也可以不写) @EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class CloudAdminApplication {
public static void main(String[] args) {
SpringApplication.run(CloudAdminApplication.class, args);
}
}
三板斧之三 :写配置文件 注意server-addr:不需要写协议
spring:
application:
name: @artifactId@
cloud:
nacos:
config:
## 服务地址
server-addr: 127.0.0.1:8848
## 命名空间 nacos的领域模型,可以设置不同的空间 进行配置隔离,NameSpace (默认的NameSpace是”public“ NameSpace可以进行资源隔离,比如我们dev环境下的NameSpace下的服务是调用不到prod的NameSpace下的微服务。我们可以配置好不同开发环境的配置,上线的时候只要切换对应的 NameSpace即可
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c
启动 cloud-admin-biz
项目
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5pnszmo-1621556997409)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429142551163.png)]
NameSpace(默认的NameSpace是”public“ NameSpace可以进行资源隔离,比如我们dev环境下的NameSpace下的服务是调用不到prod的NameSpace下的微服务)
比如: cloud-admin-biz 的配置文件中配置了注册中心namespace 地址:只会注册到对应的 namespace
server:
port: 7020
spring:
application:
name: cloud-admin-biz
cloud:
nacos:
discovery:
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c 命名空间与管理页面中的一致
server-addr: 127.0.0.1:8848
随着服务越来越多,配置参数也越来越多。有些配置是公共的,无需在每个配置文件中配置。为了统一管理配置,和动态的修改一个业务参数。在码云使用配置中心之前,维护起来都会很复杂。
微服务接入配置中心的步骤:
1.添加依赖包spring-cloud-alibaba-nacos-config
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring‐cloud‐alibaba‐nacos‐configartifactId>
dependency>
2.编写配置文件,需要写一个bootstrap.yml配置文件 (
[^]:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQDjIhXM-1621556997415)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429155248807.png)])
server-addr: 127.0.0.1:8848 表示我微服务怎么去找我的配置中心
spring.application.name=cloud-admin-biz 表示当前微服务需要向配置中心索要cloud-admin-biz
的配置
spring.profiles.active=dev 表示我需要向配置中心索要cloud-admin-biz的生产环境的配置
索要文件的格式为(data-id):
${application.name}- ${spring.profiles.active}.${file-extension}
真正在nacos配置中心上 就是 cloud-admin-biz-dev.yml
shared-configs: 共享配置 用来配置公共属性的 必须每个项目需要连接redis ,mysql 我们可以吧这两部分做一个公共的文件可以进行全局使用
refresh-enabled :支持动态刷新
配置如下:
server:
port: 7020
spring:
application:
name: cloud-admin-biz
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yml
shared-configs:
- application-dev.yml
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c
refresh-enabled: true
discovery:
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c
server-addr: 127.0.0.1:8848
profiles:
active: dev
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnK1HD61-1621556997419)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429155126435.png)]
启动项目,nacos 会自动的从配置中心去获取配置信息,本地就无需在进行配置了。
动态刷新
1. 在我的cloud-admin-biz-dev.yml 配置文件中加入配置:
edwin:
text: yulang
2.在项目中需要编写:
@RestController
@RequestMapping("/index")
@RefreshScope //这个必须要写 动态刷新的注解
public class DemoController {
@Value("${edwin.text}")
private String userName;
@GetMapping("/test")
public Object test(){
return userName;
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kak9jmFV-1621556997420)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429162614172.png)]
直接在配置文件中修改:
edwin:
text: yulang11111111
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpGdLLyi-1621556997422)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429162723485.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XsvsmcdP-1621556997423)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429162751710.png)]
D:.
├─cloud 项目父目录
├─cloud-admin admin 平台服务
│ ├─cloud-admin-api 平台api
│ │ ├─src
│ │ │ └─main
│ │ │ └─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─admin
│ │ │ └─api
│ │ │ ├─dto
│ │ │ ├─entity
│ │ │ ├─feign
│ │ │ └─vo
│ └─cloud-admin-biz 平台业务服务
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─admin
│ │ │ │ ├─config
│ │ │ │ ├─controller
│ │ │ │ ├─core
│ │ │ │ ├─handler
│ │ │ │ ├─mapper
│ │ │ │ └─service
│ │ │ │ └─impl
│ │ │ └─resources 资源信息
│ │ │ ├─mapper
│ │ │ └─tenant
│ │ └─test 测试目录
│ │ └─java
│ │ └─com
│ │ └─cloud
│ │ └─admin
├─cloud-auth 鉴权服务
│ ├─src
│ │ └─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─auth
│ │ │ ├─config
│ │ │ ├─endpoint
│ │ │ ├─handler
│ │ │ └─service
│ │ └─resources 资源文件
│ │ ├─static
│ │ │ └─css
│ │ └─templates 页面
│ │ └─ftl
├─cloud-common 项目公共服务
│ ├─cloud-common-bom 公共依赖
│ ├─cloud-common-core 核心依赖 提供了一些高级的java类库,Google Guava 和 hutool工具
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─core
│ │ │ │ ├─config
│ │ │ │ ├─constant
│ │ │ │ │ └─enums
│ │ │ │ ├─exception
│ │ │ │ ├─jackson
│ │ │ │ ├─sensitive
│ │ │ │ └─util
│ │ │ └─resources
│ │ │ ├─i18n
│ │ │ └─META-INF
│ ├─cloud-common-data 公共处理数据相关
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─data
│ │ │ │ ├─cache
│ │ │ │ ├─datascope
│ │ │ │ ├─enums
│ │ │ │ ├─handler
│ │ │ │ ├─mybatis
│ │ │ │ ├─quene
│ │ │ │ ├─resolver
│ │ │ │ └─tenant
│ │ │ └─resources
│ │ │ └─META-INF
│ ├─cloud-common-datasource 数据源相关配置
│ │ └─src
│ │ └─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─common
│ │ │ └─datasource
│ │ │ ├─annotation
│ │ │ ├─config
│ │ │ └─support
│ │ └─resources
│ │ └─META-INF
│ ├─cloud-common-gateway 网关相关配置
│ │ ├─src
│ │ │ └─main
│ │ │ └─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─common
│ │ │ └─gateway
│ │ │ ├─annotation
│ │ │ ├─configuration
│ │ │ ├─filter
│ │ │ ├─rule
│ │ │ ├─support
│ │ │ └─vo
│ ├─cloud-common-log 日志相关配置
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─log
│ │ │ │ ├─annotation
│ │ │ │ ├─aspect
│ │ │ │ ├─event
│ │ │ │ ├─init
│ │ │ │ └─util
│ │ │ └─resources
│ │ │ └─META-INF
│ ├─cloud-common-minio 文件服务相关
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─minio
│ │ │ │ ├─http
│ │ │ │ ├─service
│ │ │ │ └─vo
│ │ │ └─resources
│ │ │ └─META-INF
│ ├─cloud-common-security 安全认证相关
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─security
│ │ │ │ ├─annotation
│ │ │ │ ├─component
│ │ │ │ ├─exception
│ │ │ │ ├─feign
│ │ │ │ ├─handler
│ │ │ │ ├─listener
│ │ │ │ ├─mobile
│ │ │ │ ├─service
│ │ │ │ ├─social
│ │ │ │ └─util
│ │ │ └─resources
│ │ │ ├─META-INF
│ │ │ └─org
│ │ │ └─springframework
│ │ │ └─security
│ ├─cloud-common-sentinel 限流熔断相关
│ │ ├─src
│ │ │ └─main
│ │ │ ├─java
│ │ │ │ └─com
│ │ │ │ └─cloud
│ │ │ │ └─common
│ │ │ │ └─sentinel
│ │ │ │ ├─handle
│ │ │ │ └─parser
│ │ │ └─resources
│ │ │ └─META-INF
│ ├─cloud-common-sequence 分布式主键相关
│ │ └─src
│ │ └─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─common
│ │ │ └─sequence
│ │ │ ├─builder
│ │ │ ├─exception
│ │ │ ├─properties
│ │ │ ├─range
│ │ │ │ └─impl
│ │ │ │ ├─db
│ │ │ │ ├─name
│ │ │ │ └─redis
│ │ │ └─sequence
│ │ │ └─impl
│ │ └─resources
│ │ └─META-INF
│ └─cloud-common-swagger api接口文档
│ ├─src
│ │ └─main
│ │ └─java
│ │ └─com
│ │ └─cloud
│ │ └─common
│ │ └─swagger
│ │ ├─annotation
│ │ └─config
├─cloud-gateway 网关服务,添加了动态路由功能
│ ├─src
│ │ └─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─cloud
│ │ │ └─gateway
│ │ │ ├─config
│ │ │ ├─filter
│ │ │ └─handler
│ │ └─resources
cloud-admin 项目为系统中的平台服务,主要分为 cloud-admin-api 和 cloud-admin-biz两个项目组成。
cloud-admin-api: 主要包含一些工程中的实体类(po,vo),服务接口 提供对外暴露的api
cloud-admin-biz: 主要实现平台的核心业务逻辑,服务接口的实现类,api接口地址服务等
1.cloud-admin-api
pom文件结构
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.cloudgroupId>
<artifactId>cloud-adminartifactId>
<version>1.0.0version>
parent>
<artifactId>cloud-admin-apiartifactId>
<packaging>jarpackaging>
只需要依赖核心公共组件
<dependencies>
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-common-coreartifactId>
dependency>
dependencies>
project>
1.cloud-admin-biz
pom文件结构 需要使用的对应功能组件 可以引入 cloud-common
包下的对应的jar
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.cloudgroupId>
<artifactId>cloud-adminartifactId>
<version>1.0.0version>
parent>
<artifactId>cloud-admin-bizartifactId>
<packaging>jarpackaging>
<description>通用用户权限管理系统业务处理模块description>
<dependencies>
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-admin-apiartifactId>
dependency>
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-common-logartifactId>
dependency>
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-common-swaggerartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-undertowartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
<plugin>
<groupId>io.fabric8groupId>
<artifactId>docker-maven-pluginartifactId>
plugin>
plugins>
build>
project>
配置文件中配置好注册中心地址:
server:
port: 7020 ## 服务端口号
spring:
application:
name: cloud-admin-biz ## 应用名称
cloud:
nacos:
discovery:
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848 ## nacos 服务地址
file-extension: yml ## 配置文件扩展名
shared-configs: ## 共享资源配置
- application-dev.yml
namespace: 4c203b07-7179-4712-90c1-f0710ef11f5c ## 命名空间
profiles:
active: dev # profiles 环境
启动 cloud-admin-biz 和网关项目
访问接口文档:http://127.0.0.1:7000/doc.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suthhpcq-1621556997425)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210429163156946.png)]
项目中需要使用swagger的只需要在对应的项目中引入 :
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-common-swaggerartifactId>
dependency>
无需任何的配置文件 直接在项目中使用原生swagger注解即可使用
在项目中的 controller中编写 swaager注解:
package com.cloud.admin.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cloud.admin.api.dto.UserDTO;
import com.cloud.admin.api.dto.UserInfo;
import com.cloud.admin.api.entity.SysUser;
import com.cloud.admin.service.SysUserService;
import com.cloud.common.core.util.R;
import com.cloud.common.log.annotation.SysLog;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@AllArgsConstructor
@RequestMapping("/user")
@Api(value = "user", tags = "用户管理模块")
public class SysUserController {
private final SysUserService userService;
/**
* 获取指定用户全部信息
*
* @return 用户信息
*/
@GetMapping("/info/{username}")
@ApiOperation(value = "获取指定用户全部信息")
@ApiImplicitParam(name = "username", value = "用户名", paramType = "String", dataTypeClass = UserInfo.class, required = true)
public R info(@PathVariable("username") String username) {
SysUser user = userService.getOne(Wrappers.<SysUser>query()
.lambda().eq(SysUser::getUsername, username));
if (user == null) {
return R.failed(null, String.format("用户信息为空 %s", username));
}
return R.ok(userService.findUserInfo(user));
}
/**
* 通过ID查询用户信息
*
* @param id ID
* @return 用户信息
*/
@GetMapping("/{id}")
@ApiOperation(value = "通过ID查询用户信息")
@ApiImplicitParam(name = "id", value = "用户ID", paramType = "String", required = true)
public R user(@PathVariable Integer id) {
return R.ok(userService.selectUserVoById(id));
}
/**
* 根据用户名查询用户信息
*
* @param username 用户名
* @return
*/
@GetMapping("/details/{username}")
@ApiOperation(value = "根据用户名查询用户信息")
@ApiImplicitParam(name = "username", value = "用户名", paramType = "String", required = true)
public R user(@PathVariable String username) {
SysUser condition = new SysUser();
condition.setUsername(username);
return R.ok(userService.getOne(new QueryWrapper<>(condition)));
}
/**
* 删除用户信息
*
* @param id ID
* @return R
*/
@SysLog("删除用户信息")
@DeleteMapping("/{id}")
@ApiOperation(value = "删除用户", notes = "根据ID删除用户")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "int", paramType = "path")
public R userDel(@PathVariable Integer id) {
SysUser sysUser = userService.getById(id);
return R.ok(userService.deleteUserById(sysUser));
}
/**
* 添加用户
*
* @param userDto 用户信息
* @return success/false
*/
@SysLog("添加用户")
@PostMapping
@ApiOperation(value = "删除用户", notes = "根据ID删除用户")
public R user(@RequestBody UserDTO userDto) {
return R.ok(userService.saveUser(userDto));
}
/**
* 更新用户信息
*
* @param userDto 用户信息
* @return R
*/
@SysLog("更新用户信息")
@PutMapping
@ApiOperation(value = "更新用户信息", notes = "根据ID删除用户")
public R updateUser(@Valid @RequestBody UserDTO userDto) {
return R.ok(userService.updateUser(userDto));
}
/**
* 分页查询用户
*
* @param page 参数集
* @param userDTO 查询参数列表
* @return 用户集合
*/
@GetMapping("/page")
@ApiOperation(value = "分页查询用户", notes = "分页查询用户")
public R getUserPage(Page page, UserDTO userDTO) {
return R.ok(userService.getUsersWithRolePage(page, userDTO));
}
/**
* 修改个人信息
*
* @param userDto userDto
* @return success/false
*/
@SysLog("修改个人信息")
@PutMapping("/edit")
@ApiOperation(value = "修改个人信息", notes = "修改个人信息")
public R updateUserInfo(@Valid @RequestBody UserDTO userDto) {
return userService.updateUserInfo(userDto);
}
/**
* @param username 用户名称
* @return 上级部门用户列表
*/
@GetMapping("/ancestor/{username}")
@ApiOperation(value = "根据用户名称查询上级部门用户列表", notes = "根据用户名称查询上级部门用户列表")
public R listAncestorUsers(@PathVariable String username) {
return R.ok(userService.listAncestorUsers(username));
}
}
引入相对应依赖包:
<dependency>
<groupId>com.cloudgroupId>
<artifactId>cloud-common-coreartifactId>
dependency>
java 核心的工具类库 ,可以直接使用操作 java中集合,素组,等数据结构,以及反射,json,序列化,参数校验等功能
com.cloud
cloud-common-data
项目中封装好了,redisTemplate模板,和redis缓存的工具类。如果业务系统使用到reids缓存 可以直接集成
1.引入相对应依赖包
com.cloud
cloud-common-log
1.日志依赖包,引入后,在应用系统中 配置日志模板 logback-spring.xml 配置到具体的项目resources目录下,系统会自动对controller层的日志加入trackId,方便在日志文件中排除错误;
@SysLog
注解,系统会自动将自动会对操作方法的日志进行入库操作。1.引入相对应依赖包 (需要在需要使用文件上传存储业务中引入)
com.cloud
cloud-common-minio
启动 minio服务器
需要先下载好服务端程序:https://dl.min.io/server/minio/release/windows-amd64/minio.exe
启动服务:
minio.exe server D:\ ## D:\ 你启动的一些配置文件生成目录 可以自己制定
启动完成后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqfphFYp-1621556997426)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430103922844.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8z8oWmP8-1621556997428)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430104019901.png)]
浏览器 输入 http://127.0.0.1:9000
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P6BT1fAv-1621556997429)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430104135690.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LzMA7BQS-1621556997432)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430104319563.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uHVRzvGw-1621556997433)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430104752895.png)]
4.引入依赖后需要在业务系统中配置
minio.url = http://127.0.0.1:9000 ## minio 服务地址 http://ip:port
minio.accessKey =minioadmin ## minio 用户名
minio.secretKey =minioadmin ##minio 密码
minio.endpoint.enable =true ## 开启服务
代码中使用:
@Autowired
private MinioTemplate minioTemplate;
public void uploadImage(File file){
// bucketName bucket名称 可以理解为对应的目录名称
// objectName 你的文件名称
//InputStream stream 对应文件的二进制流
minioTemplate.putObject("/TEST","12",new FileInputStream(file));
}
MinioTemplate 中封装了 如下方法 可以直接调用:
/**
* 创建bucket
*
* @param bucketName bucket名称
*/
@SneakyThrows
public void createBucket(String bucketName) {
if (!client.bucketExists(bucketName)) {
client.makeBucket(bucketName);
}
}
/**
* 获取全部bucket
*
* https://docs.minio.io/cn/java-client-api-reference.html#listBuckets
*/
@SneakyThrows
public List<Bucket> getAllBuckets() {
return client.listBuckets();
}
/**
* @param bucketName bucket名称
*/
@SneakyThrows
public Optional<Bucket> getBucket(String bucketName) {
return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* @param bucketName bucket名称
*/
@SneakyThrows
public void removeBucket(String bucketName) {
client.removeBucket(bucketName);
}
/**
* 根据文件前置查询文件
*
* @param bucketName bucket名称
* @param prefix 前缀
* @param recursive 是否递归查询
* @return MinioItem 列表
*/
@SneakyThrows
public List<MinioItem> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
List<MinioItem> objectList = new ArrayList<>();
Iterable<Result<Item>> objectsIterator = client
.listObjects(bucketName, prefix, recursive);
for (Result<Item> itemResult : objectsIterator) {
objectList.add(new MinioItem(itemResult.get()));
}
return objectList;
}
/**
* 获取文件外链
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param expires 过期时间 <=7
* @return url
*/
@SneakyThrows
public String getObjectURL(String bucketName, String objectName, Integer expires) {
return client.presignedGetObject(bucketName, objectName, expires);
}
/**
* 获取文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @return 二进制流
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
return client.getObject(bucketName, objectName);
}
/**
* 上传文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param stream 文件流
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
*/
public void putObject(String bucketName, String objectName, InputStream stream) throws Exception {
client.putObject(bucketName, objectName, stream, (long) stream.available(), null, null, "application/octet-stream");
}
/**
* 上传文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param stream 文件流
* @param size 大小
* @param contextType 类型
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
*/
public void putObject(String bucketName, String objectName, InputStream stream, long size, String contextType) throws Exception {
client.putObject(bucketName, objectName, stream, size, null, null, contextType);
}
/**
* 获取文件信息
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#statObject
*/
public ObjectStat getObjectInfo(String bucketName, String objectName) throws Exception {
return client.statObject(bucketName, objectName);
}
/**
* 删除文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#removeObject
*/
public void removeObject(String bucketName, String objectName) throws Exception {
client.removeObject(bucketName, objectName);
}
1.依赖引入
com.cloud
cloud-common-sequence
2.功能介绍
插件包中提供2中生成分布式序列化的规则:
1: SnowflakeSequence 基于雪花算法
2: RangeSequence 基于号段 生成的有序递增的id
其中号段中 也可以支持两种拨号器 a:基于数据库 b:基于redis
3.使用,在对应的配置 目前一个只系统支持一种生成方式
1.SnowflakeSequence开启方式:
cloud:
xsequence:
snowflake:
enable: true ##开启 snowflake算法
datacenterId: 1 ##数据中心ID,值的范围在[0,31]之间,一般可以设置机房的IDC[必选] 每个系统禁止使用一样的配置 默认值为 1
workerId: 1 ## 工作机器ID,值的范围在[0,31]之间,一般可以设置机器编号[必选] 每个系统禁止使用一样的配置 默认值为 1
注意:如果使用 snowflake 方式,一定要注意时钟回拨的问题。避免造成id重复
代码中使用:
@Autowired
private Sequence sequence;
@GetMapping("/test")
public Object test() {
return sequence.nextValue();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQUz1nNg-1621556997435)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430160127382.png)]
基于DB号段模式:
cloud:
xsequence:
range:
enable: db ## 开启号段模式 db 方式 严格区分大小写
tableName: tb_range ## 表名称 默认 tb_range 表
retryTimes: 1 ## 重试次数 默认1次
step: 1000 ## 获取range步长[可选,默认:1000]
stepStart: 0 ## 序列号分配起始值[可选:默认:0]
bizName: cloud ## 业务名称 每个业务对应的名称 禁止中文
### 数据源的而配置 可以使用默认的
spring:
datasource:
hikari:
minimum-idle: 1
maximum-pool-size: 3
connection-timeout: 30000
initialization-fail-timeout: 30000
idle-timeout: 30000
max-lifetime: 30000
connection-test-query: SELECT 1
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/cloud-admin?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
第一次启动会在对应的数据库中创建一张表 :
CREATE TABLE `tb_range` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`value` bigint(20) NOT NULL,
`name` varchar(32) NOT NULL,
`gmt_create` datetime NOT NULL,
`gmt_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
第一次使用会生成一条区间数据:
1 1000 cloud 2021-04-30 15:36:56 2021-04-30 15:37:42
注意在使用的时候 不要手动去修改表中数据,避免序列重复。
代码中使用:
@Autowired
private Sequence sequence;
@GetMapping("/test")
public Object test() {
return sequence.nextValue();
}
基于redis号段模式:
cloud:
xsequence:
range:
enable: redis ## 开启号段模式 redis 方式 严格区分大小写 如果项目中已经引入了redis 则只需开启即可
##redis配置可以使用默认
spring:
redis:
host: 192.168.163.130
port: 6379
password: admin
step:1000 ##获取range步长[可选,默认:1000]
stepStart: 0 ##序列号分配起始值[可选:默认:0]
bizName: cloud ##业务名称 建议每个项目配置不同的
代码中使用:
@Autowired
private Sequence sequence;
@GetMapping("/test")
public Object test() {
return sequence.nextValue();
}
reids中会存储当前 一轮的步长,主要如果项目重启的话,第一轮步长没有使用完,回自动进入一个步长。必须第一次步长 1000,项目中id只是用到了 500,项目重启了,id会从 1001开始
redis 中步长key :前缀 + 业务名 (x_sequence_ + 业务名 )
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1CSLgWn-1621556997437)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210430162557467.png)]
1.依赖引入 (https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D)
com.cloud
cloud-common-sentinel
2.启动服务 ,配置服务监控地址
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar 也可以直接: java -jar sentinel-dashboard-1.7.0.jar
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080
sentinel 控制台配置项:
配置项 | 类型 | 默认值 | 最小值 | 描述 |
---|---|---|---|---|
auth.enabled | boolean | true | - | 是否开启登录鉴权,仅用于日常测试,生产上不建议关闭 |
sentinel.dashboard.auth.username | String | sentinel | - | 登录控制台的用户名,默认为 sentinel |
sentinel.dashboard.auth.password | String | sentinel | - | 登录控制台的密码,默认为 sentinel |
sentinel.dashboard.app.hideAppNoMachineMillis | Integer | 0 | 60000 | 是否隐藏无健康节点的应用,距离最近一次主机心跳时间的毫秒数,默认关闭 |
csp.sentinel.dashboard.server | String | localhost:8080 | 指定控制台地址和端口 | |
4.拦截详情日志
无论触发了限流、熔断降级还是系统保护,它们的秒级拦截详情日志都在 ${user_home}/logs/csp/sentinel-block.log
里。如果没有发生拦截,则该日志不会出现。日志格式如下:
2014-06-20 16:35:10|1|sayHello(java.lang.String,long),FlowException,default,origin|61,0
2014-06-20 16:35:11|1|sayHello(java.lang.String,long),FlowException,default,origin|1,0
日志含义:
index | 例子 | 说明 |
---|---|---|
1 | 2014-06-20 16:35:10 |
时间戳 |
2 | 1 |
该秒发生的第一个资源 |
3 | sayHello(java.lang.String,long) |
资源名称 |
4 | XXXException |
拦截的原因, 通常 FlowException 代表是被限流规则拦截,DegradeException 则表示被降级,SystemBlockException 则表示被系统保护拦截 |
5 | default |
生效规则的调用来源(参数限流中代表生效的参数) |
6 | origin |
被拦截资源的调用者,可以为空 |
7 | 61,0 |
61 被拦截的数量,0无意义可忽略 |
所有的资源都会产生秒级日志,它在 ${user_home}/logs/csp/${app_name}-${pid}-metrics.log
里。格式如下:
1532415661000|2018-07-24 15:01:01|sayHello(java.lang.String)|12|3|4|2|295
1532415661000
:时间戳2018-07-24 15:01:01
:格式化之后的时间戳sayHello(java.lang.String)
:资源名12
:表示到来的数量,即此刻通过 Sentinel 规则 check 的数量(passed QPS)3
:实际该资源被拦截的数量(blocked QPS)4
:每秒结束的资源个数(完成调用),包括正常结束和异常结束的情况(exit QPS)2
:异常的数量295
:资源的平均响应时间(RT)其它的日志在 ${user_home}/logs/csp/sentinel-record.log.xxx
里。该日志包含规则的推送、接收、处理等记录,排查问题的时候会非常有帮助。
${log_dir}/sentinel-cluster-client.log
:Token Client 日志,会记录请求失败的信息访问:http://127.0.0.1:8080/ (用户名/密码: sentinel /sentinel)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5L54tBER-1621556997438)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506110436001.png)]
sentinel监控性能指标详解
实时监控面板
在这个面板中我们可以实时监控我们接口的**通过QPS
和拒绝的QPS**
由于这没有配置拒绝的QPS,看到的都是正常的通过的QPS指标
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVy2UqHJ-1621556997441)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506111100402.png)]
设置流控规则为 2:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tYMY4NR-1621556997442)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506111521918.png)]
当QPS大于 2 接口返回:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPgXeSCz-1621556997443)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506111535755.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mtGDeR1b-1621556997445)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506111346852.png)]
簇点链路 用来显示 服务锁监控的api
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vp6Cx04S-1621556997448)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506111916800.png)]
流控设置
簇点链路 点击具体访问的api 然后点击 流控 按钮
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZIQ51cYn-1621556997450)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506113005337.png)]
资源名称:为我们接口的API /index/test
**针对来源:**这里是默认的default(标示不针对来源),还有一种情况就是
假设微服务A需要调用这个资源,微服务B也需要调用这个资源,那么我们就可以单独的为微服务A和微服务B进行设置阈值。
阈值类型: 分为QPS和线程数 假设阈值为2
**QPS类型:**只得是每秒钟访问接口的次数>2就进行限流
**线程数:**为接受请求该资源 分配的线程数>2就进行限流.[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XXkKyP1d-1621556997451)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506114453078.png)]
流控模式:
直接 : 这种很好理解,就是达到设置的阈值后直接被流控抛出异常
疯狂的请求这个路径, 当达到 阈值的时候会报错
关联 :业务场景 我们现在有二个api,第一个是 查询接口
,假设我们希望优先操
作是保存接口
/**
*:模仿 流控模式【关联】 读接口
* @author: 余浪
* @Date: 2021/3/10 14:33
* @Version: 1.0.0
**/
@GetMapping("/test")
@Inner(value =false)
public Object test() {
return userName + sequence.nextValue() +" >>> " +sequence.nextValue() +"------------";
}
/**
* 模仿流控模式【关联】 写接口(优先)
* @author: 余浪
* @Date: 2021/3/10 14:33
* @Version: 1.0.0
**/
@GetMapping("/save")
@Inner(value =false)
public Object save() {
return "success";
}
测试代码: 写一个for循环一直调用我们的写接口,让写接口QPS达到阈值
@GetMapping("/batch")
@Inner(value = false)
public void batch() throws InterruptedException {
RestTemplate restTemplate =new RestTemplate();
for (int i = 0; i < 1000; i++) {
restTemplate.getForEntity("http://localhost:7020/index/save", String.class);
Thread.sleep(200);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sOWPXEyr-1621556997452)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506115440921.png)]
先访问:http://localhost:7020/index/batch,
在访问 http://localhost:7020/index/test, 被限流了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSEODgXc-1621556997454)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506135120394.png)]
链路 (用法说明,本地实验没成功 用alibaba 未毕业版本0.9.0可以测试出效果 API级
别的限制流量)
测试代码
编写一个service类:
public interface SysLogService extends IService {
int selectCount();
}
@Service
public class SysLogServiceImpl extends ServiceImpl implements SysLogService {
/**
* SentinelResource 定义流控资源
/
@Override
@SentinelResource("message")
public int selectCount() {
return 1;
}
}
controller中分别定义2个接口调用
/**
* 测试链路 流控
* @author: 余浪
* @Date: 2021/3/10 14:33
* @Version: 1.0.0
**/
@GetMapping("/findAll")
public Object findAll(){
int count = sysLogService.selectCount();
return count;
}
/**
* 测试链路 流控
* @author: 余浪
* @Date: 2021/3/10 14:33
* @Version: 1.0.0
**/
@GetMapping("/findOne")
public Object findOne(){
int count = sysLogService.selectCount();
return count;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpkqkJvw-1621556997456)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506142704985.png)]
从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应SCA的2.1.1.RELEASE),官方在CommonFilter 引入了
WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。
SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛
我们当前使用的版本是SpringCloud Alibaba 2.1.0.RELEASE,无法实现链路限流。
目前官方还未发布SCA 2.1.2.RELEASE,所以我们只能使用2.1.1.RELEASE,需要写代码的形式实
现
流控效果:
①:快速失败(直接抛出异常) 每秒的QPS 操过1 就直接抛出异常
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9fwGhCT-1621556997457)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506172731744.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJ42XC2j-1621556997460)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506172744270.png)]
②:预热(warmUp)
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系
统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到
达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
冷加载因子: codeFactor 默认是3
默认 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SI2DZzZm-1621556997463)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506173127277.png)]
上图设置: 就是QPS从3/3 = 1开始算 经过10秒钟,到达3 的QPS 才进行限制流量
详情文档:https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81—
%E5%86%B7%E5%90%AF%E5%8A%A8
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
③:排队等待
这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会
把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,
而不是拒绝所有请求。
选择排队等待的阈值类型必须是QPS
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jO4qMDt7-1621556997464)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506173316929.png)]
单机阈值: 10表示 每秒通过的请求个数是10,那么每隔100ms通过一次请求.
每次请求的最大等待时间为1000=1s,超过1S就丢弃请求。
具体文档:
https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6-
%E5%8C%80%E9%80%9F%E6%8E%92%E9%98%9F%E6%A8%A1%E5%BC%8F
具体源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
降级规则
①:rt(平局响应时间)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rDs6fuSH-1621556997468)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507081626398.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQzURt4R-1621556997469)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507081611919.png)]
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒
级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中
的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛
出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作
4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置
②:异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中
的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s
为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% -
100%。
测试代码:
int a =1;
@GetMapping("/ex")
public String ex(){
a++;
if(a > 3){
throw new RuntimeException("非法运算");
}
return sequence.nextNo();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cq65yI5-1621556997472)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507083314169.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eRoyEslf-1621556997475)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507083302024.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trubRUet-1621556997477)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507083419969.png)]③:异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后
会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再
进入熔断状态。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GvaCgSFL-1621556997478)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507083615131.png)]
热点参数:
业务场景: 秒杀业务,比如商城做促销秒杀,针对苹果11(商品id=5)进行9.9秒杀活动,那么这个时候,我们去请
求订单接口(商品id=5)的请求流量十分大,我们就可以通过热点参数规则来控制
商品id=5的请求的并发量。而其他正常商品的请求不会收到限制。那么
这种热点参数规则很使用 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZCq0uua-1621556997479)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507084754043.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-846UuyXS-1621556997480)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507084946172.png)]
热点规则的高级使用,修改刚刚新建的热点规则,配置 单独参数的QPS,id=22的阈值超过5限流,其他的id超过1限流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8ba0d4T-1621556997482)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507085516184.png)]
授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
若配置白名单,则只有请求来源位于白名单内时才可通过;
若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Frczno08-1621556997483)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507090910793.png)]
上面的资源名和授权类型不难理解,但是流控应用怎么填写呢?
其实这个位置要填写的是来源标识,Sentinel提供了RequestOriginParser 接口来处理来源。只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser 的实现类去解析访问来源。
1)自定义来源处理规则
package com.cloud.common.sentinel.parser;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import javax.servlet.http.HttpServletRequest;
/**
* sentinel 请求头解析判断
*/
public class CloudHeaderRequestOriginParser implements RequestOriginParser {
/**
* Parse the origin from given HTTP request.
*
* @param request HTTP request
* @return parsed origin
*/
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
授权规则配置
这个配置的意思是只有serviceName=pc不能访问(黑名单)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBLodKz3-1621556997484)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507091214409.png)]
OpenFeign整合我们的Sentinel
1.在对应的feign-api项目中
com.cloud cloud-common-sentinel2.:在我们的Feign的声明式接口上添加fallback属性或者 fallbackFactory属
性
1)为我们添加fallbackFactory属性的api ,fallbackFactory属性可以处理我们的异常,建议统一使用fallbackFactory
package com.cloud.admin.api.feign;
import com.cloud.admin.api.entity.SysDeptRelation;
import com.cloud.admin.api.entity.SysRole;
import com.cloud.admin.api.fallbackFactory.RemoteDataScopeServiceFallBackFactory;
import com.cloud.common.core.constant.ServiceNameConstants;
import com.cloud.common.core.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* 远程数据权限调用接口
*/
@FeignClient(contextId = "remoteDataScopeService", value = ServiceNameConstants.ADMIN_SERVICE,
fallbackFactory = RemoteDataScopeServiceFallBackFactory.class)
public interface RemoteDataScopeService {
/**
* 通过角色ID 查询角色列表
*
* @param roleIdList 角色ID
* @return
*/
@PostMapping("/role/getRoleList")
R> getRoleList(@RequestBody List roleIdList);
/**
* 获取子级部门
*
* @param deptId 部门ID
* @return
*/
@GetMapping("/dept/getDescendantList/{deptId}")
R> getDescendantList(@PathVariable("deptId") Integer deptId);
}
package com.cloud.admin.api.fallbackFactory;
import com.cloud.admin.api.entity.SysDeptRelation;
import com.cloud.admin.api.entity.SysRole;
import com.cloud.admin.api.feign.RemoteDataScopeService;
import com.cloud.common.core.util.R;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author :yulang
* 降级服务,抛出异常 给返回默认值。防止服务雪崩
* @Date : 9:35
* @Version : 1.0.0
**/
@Component
@Slf4j
public class RemoteDataScopeServiceFallBackFactory implements FallbackFactory {
@Override
public RemoteDataScopeService create(Throwable throwable) {
return new RemoteDataScopeService() {
/**
* 通过角色ID 查询角色列表
* 可以做一些 其他的邮件通知等服务
如果通过fegin调用getRoleList 异常,会直接返回默认值
* @param roleIdList 角色ID
* @return
*/
@Override
public R> getRoleList(List roleIdList) {
log.error("getRoleList error :{}",throwable);
return R.ok(new ArrayList());
}
/**
* 获取子级部门
*
* @param deptId 部门ID
* @return
*/
@Override
public R> getDescendantList(Integer deptId) {
log.error("getDescendantList error :{}",throwable);
return R.ok(new ArrayList());
}
};
}
}
3.开启sentinel对fegin的支持
feign:
sentinel:
enabled: true
```
Sentinel 规则持久化
我们经过第四节课知道我们的Sentinel-dashboard配置的规则,在我们的微服
务以及控制台重启的时候就清空了,因为他是基于内存的. 4
推送模式 | 说明 | 优点 | 缺点 |
---|---|---|---|
原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource ) |
简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 |
Pull 模式 | 扩展写数据源(WritableDataSource ), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 |
简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 |
Push 模式 | 扩展读数据源(ReadableDataSource ),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 |
规则持久化;一致性;快速 | 引入第三方依赖 |
原始模式
如果不做任何修改,Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中:
这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
pull 模式的数据源(如本地文件、RDBMS 等)一般是可写入的。使用时需要在客户端注册数据源:将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry
中。以本地文件数据源为例:
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
String flowRulePath = "xxx";
ReadableDataSource> ds = new FileRefreshableDataSource<>(
flowRulePath, source -> JSON.parseObject(source, new TypeReference>() {})
);
// 将可读数据源注册至 FlowRuleManager.
FlowRuleManager.register2Property(ds.getProperty());
WritableDataSource> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerFlowDataSource(wds);
}
private String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。
这种实现方法好处是简单,不引入新的依赖,坏处是无法保证监控数据的一致性。
生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经 Sentinel 数据源推送至配置中心。这样的流程就非常清晰了:
我们提供了 ZooKeeper, Apollo, Nacos 等的动态数据源实现。以 ZooKeeper 为例子,如果要使用第三方的配置中心作为配置管理,您需要做下面的几件事情:
/sentinel_rules/{appName}/{ruleType}
,e.g. sentinel_rules/appA/flowRule
)。InMemFlowRuleStore
),可以对其进行改造使其支持应用维度的规则缓存(key 为 appName),每次添加/修改/删除规则都先更新内存中的规则缓存,然后需要推送的时候从规则缓存中获取全量规则,然后通过上面实现的 Client 将规则推送到 ZooKeeper 即可。从 Sentinel 1.4.0 开始,Sentinel 控制台提供 DynamicRulePublisher
和 DynamicRuleProvider
接口用于实现应用维度的规则推送和拉取,并提供了相关的示例。Sentinel 提供应用维度规则推送的示例页面(/v2/flow
),用户改造控制台对接配置中心后可直接通过 v2 页面推送规则至配置中心。改造详情可参考 应用维度规则推送示例。
部署多个控制台实例时,通常需要将规则存至 DB 中,规则变更后同步向配置中心推送规则。
PULL改造方案
微服务改造方案:
package com.cloud.common.sentinel.core;
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.csp.sentinel.util.ConfigUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* sentinel 持久化
* @author: 余浪
* @Date: 2021/3/10 14:33
* @Version: 1.0.0
**/
@Slf4j
@Component
public class PullModeByFileDataSource implements InitFunc {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
@Override
public void init() throws Exception {
log.info("time:{}读取配置",sdf.format(new Date()));
try {
Properties p = ConfigUtil.loadProperties("classpath:bootstrap.yml");
String applicationName = p.getProperty("name","defalut");
PersistenceRuleConstant.init(applicationName);
//创建文件存储目录
RuleFileUtils.mkdirIfNotExits(PersistenceRuleConstant.storePath,applicationName);
//创建规则文件
RuleFileUtils.createFileIfNotExits(PersistenceRuleConstant.rulesMap);
dealFlowRules();
dealDegradeRules();
dealSystemRules();
dealParamFlowRules();
dealAuthRules();
}catch (Exception e) {
log.error("错误原因:{}",e);
}
}
/**
* 方法实现说明:处理流控规则逻辑
* @author:smlz
* @return: void
* @exception: FileNotFoundException
* @date:2019/11/29 13:26
*/
private void dealFlowRules() throws FileNotFoundException {
String ruleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.FLOW_RULE_PATH).toString();
//创建流控规则的可读数据源
/* ReadableDataSource> flowRuleRDS = new FileRefreshableDataSource(
ruleFilePath,RuleListParserUtils.flowRuleListParser
);*/
ReadableDataSource> flowRuleRDS = new FileRefreshableDataSource(
new File(ruleFilePath),RuleListParserUtils.flowRuleListParser,20000,1024 * 1024, Charset.forName("utf-8")
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource> flowRuleWDS = new FileWritableDataSource>(
ruleFilePath, RuleListParserUtils.flowFuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
}
/**
* 方法实现说明:处理降级规则
* @author:smlz
* @return:void
* @exception: FileNotFoundException
* @date:2019/11/29 13:42
*/
private void dealDegradeRules() throws FileNotFoundException {
//讲解规则文件路径
String degradeRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.DEGRAGE_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource> degradeRuleRDS = new FileRefreshableDataSource(
degradeRuleFilePath,RuleListParserUtils.degradeRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource> degradeRuleWDS = new FileWritableDataSource<>(
degradeRuleFilePath, RuleListParserUtils.degradeRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
}
/**
* 方法实现说明:处理系统规则
* @author:smlz
* @return:void
* @exception: FileNotFoundException
* @date:2019/11/29 13:42
*/
private void dealSystemRules() throws FileNotFoundException {
//讲解规则文件路径
String systemRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.SYSTEM_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource> systemRuleRDS = new FileRefreshableDataSource(
systemRuleFilePath,RuleListParserUtils.sysRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource> systemRuleWDS = new FileWritableDataSource<>(
systemRuleFilePath, RuleListParserUtils.sysRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
}
/**
* 方法实现说明:热点参数规则
* @author:smlz
* @return:
* @exception:
* @date:2019/11/29 13:50
*/
private void dealParamFlowRules() throws FileNotFoundException {
//讲解规则文件路径
String paramFlowRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.HOT_PARAM_RULE).toString();
//创建流控规则的可读数据源
ReadableDataSource> paramFlowRuleRDS = new FileRefreshableDataSource(
paramFlowRuleFilePath,RuleListParserUtils.paramFlowRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRuleFilePath, RuleListParserUtils.paramRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
/**
* 方法实现说明:授权规则
* @author:smlz
* @return:
* @exception:
* @date:2019/11/29 13:56
*/
private void dealAuthRules() throws FileNotFoundException {
//讲解规则文件路径
String authFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.AUTH_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource> authRuleRDS = new FileRefreshableDataSource(
authFilePath,RuleListParserUtils.authorityRuleParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
AuthorityRuleManager.register2Property(authRuleRDS.getProperty());
WritableDataSource> authRuleWDS = new FileWritableDataSource<>(
authFilePath, RuleListParserUtils.authorityEncoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerAuthorityDataSource(authRuleWDS);
}
}
基于SPI配置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFYtTYlj-1621556997490)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210506172145575.png)]
启动项目这样 配置的流控规则,在项目启动后 就不会失效。
网关服务,主要使用做同一路由转发,和动态路由(新增加一个服务不需要重启网关服务),灰度发布等功能。网关工程已经统一配置好,直接拉取代码直接使用即可
1.基于 gateway+ nacos 的动态路由规则配置,依赖
org.springframework.cloud
spring-cloud-gateway-core
org.springframework.boot
spring-boot-starter-data-redis-reactive
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
本项目采用的是基于配置文件开关配置
#nacos动态路由配置
nacos:
gateway:
route:
config:
data-id: gateway-routes ##动态路由规则id
group: DEFAULT_GROUP ## 分组信息
dynamicRoute:
#是否开启动态网关路由
enabled: true ## true表示开启动态路由,false 默认按照网关配置文件配置
动态路由配置的代码实现
/**
* 动态路由配置
*/
@Configuration
public class GatewayConfig {
public static final long DEFAULT_TIMEOUT = 30000;
public static String NACOS_SERVER_ADDR;
public static String NACOS_NAMESPACE;
public static String NACOS_ROUTE_DATA_ID;
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr){
NACOS_SERVER_ADDR = nacosServerAddr;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNacosNamespace(String nacosNamespace){
NACOS_NAMESPACE = nacosNamespace;
}
@Value("${nacos.gateway.route.config.data-id}")
public void setNacosRouteDataId(String nacosRouteDataId){
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
}
@Value("${nacos.gateway.route.config.group}")
public void setNacosRouteGroup(String nacosRouteGroup){
NACOS_ROUTE_GROUP = nacosRouteGroup;
}
}
package com.cloud.gateway.route;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 动态更新路由网关service
* 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
* 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
*/
@Slf4j
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
/**
* 发布事件
*/
@Autowired
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 删除路由
* @param id
* @return
*/
public String delete(String id) {
try {
log.info("gateway delete route id {}",id);
this.routeDefinitionWriter.delete(Mono.just(id));
return "delete success";
} catch (Exception e) {
return "delete fail";
}
}
/**
* 更新路由
* @param definition
* @return
*/
public String update(RouteDefinition definition) {
try {
log.info("gateway update route {}",definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
return "update fail,not find route routeId: "+definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}
/**
* 增加路由
* @param definition
* @return
*/
public String add(RouteDefinition definition) {
log.info("gateway add route {}",definition);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
}
package com.cloud.gateway.route;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.cloud.gateway.config.dynamic.GatewayConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
/**
* 通过nacos下发动态路由配置,监听Nacos中gateway-route配置
*
*/
@Component
@Slf4j
@ConditionalOnProperty(prefix = "nacos.gateway.dynamicRoute", name = "enabled", havingValue = "true")
@DependsOn({"gatewayConfig"}) // 依赖于gatewayConfig bean
public class DynamicRouteServiceImplByNacos {
@Autowired
private DynamicRouteServiceImpl dynamicRouteService;
private ConfigService configService;
@PostConstruct
public void init() {
log.info("初始化网关路由。。。");
try{
configService = initConfigService();
if(configService == null){
log.warn("initConfigService fail");
return;
}
String configInfo = configService.getConfig(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP, GatewayConfig.DEFAULT_TIMEOUT);
if(StrUtil.isNotBlank(configInfo)) {
log.info("获取网关当前配置:\r\n{}", configInfo);
List definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition definition : definitionList) {
log.info("update route : {}", definition.toString());
dynamicRouteService.add(definition);
}
}else{
log.error("没有从nacos获取到网关路由信息,请检查!");
}
} catch (Exception e) {
log.error("初始化网关路由时发生错误",e);
}
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,GatewayConfig.NACOS_ROUTE_GROUP);
}
/**
* nacos config 监听Nacos下发的动态路由配置
* @param dataId
* @param group
*/
public void dynamicRouteByNacosListener (String dataId, String group){
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("进行网关更新:\n\r{}",configInfo);
List definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
for(RouteDefinition definition : definitionList){
log.info("update route : {}",definition.toString());
dynamicRouteService.update(definition);
}
}
@Override
public Executor getExecutor() {
log.info("getExecutor\n\r");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!",e);
}
}
/**
* 初始化网关路由 nacos config
* @return
*/
private ConfigService initConfigService(){
try{
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace",GatewayConfig.NACOS_NAMESPACE);
return configService= NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误",e);
return null;
}
}
}
网关中添加配置,只需要在nacos配置中心修改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dw7WYbxa-1621556997493)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507131938224.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVz19NJw-1621556997495)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507132057323.png)]
点击发布 网关会自动刷新路由信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7f7VqoM4-1621556997496)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507132313748.png)]
我们也可以在nacos中的监听查询列表中查询
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1MT1oiyY-1621556997497)(C:\Users\yulan\AppData\Roaming\Typora\typora-user-images\image-20210507132429228.png)]
我们现在需要解决生产环境金丝雀发布问题
比如 server-A 存在二个版本 V1(老版本) V2(新版
本),server-B 也存在二个版本V1(老版本) V2新版本 现在需要
做到的是
server-A(V1)---->server-B(v1),server-A(V2)—
->server-B(v2)。记住v2版本是小面积部署的,用来测试用
户对新版本功能的。若用户完全接受了v2。我们就可以把V1版本卸载
完全部署V2版本
@Slf4j(topic = "theSameClusterPriorityWithVersionRule")
public class GrayTheSameClusterPriorityWithVersionRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
try {
//获取本地所部署集群的名称 NJ-CLUSTER
String localClusterName = nacosDiscoveryProperties.getClusterName();
//去nacos上获取和本地 相同集群 相同版本的所有实例信息
List theSameClusterNameAndTheSameVersionInstList = getTheSameClusterAndTheSameVersionInstances(nacosDiscoveryProperties);
//声明被调用的实例
Instance toBeChooseInstance;
//判断同集群同版本号的微服务实例是否为空
if (theSameClusterNameAndTheSameVersionInstList.isEmpty()) {
//跨集群调用相同的版本
toBeChooseInstance = crossClusterAndTheSameVersionInovke(nacosDiscoveryProperties);
} else {
//具有同集群 同版本号的实例
toBeChooseInstance = CustomerWeightedBalancer.chooseInstanceByRandomWeight(theSameClusterNameAndTheSameVersionInstList);
log.info("同集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
localClusterName, toBeChooseInstance.getClusterName(), nacosDiscoveryProperties.getMetadata().get("version"),
toBeChooseInstance.getMetadata().get("version"), toBeChooseInstance.getIp(), toBeChooseInstance.getPort());
}
return new NacosServer(toBeChooseInstance);
} catch (NacosException e) {
log.error("同集群优先权重负载均衡算法选择异常:{}", e);
return null;
}
}
/**
* 方法实现说明:获取相同集群下,相同版本的 所有实例
*
* @param nacosDiscoveryProperties nacos的配置
* @author:smlz
* @return: List
* @exception: NacosException
*/
private List getTheSameClusterAndTheSameVersionInstances(NacosDiscoveryProperties nacosDiscoveryProperties) throws NacosException {
//当前的集群的名称
String currentClusterName = nacosDiscoveryProperties.getClusterName();
//当前的版本号
String currentVersion = nacosDiscoveryProperties.getMetadata().get("version");
//获取所有实例的信息(包括不同集群的,不同版本号的)
List allInstance = getAllInstances(nacosDiscoveryProperties);
List theSameClusterNameAndTheSameVersionInstList = new ArrayList<>();
//过滤相同集群 同版本号的实例
for (Instance instance : allInstance) {
if (StringUtils.endsWithIgnoreCase(instance.getClusterName(), currentClusterName) &&
StringUtils.endsWithIgnoreCase(instance.getMetadata().get("version"), currentVersion)) {
theSameClusterNameAndTheSameVersionInstList.add(instance);
}
}
return theSameClusterNameAndTheSameVersionInstList;
}
/**
* 方法实现说明:获取被调用服务的所有实例
*
* @param nacosDiscoveryProperties nacos的配置
* @author:smlz
* @return: List
* @exception: NacosException
* @date:2019/11/21 16:42
*/
private List getAllInstances(NacosDiscoveryProperties nacosDiscoveryProperties) throws NacosException {
//第1步:获取一个负载均衡对象
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) getLoadBalancer();
//第2步:获取当前调用的微服务的名称
String invokedSerivceName = baseLoadBalancer.getName();
//第3步:获取nacos clinet的服务注册发现组件的api
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
//第4步:获取所有的服务实例
List allInstance = namingService.getAllInstances(invokedSerivceName);
return allInstance;
}
/**
* 方法实现说明:跨集群环境下 相同版本的
*
* @param nacosDiscoveryProperties
* @author:smlz
* @return: List
* @exception: NacosException
* @date:2019/11/21 17:11
*/
private List getCrossClusterAndTheSameVersionInstList(NacosDiscoveryProperties nacosDiscoveryProperties) throws NacosException {
//版本号
String currentVersion = nacosDiscoveryProperties.getMetadata().get("version");
//被调用的所有实例
List allInstance = getAllInstances(nacosDiscoveryProperties);
List crossClusterAndTheSameVersionInstList = new ArrayList<>();
//过滤相同版本
for (Instance instance : allInstance) {
if (StringUtils.endsWithIgnoreCase(instance.getMetadata().get("version"), currentVersion)) {
crossClusterAndTheSameVersionInstList.add(instance);
}
}
return crossClusterAndTheSameVersionInstList;
}
private Instance crossClusterAndTheSameVersionInovke(NacosDiscoveryProperties nacosDiscoveryProperties) throws NacosException {
//获取所有集群下相同版本的实例信息
List crossClusterAndTheSameVersionInstList = getCrossClusterAndTheSameVersionInstList(nacosDiscoveryProperties);
//当前微服务的版本号
String currentVersion = nacosDiscoveryProperties.getMetadata().get("version");
//当前微服务的集群名称
String currentClusterName = nacosDiscoveryProperties.getClusterName();
//声明被调用的实例
Instance toBeChooseInstance = null;
//没有对应相同版本的实例
if (crossClusterAndTheSameVersionInstList.isEmpty()) {
log.info("跨集群调用找不到对应合适的版本当前版本为:currentVersion:{}", currentVersion);
throw new RuntimeException("找不到相同版本的微服务实例");
} else {
toBeChooseInstance = CustomerWeightedBalancer.chooseInstanceByRandomWeight(crossClusterAndTheSameVersionInstList);
log.info("跨集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
currentClusterName, toBeChooseInstance.getClusterName(), nacosDiscoveryProperties.getMetadata().get("current-version"),
toBeChooseInstance.getMetadata().get("current-version"), toBeChooseInstance.getIp(), toBeChooseInstance.getPort());
}
return toBeChooseInstance;
}
}