一、下载 Sentinel
下载地址:https://github.com/alibaba/Sentinel/releases
下面提供网盘下载链接,觉得官网下载慢的,可以点击网盘下载(sentinel-dashboard-1.8.1.jar )
链接:https://pan.baidu.com/s/1V1-03IBN28DUutDBiJl1NQ
提取码:sdhp
二、在地址栏中输入cmd,打开命令窗口,执行启动命令
输入命令 ,启动sentinel(默认8080端口不能被占用,可以通过启动命令更改端口)
java -jar sentinel-dashboard-1.8.1.jar --server.port=8080
其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080
如遇到下图错误,请保证jdk安装正确,jdk环境变量配置无误,继续报错时进入到 C:\Program Files (x86)\Common Files\Oracle\Java\javapath 目录下删除此文件下的java.exe,javaw.exe,javaws.exe三个文件。
三、浏览器输入:localhost:8080,默认账号密码都是sentinel
注意:只有1.6.0及以上版本,才有这个简单的登录页面。默认用户名和密码都是sentinel。对于用户登录的相关配置可以在启动命令中增加下面的参数来进行配置:
- -Dsentinel.dashboard.auth.username=sentinel : 用于指定控制台的登录用户名为 sentinel;
- -Dsentinel.dashboard.auth.password=123456 : 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel
-
-Dserver.servlet.session.timeout=7200 : 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;
输入账户密码登录后,进入Sentinel首页,登录成功
四、gateway网关模块集成Sentinel
修改pom.xml文件,上一章节中 我们已经引入过了,所以无需再次引入,这里只提供参考
<!--GateWay 网关-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<!--sentinel 核心环境 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel支持采用 Nacos 作为规则配置数据源,引入该适配依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改bootstrap.yml文件
配置文件中增加 Sentinel 信息
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
完整bootstrap.yml文件
spring:
profiles:
active: test
---
server:
port: 8008
spring:
profiles: test
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: a60273f4-07fb-4568-82eb-d078a3b02107
config:
server-addr: 127.0.0.1:8848
namespace: a60273f4-07fb-4568-82eb-d078a3b02107
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
---
server:
port: 8008
spring:
profiles: pre
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: c60d2198-0b2f-46c1-82cb-4c2f20fb8123
config:
server-addr: 127.0.0.1:8848
namespace: c60d2198-0b2f-46c1-82cb-4c2f20fb8123
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
---
server:
port: 8008
spring:
profiles: prd
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: 0be74aa4-00e5-4c48-ae8c-34965c327212
config:
server-addr: 127.0.0.1:8848
namespace: 0be74aa4-00e5-4c48-ae8c-34965c327212
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
启动engine、gateway项目,刷新Sentinel控制台界面
可以看到 sentinel 已经对 gateway 进行了实时监控
五、制定Sentinel规则(这里我们只进行Sentinel的流控、熔断、和自定义异常处理)
1. 流控
新增接口 testFlow ,因为只是测试,这里调用engine方法不变
/**
* 测试流控规则
*/
@PostMapping("/testFlow")
public String testFlow() {
User user = userService.userInfo();
return JSON.toJSONString(user);
}
对测试接口 /api/user/testFlow 进行流控(每秒最多请求1次,用于测试)
发现Postman一秒内调用接口超过一次时,返回Blocked by Sentinel: FlowException 错误
如果把QPS改为线程数时,再请求接口,无论请求速率多么快,都不会进入限流回执
QPS 和线程数这两种方式的区别
2. 熔断降级
新增接口 testDegrade ,因为只是测试,这里调用engine方法不变
/**
* 测试降级规则
*/
@PostMapping("/testDegrade")
public String testDegrade() {
User user = userService.userInfo();
return JSON.toJSONString(user);
}
新增降级规则(这里用来测试,用慢调用比例,实际根据自身业务选择即可)
Postman调用接口一秒内调用多次时,会触发降级等候
3. 自定义 sentinel 异常处理
这里需要用到 @SentinelResource 注解,和新增 UserBlockHandler.class 类
修改 UserController.class
@RestController
@RequestMapping("/api/user")
public class UserController {
@Reference
private UserService userService;
/**
* 获取用户信息
*/
@PostMapping("/userInfo")
public String userInfo() {
User user = userService.userInfo();
return JSON.toJSONString(user);
}
/**
* 测试流控规则
*/
@PostMapping("/testFlow")
@SentinelResource(value = "user-testFlow",
blockHandlerClass = UserBlockHandler.class, //对应异常类
blockHandler = "handleException", //只负责sentinel控制台配置违规
fallback = "handleError", //只负责业务异常
fallbackClass = UserBlockHandler.class)
public String testFlow() {
User user = userService.userInfo();
return JSON.toJSONString(user);
}
/**
* 测试降级规则
*/
@PostMapping("/testDegrade")
@SentinelResource(value = "user-testDegrade",
blockHandlerClass = UserBlockHandler.class, //对应异常类
blockHandler = "handleException", //只负责sentinel控制台配置违规
fallback = "handleError", //只负责业务异常
fallbackClass = UserBlockHandler.class)
public String testDegrade() {
User user = userService.userInfo();
return JSON.toJSONString(user);
}
}
UserBlockHandler.class(这里演示Sentinel通用判断清晰,所以写在了一个方法里,实战中最好每一个接口都有自己单独的异常处理)
package com.bi.cloud.controller;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
public class UserBlockHandler {
public static String handleException(BlockException ex) {
HashMap<String, Object> map = new HashMap<>();
if (ex instanceof FlowException) {
map.put("code", -1);
map.put("msg", "系统限流,请稍等");
} else if (ex instanceof DegradeException) {
map.put("code", -2);
map.put("msg", "降级了");
} else if (ex instanceof ParamFlowException) {
map.put("code", -3);
map.put("msg", "热点参数限流");
} else if (ex instanceof SystemBlockException) {
map.put("code", -4);
map.put("msg", "系统规则(负载/...不满足要求)");
} else if (ex instanceof AuthorityException) {
map.put("code", -5);
map.put("msg", "授权规则不通过");
}
return JSON.toJSONString(map);
}
public static String handleError() {
HashMap<String, Object> map = new HashMap<>();
map.put("code", 500);
map.put("msg", "系统异常");
return JSON.toJSONString(map);
}
}
重启gateway,这时我们发现Sentinel配置会全部重置,是因为我们没有配置Sentinel持久化的问题
这里有一个问题说明,当自定义异常处理时,Sentinel 配置资源名需要写 @SentinelResource(value内的内容),如果还写 @PostMapping 内的路径,则捕捉不到异常
资源名需引用@SentinelResource value
流控
Postman 流控接口测试,返回自定义异常
熔断降级
Postman 熔断降级接口测试,返回自定义异常
六、Sentinel 规则持久化
1. 修改pom.xml文件,上章节中已有加载,所以这里不需添加,只用来展示参考
<!-- Sentinel支持采用 Nacos 作为规则配置数据源,引入该适配依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2. 修改bootstrap.yml,这里以test环境为例
配置详情
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-flow-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: flow #类型来自RuleType类 - 流控规则
degrade:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-degrade-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: degrade #类型来自RuleType类 - 熔断规则
完整bootstrap.yml文件
spring:
profiles:
active: test
---
server:
port: 8008
spring:
profiles: test
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: a60273f4-07fb-4568-82eb-d078a3b02107
config:
server-addr: 127.0.0.1:8848
namespace: a60273f4-07fb-4568-82eb-d078a3b02107
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-flow-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: flow #类型来自RuleType类 - 流控规则
degrade:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-degrade-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: degrade #类型来自RuleType类 - 熔断规则
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
---
server:
port: 8008
spring:
profiles: pre
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: c60d2198-0b2f-46c1-82cb-4c2f20fb8123
config:
server-addr: 127.0.0.1:8848
namespace: c60d2198-0b2f-46c1-82cb-4c2f20fb8123
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-flow-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: flow #类型来自RuleType类 - 流控规则
degrade:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-degrade-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: degrade #类型来自RuleType类 - 熔断规则
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
---
server:
port: 8008
spring:
profiles: prd
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: 0be74aa4-00e5-4c48-ae8c-34965c327212
config:
server-addr: 127.0.0.1:8848
namespace: 0be74aa4-00e5-4c48-ae8c-34965c327212
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
sentinel:
transport:
dashboard: 127.0.0.1:8080 #配置Sentinel dashboard地址
port: 8719 #这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与 Sentinel 控制台做交互
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-flow-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: flow #类型来自RuleType类 - 流控规则
degrade:
nacos:
server-addr: 127.0.0.1:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: cloud-sentinel-degrade-service #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
namespace: a60273f4-07fb-4568-82eb-d078a3b02107 #Nacos 命名空间的ID
data-type: json #配置文件类型
rule-type: degrade #类型来自RuleType类 - 熔断规则
gateway:
# 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
routes:
- id: bi-cloud-oauth # 当前路由的标识, 要求唯一
uri: lb://bi-cloud-oauth # lb指的是从 nacos 中按照名称获取微服务,并遵循负载均衡策略
predicates: # 断言(就是路由转发要满足的条件)
- Path=/oauth/** # 当请求路径满足Path指定的规则时,才进行路由转发
# 我们⾃定义的路由 ID,保持唯⼀
- id: bi-cloud-gateway
# ⽬标服务地址(部署多实例)
uri: lb://bi-cloud-gateway
# gateway⽹关从服务注册中⼼获取实例信息然后负载后路由
# 断⾔:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默认⽅法来将 Predicate 组合成其他复杂的逻辑(⽐如:与,或,⾮)。
predicates:
- Path=/bi-gateway/api/**
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
application:
name: bi-cloud-gateway
rule-type 来自RuleType类,类型有:如下
flow:流控规则
degrade:降级规则
param_flow:热点规则
system:系统规则
authority: 授权规则
添加Nacos业务流控规则配置 - cloud-sentinel-flow-service
json配置:配置内容默认是数组格式,如果要配置多条,可以用 “,” 号隔开各个 “{}”
[
{
"resource": "user-testFlow",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
添加Nacos业务熔断规则配置 - cloud-sentinel-degrade-service
json配置:配置内容默认是数组格式,如果要配置多条,可以用 “,” 号隔开各个 “{}”
[
{
"resource": "user-testDegrade",
"grade": 0,
"count": 2,
"timeWindow": 3,
"minRequestAmount": 5,
"statIntervalMs": 1000,
"slowRatioThreshold":1
}
]
重新启动gateway,使 bootstrap.yml 配置生效
刷新Sentinel 控制台,发现流控配置已经自动注入,有改动时,修改Nacos配置文件无需重新启动gateway,刷新Sentinel实时生效
第七章 RocketMQ 消息发布与订阅服务集成 https://www.jianshu.com/p/e56bd2dcab26
参考文献:
https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7%E8%A7%84%E5%88%99-degraderule
https://blog.csdn.net/weixin_44757206/article/details/107119085
https://blog.csdn.net/qq_39668819/article/details/109215156