SpringCloud
SpringCloud
描述:一套完整的微服务解决方案,一系列不同功能的微服务框架的集合。
类别:Netflix(奈飞),Alibaba
源码:https://github.com/qiyisoft/sca
各个组件
- Nacos
- Ribbon
- OpenFeign
- GateWay
- Sentinel
- SkyWalking
- Seata
- 分布式锁
- 认证与授权 OAuth2+JWT+Security
1.注册中心Nacos
特性,Nacos使用,Nacos的心跳机制和健康检查
特性
出现的背景:在以往单实例情况下,服务间通常采用点对点通信,即采用 IP+端口+接口的形式直接调用。考虑避免单点负载压力过大以及高可用的性能要求,通常会部署多实例节点保障系统的性能,但增加多实例后,调用方该如何选择哪个服务提供者进行处理呢?还有当服务提供者出现故障后,如何将后续请求转移到其他可用实例上呢?
功能:
服务发现与管理
动态配置服务
动态DNS服务
Nacos注册中心使用
服务端部署
环境准备
操作系统CentOS7
安装JDK8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16yum -y install java-1.8.0-openjdk-devel.x86_64 # 默认安装位置: /usr/lib/jvm/
# 安装成功后验证Java版本
java -version
# 编辑profile配置 JAVA_HOME 环境变量
[root@server-1 ~]# vim /etc/profile
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.272.b10-1.el7_9.x86_64
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
# source:在当前bash环境下读取并执行FileName中的命令。
[root@server-1 ~]# source /etc/profile
# 验证配置是否正确 ? java javac
[root@server-1 ~]# echo $JAVA_HOME
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.272.b10-1.el7_9.x86_64
安装并启动
获取安装包
访问 Nacos GitHub:https://github.com/alibaba/nacos/releases/获取 Nacos 安装包 nacos-server-1.4.0.tar.gz。
上传&解压缩
1
[root@server-1 local]# tar -xvf nacos-server-1.4.0.tar.gz
启动
1
2
3[root@server-1 local]# cd nacos/bin
# 以单点方式启动 Nacos
[root@server-1 bin]# sh startup.sh -m standaloneNacos默认以后台模式启动,利用 tail 命令查看启动日志。
1
2
3
4
5
6[root@server-1 bin]# tail -f /usr/local/nacos/logs/start.out
2020-12-06 21:03:18,759 INFO Tomcat started on port(s): 8848 (http) with context path '/nacos'
2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/local/nacos/nacos/logs
2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/loca/nacos/nacos/conf
2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/local/nacos/nacos/data
2020-12-06 21:03:18,767 INFO Nacos started successfully in stand alone mode. use embedded storage对外开放7848/8848端口
8848 端口是 Nacos 对客户端提供服务的端口,7848 是 Nacos 集群通信端口,用于Nacos 集群间进行选举,检测等。
1
2
3
4
5
6[root@server-1 bin]# firewall-cmd --zone=public --add-port=8848/tcp --permanent
success
[root@server-1 bin]# firewall-cmd --zone=public --add-port=7848/tcp --permanent
success
[root@server-1 bin]# firewall-cmd --reload
success从浏览器进入管理界面
http://192.168.31.102:8848/nacos
账号&密码 nacos&nacos
服务管理->服务列表,用于查看已注册微服务列表。

微服务接入nacos
创建项目
idea工具,创建项目,选择Spring InitiaLizr
Custom可以选择阿里云镜像地址 http://start.aliyun.com

添加依赖
1
2
3
4
5
6
7
8
9
10
11<!-- nacos依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- web 使微服务具备http相应能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>配置nacos相关属性
1
2
3
4
5
6
7
8
9# 应用名称,默认也是在微服务中注册的微服务 ID
spring.application.name=sample-service
# 配置 Nacos 服务器的IP地址
spring.cloud.nacos.discovery.server-addr=192.168.31.102:8848
#连接 Nacos 服务器使用的用户名、密码,默认为 nacos
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discvery.password=nacos
#微服务提供Web服务的端口号
server.port=8081启动项目
启动日志
1
2
3
4
5
6#Web 服务端口号 8081
INFO 14188 o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
#微服务向 Nacos 注册成功,微服务 ID:sample-service
INFO 14188 c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP sample-service 192.168.47.1:8081 register finished
#微服务启动成功
INFO 14188 c.l.s.SampleServiceApplication : Started SampleServiceApplication in 4.911 seconds (JVM running for 6.039)nacos查看刚刚注册的项目
浏览器打开 http://192.168.31.102:8848/nacos ,服务管理-服务列表

Nacos配置中心
介绍
场景:
- 动态配置,无需重启服务
- 多实例批量修改
- 版本管理
- 配置文件多环境切换
- 变更推送
- 监听查询?
微服务应用只持有应用启动的最小化配置,在应用启动时微服务应用所需的其他配置数据,诸如数据库连接字符串、各种用户名密码、IP 等信息均从配置中心远程下载。书写应用配置时不直接写入 application.yml 配置,而是直接在配置中心提供的 UI 进行设置。
使用
部署配置中心
下载Nacos
1
tar -xvf nacos-server-1.4.0.tar.gz
配置数据库,执行/nacos/conf/nacos-mysql.sql

配置Nacos数据源
打开 /usr/local/nacos/conf/application.properties,36行
1
2
3
4
5
6### Count of DB: 数据库总数
db.num=1
### Connect URL of DB: 数据库连接,根据你的实际情况调整
db.url.0=jdbc:mysql://192.168.31.10:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=root添加所有 Nacos 集群节点 IP 及端口
1
2
3
4
5# 复制命令创建 cluster.conf 文件
cp cluster.conf.example cluster.conf
# 打开 cluster.conf,添加所有 Nacos 集群节点 IP 及端口
vim cluster.conf
192.168.31.10:8848按集群模式启动 Nacos
1
sh /usr/local/nacos/bin/startup.sh
访问配置页面
微服务接入配置中心
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- Spring Boot Web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos注册中心starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos配置中心starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>配置文件
bootstrap.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# “微服务 id”-“环境名”.“文件扩展名” 三部分组合为有效的 data id,即order-service-dev.yml。data id 要和 Nacos 的设置大小写保持完全一致,这样在微服务启动时便自动会从 Nacos配置中心获取 order-service-dev.yml 配置并下载到本地完成启动过程。
spring:
application:
name: order-service #微服务id
profiles:
active: dev #环境名
cloud:
nacos:
config: #Nacos配置中心配置
file-extension: yml #文件扩展名
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
logging: #开启debug日志,本地使用
level:
root: debugcontroller
1
2
3
4
5
6
7
8
9
10
11@RestController
public class TestController {
@Value("${custom.flag}")
private String flag;
@Value("${custom.database}")
private String database;
@GetMapping("/test")
public String test(){
return "flag:" + flag + "<br/> database:" + database;
}
}配置中心配置

Data ID,Group,描述,说明,配置格式,配置内容
Data ID:配置的唯一标识,格式固定为:{微服务id}-{环境名}.yml,这里填写 order-service-dev.yml,其中 dev 就是环境名代表这个配置文件是 order-service 的开发环境配置文件。
Group:指定配置文件的分组,这里设置默认分组 DEFAULT_GROUP 即可。
描述:说明 order-service-dev.yml 配置文件的用途。
配置格式:指定“配置内容”的类型,这里选择 YAML 即可。
配置内容:

点击右下角的“发布”按钮完成设置。

nacos_config 数据库的 config_info 表中也出现了对应配置数据。

启动服务并访问接口
1
2
3
4http://localhost:8000/test
结果如下:
flag:development
database:192.168.10.31
高级配置
####### 热加载
为了支持热加载,服务 A 的程序针对热加载需要作出如下变动:
第一,配置数据必须被封装到单独的配置 Bean 中;
第二,这个配置 Bean 需要被 @Configuration 与 @RefreshScope 两个注解描述。
1 | @Configuration // 说明这是配置Bean |
controller
1 | @RestController |
启动应用后,修改配置文件,并发布.
日志立即产生重新加载的信息
1 | [192.168.31.10_8848] c.a.c.n.refresh.NacosContextRefresher : Refresh Nacos config group=DEFAULT_GROUP,dataId=order-service-dev.yml,configInfo=server: |
####### 切换配置文件
在 Nacos 中设置生产环境的配置,Data Id 为 order-service-prd.yml,其中 prd 是 production 的缩写,代表生产环境配置。

调整 order-service 的 bootstrap.yml 引导文件,最重要的地方是修改环境名为 prd,同时更换为生产环境 Nacos 的通信地址,打包后发布。
1 | spring: |
####### 管理基础配置数据
对比 order-service-dev.yml 与 order-service-prd.yml 发现,在不同环境的配置文件中普遍存在固定的配置项,例如:spring.application.name=order-service 配置项就是稳定的,且修改它会影响所有环境配置文件。对于这种基础的全局配置,我们可以将其存放到单独的 order-service.yml 配置中,在 order-service 服务启动时,这个不带环境名的配置文件必然会被加载。

1 | # order-service.yml |
1 | # order-service-dev.yml |
Nacos原理
心跳机制与健康检查
微服务与Nacos服务器通信过程:
微服务(客户端)启动后每过5秒,会由微服务内置的 Nacos 客户端主动向 Nacos 服务器发起心跳包(HeartBeat)。心跳包会包含当前服务实例的名称、IP、端口、集群名、权重等信息。

Nacos服务端收到心跳包后的处理
首先根据 IP 与端口判断 Nacos 是否存在该服务实例?如果实例信息不存在,在 Nacos 中注册登记该实例。而注册的本质是将新实例对象存储在“实例 Map”集合中;
如果实例信息已存在,记录本次心跳包发送时间;
设置实例状态为“健康”;
推送“微服务状态变更”消息;
返回心跳包时间间隔。

实例的不健康状态与剔除
1
2
3
4
5
6
7#心跳间隔。时间单位:秒。心跳间隔
spring.cloud.nacos.discovery.heart-beat-interval=3
# Nacos Server 默认每过 15 秒对“实例 Map”中的所有实例进行扫描,服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康
spring.cloud.nacos.discovery.heart-beat-timeout=15
# Nacos Server 默认每过 20 秒对“实例 Map”中的所有“非健康”实例进行扫描,如发现“非健康”实例,随即从“实例 Map”中将该实例删除。
spring.cloud.nacos.discovery.ip-delete-timeout=30
热加载原理
热加载介绍:Nacos 中支持配置热加载,在运行过程中允许直接对新的配置项进行重新加载而不需要手动重启。
配置中心长轮询机制:

Nacos 服务端收到请求之后,先检查配置是否发生了变更,如果没有,则设置一个定时任务,延期 29.5s 执行,并且把当前的客户端长轮询连接加入 allSubs 队列。这时候有两种方式触发该连接结果的返回:
• 第一种是在等待 29.5s 后触发自动检查机制,这时候不管配置有没有发生变化,都会把结果返回客户端。而 29.5s 就是这个长连接保持的时间。
• 第二种是在 29.5s 内任意一个时刻,通过 Nacos Dashboard 或者 API 的方式对配置进行了修改,这会触发一个事件机制,监听到该事件的任务会遍历 allSubs 队列,找到发生变更的配置项对应的 ClientLongPolling 任务,将变更的数据通过该任务中的连接进行返回,就完成了一次“推送”操作。
这样既能够保证客户端实时感知配置的变化,也降低了服务端的压力。其中,这个长连接的会话超时时间默认为 30s。
2.微服务高可用-负载均衡Ribbon
介绍
负载均衡将来自客户端的请求按照某种策略平均的分配到集群的每一个节点上,保证这些节点的 CPU、内存等设备负载情况大致在一条水平线,避免由于局部节点负载过高产生宕机,再将这些处理压力传递到其他节点上产生系统性崩溃。
分类:按实现方式可区分为服务端负载均衡与客户端负载均衡。
服务端负载均衡:客户端先发送请求到负载均衡服务器,然后由负载均衡服务器通过负载均衡算法,在众多可用的服务器之中选择一个来处理请求。
服务器端负载均衡又分为两种,一种是硬件负载均衡,还有一种是软件负载均衡。
硬件负载均衡主要通过在服务器节点之前安装专门用于负载均衡的设备,常见的如:F5。
软件负载均衡则主要是在服务器上安装一些具有负载均衡功能的软件来完成请求分发进而实现负载均衡,常见的如:LVS 、 Nginx 、Haproxy。
客户端负载均衡:客户端自己维护一个可用服务器地址列表,在发送请求前先通过负载均衡算法选择一个将用来处理本次请求的服务器,然后再直接将请求发送至该服务器。
Ribbon负载均衡:是一个基于HTTP和TCP的客户端负载均衡器。Ribbon会到注册中心去获取服务端列表,然后进行按照负载均衡策略(如:轮询)访问以到达负载均衡的作用。

Ribbon 执行流程:
- 订单服务(order-service)与商品服务(goods-service)实例在启动时向 Nacos 注册;
- 订单服务向商品服务发起通信前,Ribbon 向 Nacos 查询商品服务的可用实例列表;
- Ribbon 根据设置的负载策略从商品服务可用实例列表中选择实例;
- 订单服务实例向商品服务实例发起请求,完成 RESTful 通信;
Ribbon-使用
demo
提供者服务
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11spring:
application:
name: customer-service #应用/微服务名字
cloud:
nacos:
discovery:
server-addr: 192.168.31.102:8848 #nacos服务器地址
username: nacos #用户名密码
password: nacos
server:
port: 80代码
controller
1
2
3
4
5
6
7
8
9
10
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderController {
@GetMapping("/provider/msg")
public String sendMessage(){
return "This is the message from provider service!";
}
}
消费者服务
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11
12spring:
application:
name: consumer-service #应用/微服务名字
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos服务器地址
username: nacos #用户名密码
password: nacos
logging:
level:
root: debug代码
启动类
1
2
3
4
5
6
7
8
9
10
11
12@SpringBootApplication
public class ConsumerServiceApplication {
//Java Config声明RestTemplate对象
//在应用启动时自动执行restTemplate()方法创建RestTemplate对象,其BeanId为restTemplate。
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class, args);
}
}controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34@RestController
public class ConsumerController {
private Logger logger = LoggerFactory.getLogger(ConsumerController.class);
//注入 Ribbon 负载均衡器对象
//在引入 starter-netflix-ribbo n后在 SpringBoot 启动时会自动实例化 LoadBalancerClient 对象。
//在 Controlle 使用 @Resource 注解进行注入即可。
@Resource
private LoadBalancerClient loadBalancerClient;
@Resource
//将应用启动时创建的 RestTemplate 对象注入 ConsumerController
private RestTemplate restTemplate;
@GetMapping("/consumer/msg")
public String getProviderMessage() {
//loadBalancerClient.choose()方法会从 Nacos 获取 provider-service 所有可用实例,
//并按负载均衡策略从中选择一个可用实例,封装为 ServiceInstance(服务实例)对象
//结合现有环境既是从192.168.31.111:80、192.168.31.112:80、192.168.31.113:80三个实例中选择一个包装为ServiceInstance
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-service");
//获取服务实例的 IP 地址
String host = serviceInstance.getHost();
//获取服务实例的端口
int port = serviceInstance.getPort();
//在日志中打印服务实例信息
logger.info("本次调用由provider-service的" + host + ":" + port + " 实例节点负责处理" );
//通过 RestTemplate 对象的 getForObject() 方法向指定 URL 发送请求,并接收响应。
//getForObject()方法有两个参数:
//1. 具体发送的 URL,结合当前环境发送地址为:http://192.168.31.111:80/provider/msg
//2. String.class说明 URL 返回的是纯字符串,如果第二参数是实体类, RestTemplate 会自动进行反序列化,为实体属性赋值
String result = restTemplate.getForObject("http://" + host + ":" + port + "/provider/msg", String.class);
//输出响应内容
logger.info("provider-service 响应数据:" + result);
//向浏览器返回响应
return "consumer-service 响应数据:" + result;
}
}
启动1个提供者,3个消费者
启动消费者,可以启动一个,修改端口后,再启动下一个
测试
默认轮询
访问:http://192.168.31.120/consumer/msg,查看后台日志.
1
2
3
4
5
6
7
8本次调用由 provider-service 的 192.168.31.111:80 实例节点负责处理
consumer-service 获得数据:This is the message from provider service!
本次调用由 provider-service 的 192.168.31.112:80 实例节点负责处理
consumer-service 获得数据:This is the message from provider service!
本次调用由 provider-service 的 192.168.31.113:80 实例节点负责处理
consumer-service 获得数据:This is the message from provider service!
本次调用由 provider-service 的 192.168.31.111:80 实例节点负责处理
consumer-service 获得数据:This is the message from provider service!
负载均衡策略配置
- RoundRobinRule:轮询策略,Ribbon 默认策略。默认超过 10 次获取到的 server 都不可用,会返回⼀个空的 server。
RandomRule:随机策略,如果随机到的 server 为 null 或者不可用的话。会不停地循环选取。
RetryRule:重试策略,⼀定时限内循环重试。默认继承 RoundRobinRule,也⽀持自定义注⼊,RetryRule 会在每次选取之后,对选举的 server 进⾏判断,是否为 null,是否 alive,并且在 500ms 内会不停地选取判断。而 RoundRobinRule 失效的策略是超过 10 次,RandomRule 没有失效时间的概念,只要 serverList 没都挂。
BestAvailableRule:最小连接数策略,遍历 serverList,选取出可⽤的且连接数最小的⼀个 server。那么会调用 RoundRobinRule 重新选取。
AvailabilityFilteringRule:可用过滤策略。扩展了轮询策略,会先通过默认的轮询选取⼀个 server,再去判断该 server 是否超时可用、当前连接数是否超限,都成功再返回。
ZoneAvoidanceRule:区域权衡策略。扩展了轮询策略,除了过滤超时和链接数过多的 server,还会过滤掉不符合要求的 zone 区域⾥⾯的所有节点,始终保证在⼀个区域/机房内的服务实例进行轮询。
配置文件修改策略
1 | provider-service: #服务提供者的微服务id |
3.服务间通信OpenFeign
OpenFeign 是Spring官方在 Netflix Feign 的基础上进行封装的技术,结合原有 Spring MVC 的注解,对 Spring Cloud 微服务通信提供了良好的支持。
OpenFeign 开发的方式与开发 Spring MVC Controller 颇为相似。
OpenFeign使用
feign使用流程:
1.导包
2.yml配置到eureka中
3.启动类增加注解@EnableDiscoveryClient
4.生产者消费者写一样的接口,者接口上写@feignclient
5.生产者有订单接口的具体实现
6.消费者写法同平常的controller
demo
提供者服务
引入依赖
1
2
3
4
5
6
7
8<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11spring:
application:
name: warehouse-service #应用/微服务名字
cloud:
nacos:
discovery:
server-addr: 192.168.31.102:8848 #nacos服务器地址
username: nacos #用户名密码
password: nacos
server:
port: 80代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@RestController
public class WarehouseController {
/**
* 查询对应 skuId 的库存状况
* @param skuId skuId
* @return Stock 库存对象
*/
@GetMapping("/stock")
public Stock getStock(Long skuId){
Map result = new HashMap();
Stock stock = null;
if(skuId == 1101l){
//模拟有库存商品
stock = new Stock(1101l, "Apple iPhone 11 128GB 紫色", 32, "台");
stock.setDescription("Apple 11 紫色版对应商品描述");
}else if(skuId == 1102l){
//模拟无库存商品
stock = new Stock(1101l, "Apple iPhone 11 256GB 白色", 0, "台");
stock.setDescription("Apple 11 白色版对应商品描述");
}else{
//演示案例,暂不考虑无对应 skuId 的情况
}
return stock;
}
}
消费者服务
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 192.168.31.102:8848
username: nacos
password: nacos
server:
port: 80代码
启动类
1
2
3
4
5
6
7
8
9
10import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //启用OpenFeign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}OpenFeign通信接口
1
2
3
4
5
6
7
8import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient("warehouse-service") //说明当前接口为 OpenFeign 通信客户端,参数值 warehouse-service 为服务提供者 ID,这一项必须与 Nacos 注册 ID 保持一致。
public interface WarehouseServiceFeignClient {
@GetMapping("/stock")
public Stock getStock(@RequestParam("skuId") Long skuId);
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OrderController {
//利用@Resource将IOC容器中自动实例化的实现类对象进行注入
@Resource
private WarehouseServiceFeignClient warehouseServiceFeignClient;
/**
* 创建订单业务逻辑
* @param skuId 商品类别编号
* @param salesQuantity 销售数量
* @return
*/
@GetMapping("/create_order")
public Map createOrder(Long skuId , Long salesQuantity){
Map result = new LinkedHashMap();
//查询商品库存,像调用本地方法一样完成业务逻辑。
Stock stock = warehouseServiceFeignClient.getStock(skuId);
System.out.println(stock);
if(salesQuantity <= stock.getQuantity()){
//创建订单相关代码,此处省略
//CODE=SUCCESS代表订单创建成功
result.put("code" , "SUCCESS");
result.put("skuId", skuId);
result.put("message", "订单创建成功");
}else{
//code=NOT_ENOUGN_STOCK代表库存不足
result.put("code", "NOT_ENOUGH_STOCK");
result.put("skuId", skuId);
result.put("message", "商品库存数量不足");
}
return result;
}
}
OpenFeign执行流程
- 第一次访问 WarehouseServiceFeignClient (OpenFeign)接口时,Spring 自动生成接口的实现类并实例化对象。
- 调用 getStock() 方法时,Ribbon 获取 warehouse-service 可用实例信息,根据负载均衡策略选择合适实例。
- OpenFeign 根据方法上注解描述的映射关系生成完整的 URL 并发送 HTTP 请求,如果请求方法是 @PostMapping,则参数会附加在请求体中进行发送。
- warehouse-service 处理完毕返回 JSON 数据,消费者端 OpenFeign 接收 JSON 的同时反序列化到 Stock 对象,并将该对象返回。
高级配置
负载均衡+数据压缩
1 | # OpenFeign负载均衡策略,默认引用Ribbon实现客户端负载均衡 |
替换默认通信组件
基于 Apache HttpClient、OKHttp 组件自带的连接池,可以更好地对 HTTP 连接对象进行重用与管理。
引入依赖
1
2
3
4
5<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>11.0</version>
</dependency>应用入口,利用 Java Config 形式初始化 OkHttpClient 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
//Spring IOC容器初始化时构建okHttpClient对象
@Bean
public okhttp3.OkHttpClient okHttpClient(){
return new okhttp3.OkHttpClient.Builder()
//读取超时时间
.readTimeout(10, TimeUnit.SECONDS)
//连接超时时间
.connectTimeout(10, TimeUnit.SECONDS)
//写超时时间
.writeTimeout(10, TimeUnit.SECONDS)
//设置连接池
.connectionPool(new ConnectionPool())
.build();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}配置启用 OKHttp
1
2
3feign:
okhttp:
enabled: true
同类产品Dubbo
4.微服务大门-网关GateWay
介绍
场景
- 用户身份鉴权
- 日志记录
- 黑白名单
- 反爬虫
网关作用
- 解耦
- 统一的一个入口,方便做统一的事。

Gateway特点
基于 NIO 异步处理,有更好的性能。
配置简单
基于 JDK 8+ 开发
基于 Spring Framework 5 + Project Reactor + Spring Boot 2.0 构建
支持动态路由,能够匹配任何请求属性上的路由;
基于 HTTP 请求的路由匹配(Path、Method、Header、Host 等)
过滤器可以修改 HTTP 请求和 HTTP 响应(增加/修改 Header、增加/修改请求参数、改写请求 Path 等等);
使用
前提
service-a提供了三个RESTFul接口

service-b提供了三个接口

Gatway-demo
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- Nacos客户端 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Gateway Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 对外提供Gateway应用监控指标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>application.yml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20spring:
application:
name: gateway #配置微服务id
cloud:
nacos:
discovery:
server-addr: 192.168.31.101:8848 #nacos通信地址
username: nacos
password: nacos
gateway: #让gateway通过nacos实现自动路由转发
discovery:
locator:
enabled: true #locator.enabled 开启自动转发,根据URL规则实现默认路由转发
server:
port: 80 #服务端口号
management:
endpoints:
web:
exposure:
include: '*' #对外暴露actuator所有监控指标,便于监控系统收集跟踪启动服务并访问
1
2
3
4// 规则
http://网关IP:端口/微服务id/URI
http://192.168.31.103:80/service-a/list
高级配置
路由、谓词和过滤器。
路由(Route)是指一个完整的网关地址映射与处理过程。(前端的请求经过网关,网关判断请求转发到)一个完整的路由包含两部分配置:谓词(Predicate)与过滤器(Filter)。前端应用发来的请求要被转发到哪个微服务上,是由谓词决定的;而转发过程中请求、响应数据被网关如何加工处理是由过滤器决定的。
谓词
指定时点后路由规则生效。
1 | predicates: |
URI 符合映射规则时生效
1 | predicates: |
Header 包含指定请求头时生效
1 | predicates: |
过滤器
对所有匹配的请求添加一个查询参数。
1 | filters: |
返回客户端之前,添加响应数据
1 | # 在Response中添加Header头,key=X-Response-Foo,Value=Bar。 |
返回 503 状态码的响应后,Retry 过滤器重新发起请求,最多重试 3 次。
1 | filters: |
实例
1 | spring: |
跨域
1 | /** |
原理
路由转发流程

内部执行原理

- Spring Cloud Gateway 启动时基于 Netty Server 监听指定的端口(该端口可以通过 server.port 属性自定义)。当前端应用发送一个请求到网关时,进入 Gateway Handler Mapping 处理过程,网关会根据当前 Gateway 所配置的谓词(Predicate)来决定是由哪个微服务进行处理。
- 确定微服务后,请求向后进入 Gateway Web Handler 处理过程,该过程中 Gateway 根据过滤器(Filters)配置,将请求按前后顺序依次交给 Filter 过滤链进行前置(Pre)处理,前置处理通常是对请求进行前置检查,例如:判断是否包含某个指定请求头、检查请求的 IP 来源是否合法、请求包含的参数是否正确等。
- 当过滤链前置(Pre)处理完毕后,请求会被 Gateway 转发到真正的微服务实例进行处理,微服务处理后会返回响应数据,这些响应数据会按原路径返回被 Gateway 配置的过滤链进行后置处理(Post),后置处理通常是对响应进行额外处理,例如:将处理过程写入日志、为响应附加额外的响应头或者流量监控等。
5.服务流量控制Sentinel
背景
微服务环境下受制于网络、机器性能、算法、程序各方面影响,运行异常的情况也在显著提升,加上并发量不可控,需要做好异常保护。
微服务雪崩:在微服务项目中指由于突发流量导致某个服务不可用,从而导致上游服务不可用,并产生级联效应,最终导致整个系统不可用。例子:服务A调⽤服务B,此时⼤量请求突然请求服务A,假如服务A本身能抗住这些请
求,但是如果服务B抗不住,导致服务B请求堆积,从而服务A请求堆积,直到服务A不可用。
如何避免雪崩效应:
限流
控制请求的流入,让流量有序的进入应用,保证流量在一个可控的范围内。
服务降级
当应用处理时间超过规定上限后,无论服务是否处理完成,响应返回预先设置的异常信息。
服务熔断
设置服务为不可用,发送心跳包,服务可用后再给该服务发请求。
Sentinel介绍
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
特征:
- 丰富的应用场景:承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其他开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 整合只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel使用
demo
Sentinel 分为两个部分:Sentinel Dashboard和Sentinel 客户端。
Sentinel Dashboard
Sentinel Dashboard 是 Sentinel 配套的可视化控制台与监控仪表盘套件,它支持节点发现,以及健康情况管理、监控(单机和集群)、规则管理和推送的功能。Sentinel Dashboard 是基于 Spring Boot 开发的 WEB 应用,打包后可以直接运行。
Sentinel Dashboard 监听9100端口实现与微服务的通信。

部署Sentinel Dashboard
访问:https://github.com/alibaba/Sentinel/releases,下载Sentinel Dashboard
启动Dashboard
1
java -jar -Dserver.port=9100 sentinel-dashboard-1.8.0.jar
SentinelUI界面
账号&密码 sentinel&sentinel
Sentinel 客户端
Sentinel 客户端需要集成在 Spring Boot 微服务应用中,用于接收来自 Dashboard 配置的各种规则,并通过 Spring MVC Interceptor 拦截器技术实现应用限流、熔断保护。
客户端配置
创建工程,加入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- Nacos客户端Starter-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Sentinel客户端Starter-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 对外暴露Spring Boot监控指标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>配置Nacos和Sentinel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19spring:
application:
name: sentinel-sample #应用名&微服务id
cloud:
sentinel: #Sentinel Dashboard通信地址
transport:
dashboard: 192.168.31.10:9100
eager: true #取消控制台懒加载
nacos: #Nacos通信地址
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
server:
port: 80
management:
endpoints:
web: #将所有可用的监控指标项对外暴露
exposure: #可以访问 /actuator/sentinel进行查看Sentinel监控项
include: '*'验证
访问Sentinel Dashboard

限流demo
Sentinel Core 写一个Controller,启动微服务
在Sentinel Dashboard配置限流规则
每秒钟只允许 1QPS 访问,超出的请求直接服务降级返回异常。


验证
Dashboard限流配置
在 Sentinel Dashboard 中“簇点链路”,找到需要限流的 URI,点击“+流控”进入流控设置。
Sentinel-Dashboard 加载链路使用懒加载模式,如果在簇点链路没有找到对应的 URI,需要先访问下这个功能对应的 URI
流控规则说明:
- 资源名:要流控的 URI,在 Sentinel 中 URI 被称为“资源”;
- 针对来源:默认 default 代表所有来源,可以针对某个微服务或者调用者单独设置;
- 阈值类型:是按每秒访问数量(QPS)还是并发数(线程数)进行流控;
单机阈值:具体限流的数值是多少。

高级选项:
流控模式是指采用什么方式进行流量控制。
直接模式
List 接口 QPS 超过 1个时限流,浏览器会出现“Blocked by Sentinel”。

关联
同 List 接口关联的update 接口 QPS 超过 1 时,再次访问List 接口便会响应“Blocked by Sentinel”。

链路
/check
→ /list
/scan
两条链路都可以访问到/list

当访问 check 接口的QPS 超过 1 时,List 接口就会被限流。而另一条链路从 scan 接口到List 接口的链路则不会受到任何影响。
流控效果
快速失败
指流量当过限流阈值后,直接返回响应并抛出 BlockException
Warm Up(预热)
用于应对瞬时大并发流量冲击。当遇到突发大流量 Warm Up 会缓慢拉升阈值限制,预防系统瞬时崩溃,这期间超出阈值的访问处于队列等待状态,并不会立即抛出 BlockException。
List 接口平时单机阈值 QPS 处于低水位:默认为 1000/3 (冷加载因子)≈333,当瞬时大流量进来,10 秒钟内将 QPS 阈值逐渐拉升至 1000,为系统留出缓冲时间,预防突发性系统崩溃。

排队等待
采用匀速放行的方式对请求进行处理。如下所示,假设现在有100个请求瞬间进入,那么会出现以下几种情况:
单机 QPS 阈值=4,代表 250 毫秒匀速放行 1 个请求,其他请求队列等待,共需 25 秒处理完毕; 单机 QPS 阈值=200,代表 5 毫秒匀速放行一个请求,其他请求队列等待,共需 0.5 秒处理完毕; 如果某一个请求在队列中处于等待状态超过 2000 毫秒,则直接抛出 BlockException。匀速队列只支持 QPS 模式,且单机阈值不得大于 1000。

Dashboard熔断配置
介绍
微服务的熔断是指在某个服务接口在执行过程中频繁出现故障的情况,我们便认为这种状态是“不可接受”的,立即对当前接口实施熔断。在规定的时间内,所有送达该接口的请求都将直接抛出 BlockException,在熔断期过后新的请求进入看接口是否恢复正常,恢复正常则继续运行,仍出现故障则再次熔断一段时间,以此往复直到服务接口恢复。
熔断过程

熔断设置
Sentinel Dashboard可以设置三种不同的熔断模式:慢调用比例、异常比例、异常数
慢调用比例是指当接口在1秒内“慢处理”数量超过一定比例,则触发熔断。


异常比例是指 1 秒内按接口调用产生异常的比例(异常调用数/总数量)触发熔断。


异常数是指在 1 分钟内异常的数量超过阈值则触发熔断。


Sentinel与Nacos配置中心整合
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- Nacos 客户端 Starter-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Sentinel 客户端 Starter-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 对外暴露 Spring Boot 监控指标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24spring:
application:
name: sentinel-sample #应用名&微服务 id
cloud:
sentinel: #Sentinel Dashboard 通信地址
transport:
dashboard: 192.168.31.10:9100
eager: true #取消控制台懒加载
nacos: #Nacos 通信地址
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
jackson:
default-property-inclusion: non_null
server:
port: 80
management:
endpoints:
web: #将所有可用的监控指标项对外暴露
exposure: #可以访问 /actuator/sentinel进行查看Sentinel监控项
include: '*'
logging:
level:
root: debug #开启 debug 是学习需要,生产改为 info 即可业务代码
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32@RestController
public class SentinelSampleController {
//演示用的业务逻辑类
@Resource
private SampleService sampleService;
/**
* 流控测试接口
* @return
*/
@GetMapping("/test_flow_rule")
public ResponseObject testFlowRule(){
//code=0 代表服务器处理成功
return new ResponseObject("0","success!");
}
/**
* 熔断测试接口
* @return
*/
@GetMapping("/test_degrade_rule")
public ResponseObject testDegradeRule(){
try {
sampleService.createOrder();
}catch (IllegalStateException e){
//当 createOrder 业务处理过程中产生错误时会抛出IllegalStateException
//IllegalStateException 是 JAVA 内置状态异常,在项目开发时可以更换为自己项目的自定义异常
//出现错误后将异常封装为响应对象后JSON输出
return new ResponseObject(e.getClass().getSimpleName(),e.getMessage());
}
return new ResponseObject("0","order created!");
}
}返回结果封装类
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 封装响应数据的对象
*/
public class ResponseObject {
private String code; //结果编码,0-固定代表处理成功
private String message;//响应消息
private Object data;//响应附加数据(可选)
public ResponseObject(String code, String message) {
this.code = code;
this.message = message;
}
//Getter/Setter省略
}service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 演示用的业务逻辑类
*/
@Service
public class SampleService {
//模拟创建订单业务
public void createOrder(){
try {
//模拟处理业务逻辑需要101毫秒
Thread.sleep(101);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已创建");
}
}测试
整合-流控规则持久化
增加依赖
1
2
3
4<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>配置文件增加配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22spring:
application:
name: sentinel-sample #应用名&微服务id
cloud:
sentinel: #Sentinel Dashboard通信地址
transport:
dashboard: 192.168.31.10:9100
eager: true #取消控制台懒加载
datasource:
flow: #数据源名称,可以自定义
nacos: #nacos配置中心
#Nacos内置配置中心,因此重用即可
server-addr: ${spring.cloud.nacos.server-addr}
dataId: ${spring.application.name}-flow-rules #定义流控规则data-id,完整名称为:sentinel-sample-flow-rules,在配置中心设置时data-id必须对应。
groupId: SAMPLE_GROUP #gourpId对应配置文件分组,对应配置中心groups项
rule-type: flow #flow固定写死,说明这个配置是流控规则
username: nacos #nacos通信的用户名与密码
password: nacos
nacos: #Nacos通信地址
server-addr: 192.168.31.10:8848
username: nacos
password: nacosnacos配置中添加 流控配置
同UI 页面配置同样的效果

1
2
3
4
5
6
7
8
9
10
11[
{
"resource":"/test_flow_rule", #资源名,说明对那个URI进行流控
"limitApp":"default", #命名空间,默认default
"grade":1, #类型 0-线程 1-QPS
"count":2, #超过2个QPS限流将被限流
"strategy":0, #限流策略: 0-直接 1-关联 2-链路
"controlBehavior":0, #控制行为: 0-快速失败 1-WarmUp 2-排队等待
"clusterMode":false #是否集群模式
}
]验证流控是否生效
访问 http://localhost/test_flow_rule
1
2// 服务启动时已向 Nacos 配置中心获取到流控规则。
DEBUG 12728 --- [main] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@5432948015 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}...在浏览器反复刷新,当 test_flow_rule 接口每秒超过 2 次访问,就会出现“Blocked by Sentinel (flow limiting)”的错误信息,说明流控规则已生效。
验证能否自动推送新规则
将Nacos 配置中心中流控规则 count 选项改为 1。
1
2
3
4
5
6
7
8
9
10
11[
{
"resource":"/test_flow_rule",
"limitApp":"default",
"grade":1,
"count":1, #2改为1
"strategy":0,
"controlBehavior":0,
"clusterMode":false
}
]新规则发布后,sentinel-sample控制台会立即收到下面的日志,说明新的流控规则即时生效。
1
DEBUG 12728 --- [.168.31.10_8848] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@41257f3915 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}
通过 Spring Boot Actuator 提供的监控指标确认流控规则已生效。
访问 http://localhost/actuator/sentinel,在 flowRules 这个数组中,可以看到 test_flow_rule 的限流规则
自定义资源流控
介绍:针对某一个 Service 业务逻辑方法进行限流熔断等规则设置。
实例:对 SampleSerivce.createOrder方法进行熔断保护
声明切面类
1
2
3
4
5
6
7
8
9
10
11@SpringBootApplication
public class SentinelSampleApplication {
// 注解支持的配置Bean
@Bean
public SentinelResourceAspect sentinelResourceAspect() {// 用于进行熔断的前置检查
return new SentinelResourceAspect();
}
public static void main(String[] args) {
SpringApplication.run(SentinelSampleApplication.class, args);
}
}声明资源点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 演示用的业务逻辑类
*/
@Service
public class SampleService {
//资源点名称为createOrder
@SentinelResource("createOrder")
/**
* 模拟创建订单业务
* 抛出IllegalStateException异常用于模拟业务逻辑执行失败的情况
*/
public void createOrder() throws IllegalStateException{
try {
//模拟处理业务逻辑需要101毫秒
Thread.sleep(101);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已创建");
}
}启动服务测试接口,确认资源点已存在 Sentinel Dashboard
访问 http://localhost/test_degrade_rule

获取熔断规则
1
2
3
4
5
6
7
8
9
10
11datasource:
flow: #之前的流控规则,直接忽略
...
degrade: #熔断规则
nacos:
server-addr: ${spring.cloud.nacos.server-addr}
dataId: ${spring.application.name}-degrade-rules
groupId: SAMPLE_GROUP
rule-type: degrade #代表熔断
username: nacos
password: nacos确定配置,启动日志如下
1
[main] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@d96945215 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-degrade-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg5MDMwNH0.ooHkFb4zX14klmHMuLXTDkHSoCrwI8LtN7ex__9tMHg&group=SAMPLE_GROUP HTTP/1.1: null}...
配置熔断规则

1
2
3
4
5
6
7
8
9[{
"resource": "createOrder", #自定义资源名
"limitApp": "default", #命名空间
"grade": 0, #0-慢调用比例 1-异常比例 2-异常数
"count": 100, #最大RT 100毫秒执行时间
"timeWindow": 5, #时间窗口5秒
"minRequestAmount": 1, #最小请求数
"slowRatioThreshold": 0.1 #比例阈值
}]熔断机制

访问 Spring Boot Actuatorhttp://localhost/actuator/sentinel,可以看到此时 gradeRules 数组下 createOrder 资源点的熔断规则已被 Nacos推送并立即生效。
验证
连续访问 http://localhost/test_degrade_rule,当第二次访问时便会出现 500 错误。

完善异常提示
针对 RESTful 接口的统一异常处理需要实现 BlockExceptionHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35@Component //Spring IOC实例化并管理该对象
public class UrlBlockHandler implements BlockExceptionHandler {
/**
* RESTFul异常信息处理器
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws Exception
*/
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
String msg = null;
if(e instanceof FlowException){//限流异常
msg = "接口已被限流";
}else if(e instanceof DegradeException){//熔断异常
msg = "接口已被熔断,请稍后再试";
}else if(e instanceof ParamFlowException){ //热点参数限流
msg = "热点参数限流";
}else if(e instanceof SystemBlockException){ //系统规则异常
msg = "系统规则(负载/....不满足要求)";
}else if(e instanceof AuthorityException){ //授权规则异常
msg = "授权规则不通过";
}
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
//ObjectMapper是内置Jackson的序列化工具类,这用于将对象转为JSON字符串
ObjectMapper mapper = new ObjectMapper();
//某个对象属性为null时不进行序列化输出
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.writeValue(httpServletResponse.getWriter(),
new ResponseObject(e.getClass().getSimpleName(), msg)
);
}
}自定义资源点的异常处理
在 @SentinelResource 注解上额外附加 blockHandler属性进行异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/**
* 演示用的业务逻辑类
*/
@Service
public class SampleService {
@SentinelResource(value = "createOrder",blockHandler = "createOrderBlockHandler")
/**
* 模拟创建订单业务
* 抛出 IllegalStateException 异常用于模拟业务逻辑执行失败的情况
*/
public void createOrder() throws IllegalStateException{
try {
//模拟处理业务逻辑需要 101 毫秒
Thread.sleep(101);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已创建");
}
public void createOrderBlockHandler(BlockException e) throws IllegalStateException{
String msg = null;
if(e instanceof FlowException){//限流异常
msg = "资源已被限流";
}else if(e instanceof DegradeException){//熔断异常
msg = "资源已被熔断,请稍后再试";
}else if(e instanceof ParamFlowException){ //热点参数限流
msg = "热点参数限流";
}else if(e instanceof SystemBlockException){ //系统规则异常
msg = "系统规则(负载/....不满足要求)";
}else if(e instanceof AuthorityException){ //授权规则异常
msg = "授权规则不通过";
}
throw new IllegalStateException(msg);
}
}createOrderBlockHandler 方法的书写有两个要求
方法返回值、访问修饰符、抛出异常要与原始的 createOrder 方法完全相同。
createOrderBlockHandler 方法名允许自定义,但最后一个参数必须是 BlockException 对象,这是所有规则异常的父类,通过判断 BlockException 我们就知道触发了哪种规则异常。
Sentinel原理
Sentinel Dashboard 是Sentinel的控制端,当内置在微服务内的 Sentinel Core(客户端)接收到控制端新的限流、熔断规则后,微服务便会自动启用的相应的保护措施。
Sentinel执行流程:
- Sentinel Core与Sentinel Dashboard建立连接
- Sentinel Dashboard 向Sentinel Core下发新的保护规则
- Sentinel Core应用新的保护规则,实施限流、熔断等。
Sentinel执行流程细节:
建立连接
Sentinel core 初始化时,主动向application.yml中配置的Dashboard的IP地址发起连接请求。
1
2
3
4
5
6#Sentinel Dashboard通信地址
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.31.10:9100该请求以心跳包的方式定时向Dashboard发送,包含Sentinel Core 的AppName、IP、端口信息。
Sentinel Core持续接收Dashboard数据和发送心跳包使用8719 端口。
Sentinel Dashboard 接收到心跳包后,来自 Sentinel Core的AppName、IP、端口信息会被封装为 MachineInfo 对象放入 ConcurrentHashMap 保存在 JVM的内存中,以备后续使用。
Dashboard下发保护规则
- Dashboard 页面中设置了新的保护规则,
- 从当前的 MachineInfo 中提取符合要求的微服务实例信息
- 通过 Dashboard内置的 transport 模块将新规则打包推送到微服务实例的 Sentinel Core
Sentinel Core应用新规则
Sentinel Core收 到新规则
对本地规则进行更新。
依据规则处理请求。
Sentinel Core 为服务限流、熔断提供了核心拦截器 SentinelWebInterceptor,这个拦截器默认对所有请求 /** 进行拦截,然后开始请求的链式处理流程,在对于每一个处理请求的节点被称为 Slot(槽),通过多个槽的连接形成处理链,在请求的流转过程中,如果有任何一个 Slot 验证未通过,都会产生 BlockException,请求处理链便会中断,并返回“Blocked by sentinel” 异常信息。

默认 Slot 有7 个,前 3 个 Slot为前置处理,用于收集、统计、分析必要的数据;后 4 个为规则校验 Slot,从Dashboard 推送的新规则保存在“规则池”中,然后对应 Slot 进行读取并校验当前请求是否允许放行,允许放行则送入下一个 Slot 直到最终被 RestController 进行业务处理,不允许放行则直接抛出 BlockException 返回响应。
Slot具体职责:
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT(运行时间), QPS, thread count(线程总数)等,这些信息将用作为多维度限流,降级的依据;
- StatistcSlot 则用于记录,统计不同维度的runtime 信息;
- SystemSlot 则通过系统的状态,例如CPU、内存的情况,来控制总的入口流量;
- AuthoritySlot 则根据黑白名单,来做黑白名单控制;
- FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
- DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级。
6.应用性能监控SkyWalking
博客地址 TODO
Sleuth+Zipkin
使用 Tracer 在访问链路中创建自定义的 Span
介绍:创建自定义的 Span 并纳入可视化监控机制中
场景:在业务系统中重点监控某些业务操作
版本1.X
实现:通过 Spring Cloud Sleuth 自带的 org.springframework.cloud.sleuth.Tracer 接口创建和管理自定义 Span。
版本:2.X
实现:使用 Brave 创建自定义 Span
代码:
业务中添加代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@Service
public class MyService {
@Autowired
private Tracer tracer;//
public void perform() {
// 创建并启动了一个“spanName”新的 Span
Span newSpan = tracer.nextSpan().name("spanName").start();
//ScopedSpan newSpan = tracer.startScopedSpan("spanName");
try {
//执行业务逻辑
}
finally{
newSpan.tag("key", "value");
newSpan.annotate("myannotation");
newSpan.finish();
}
}
}注解
1
2@NewSpan(name = "myspan")//创建新span
void myMethod(@SpanTag("mykey") String param);//生成一个键为“mykey”,值为 param 的新标签。
7.分布式事务Seata
介绍
产生原因
跨数据库操作
跨系统的分布式事务
与第三方系统(企业内外)集成可能遇到
跨服务的分布式事务
跨数据库与消息的分布式事务
分布式解决方案
分布式事务解决方案:二阶段提交(2PC)与三阶段提交(3PC)。
三阶段提交

阶段一,事务预处理阶段。
事务协调者会向各服务下达“处理本地事务”的通知,本地事务开始处理业务逻辑,如订单服务中负责创建新的订单记录;会员服务负责增加会员的积分;库存服务负责减少库存数量。被操作的所有数据都处于未提交(uncommit)的状态,会被排它锁锁定。本地事务都处理完成后,会通知事务协调者“本地事务处理完毕”。所有本地事务处理完毕后,进入阶段二。
阶段二,预提交阶段。
预提交阶段只是一个询问机制,以确认所有服务都已准备好,同时在此阶段协调者和参与者都设置了超时时间以防止出现长时间资源锁定。当阶段二所有服务返回“可以提交”,进入阶段三“提交阶段”。
阶段三,提交阶段
3PC 的提交阶段与 2PC 的提交阶段是一致的,在每一个数据库中执行提交实现数据的资源写入,如果协调者与服务通信中断导致无法提交,在服务端超时后在也会自动执行提交操作来保证资源释放。
问题:预提交阶段引入了超时机制,让数据库资源不会被长期锁定,数据一致性也很可能因为超时后的强制提交被破坏。
解决:增加异步的数据补偿任务、日终跑批前的数据补偿、更完善的业务数据完整性的校验代码、引入数据监控及时通知人工补录。
Seata
AT:需要数据库支持事务,使用简单。(自动解析sql生成反向SQL,用于回滚。)
TCC:开启、提交、回滚程序员控制,灵活,复杂(失败时,事务自动多次提交,还需考虑幂等性)
使用
AT 模式

部署TC-Seata-Server
下载seata
https://github.com/seata/seata/releases/download/v1.4.0/seata-server-1.4.0.tar.gz
配置seata接入注册中心和配置中心, conf/registry.conf 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37registry {
# Seata-Server支持以下几种注册中心,这里改为nacos,默认是file文件形式不介入任何注册中心。
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
# 负载均衡采用随机策略
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
# nacos注册中心接入配置
nacos {
# 应用名称
application = "seata-server"
#IP地址与端口
serverAddr = "192.168.31.10:8848"
# 分配应用组,采用默认值SEATA_GROUP即可
group = "SEATA_GROUP"
namespace = ""
# 集群名称,采用默认值default即可
cluster = "default"
# Nacos接入用户名密码
username = "nacos"
password = "nacos"
}
}
#Seata-Server接入配置中心
config {
# Seata-Server支持以下配置中心产品,这里设置为nacos,默认是file即文件形式保存配置内容。
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
# 设置Nacos的通信地址
nacos {
serverAddr = "192.168.31.10:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}nacos配置中心初始化seata配置
官网找初始化脚本https://github.com/seata/seata/blob/1.4.0/script/config-center/config.txt

在 /usr/local/seata-server-1.4.0 目录创建 config.txt文件,复制脚本内容到文件中
1
2
3
4# 修改
store.db.url=jdbc:mysql://192.168.31.103:3309/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root下载运行脚本
https://github.com/seata/seata/blob/1.4.0/script/config-center/nacos/nacos-config.sh
在 /usr/local/seata-server-1.4.0 目录创建 script 子目录。放入文件
运行导入脚本
1
sh nacos-config.sh -h 192.168.31.10
Nacos 后台http://192.168.31.10:8848/nacos ,会看到大量 SEATA_GROUP 分组的配置,Seata-Server 启动时自动读取
创建并初始化Seata-Server全局事务数据库
下载 SQL 脚本 https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql
MySQL中创建数据库 seata,执行SQL脚本创建全局事务表

启动 seata-server
1
sh bin/seata-server.sh
启动过程中提示数据库无法访问,说明 IP、端口配置有问题,可以通过 Nacos 配置中心设置 store.db.url 选项,而不是重新导入 config.txt。

开发RM资源管理器
订单服务、会员服务、库存服务
订单服务
创建seata-order数据库和 undo_log 表。Seata 强制要求在每个 RM 端数据库创建的表,用于存储反向 SQL 的元数据
https://github.com/seata/seata/blob/1.4.0/script/client/at/db/mysql.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33# 订单业务表 order
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`order_id` int(255) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
`goods_id` int(32) NOT NULL COMMENT '商品编号',
`member_id` int(32) NOT NULL COMMENT '会员编号',
`quantity` int(255) NOT NULL COMMENT '购买数量',
`points` int(255) NOT NULL COMMENT '增加会员积分',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime(0) NOT NULL,
`log_modified` datetime(0) NOT NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48<!--Spring Boot JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--Web MVC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--seata 客户端最新版-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
<!--seata与spring boot starter-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--JDBC驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62#seata配置
seata:
# 开启seata分布式事务
enabled: true
# 事务服务分组名,与naocs一致
tx-service-group: my_test_tx_group
# 是否启用数据源代理
enable-auto-data-source-proxy: true
# 事务服务配置
service:
vgroup-mapping:
# 事务分组对应集群名称
my_test_tx_group: default
grouplist:
# Seata-Server服务的IP地址与端口
default: 192.168.31.107:8091
enable-degrade: false
disable-global-transaction: false
# Nacos配置中心信息
config:
type: nacos
nacos:
namespace:
serverAddr: 192.168.31.10:8848
group: SEATA_GROUP
username: nacos
password: nacos
cluster: default
# Nacos注册中心信息
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.31.10:8848
group : SEATA_GROUP
namespace:
username: nacos
password: nacos
cluster: default
# 应用配置
spring:
application:
name: rm-order
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.103:3306/seata-order
username: root
password: root
cloud:
nacos:
discovery:
username: nacos
password: nacos
server-addr: 192.168.31.10:8848
jpa:
show-sql: true
server:
port: 8002
logging:
level:
io:
seata: debug开发crud
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//JPA实体类
@Entity
@Table(name = "`order`") //对应order表
public class Order {
@Id
@Column(name = "order_id")
private Integer id; //订单编号
private Integer memberId; //会员编号
@Column(name = "goods_id")
private Integer goodsId; //商品编号
private Integer points; //新增积分
private Integer quantity; //销售数量
public Order() {
}
public Order(Integer id, Integer memberId, Integer goodsId, Integer points, Integer quantity) {
this.id = id;
this.memberId = memberId;
this.points = points;
this.goodsId = goodsId;
this.quantity = quantity;
}
//...getter & setter
}1
2public interface OrderRepository extends JpaRepository<Order,Integer> {
}1
2
3
4
5
6
7
8
9
10@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
@Transactional
public Order createOrder(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity){
return orderRepository.save(new Order(orderId, memberId,goodsId,points,quantity));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/create_order")
public String createOrder(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity) throws JsonProcessingException {
Map result = new HashMap<>();
Order order = orderService.createOrder(orderId,memberId,goodsId,points,quantity);
result.put("code", "0");
result.put("message", "create order success");
return new ObjectMapper().writeValueAsString(result);
}
}配置 DataSourceProxy 数据源代理类,用于seata自动生成 undo_log 回滚数据,以及自动完成 RM 端分布式事务的提交或回滚操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DataSourceProxyConfig {
//创建Druid数据源
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
//建立DataSource数据源代理
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}启动 rm-order,访问 create_order 接口
http://192.168.31.106:8002/create_order?orderId=6&memberId=1&goodsId=2&points=20&quantity=200
rm-points 积分服务
rm-storage 库存服务
开发TM 事务管理器
创建 seata-mall 数据库和undo_log 表
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#seata配置与rm-order完全相同,省略
seata:
...
spring:
application:
name: tm-mall
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.103:3305/seata-mall
username: root
password: root
...
server:
port: 8001
...启动类增加远程调用注解
1
2
3
4
5
6
7@SpringBootApplication
@EnableFeignClients
public class TmMallApplication {
public static void main(String[] args) {
SpringApplication.run(TmMallApplication.class, args);
}
}开发三个远程调用接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//订单服务客户端
@FeignClient("rm-order")
public interface OrderFeignClient {
@GetMapping("/create_order")
public String createOrder(@RequestParam("orderId") Integer orderId,
@RequestParam("memberId") Integer memberId,
@RequestParam("goodsId") Integer goodsId,
@RequestParam("points") Integer points,
@RequestParam("quantity") Integer quantity
);
}
//积分服务客户端
@FeignClient("rm-points")
public interface PointsFeignClient {
@GetMapping("/add_points")
public String addPoints(@RequestParam("memberId") Integer memberId, @RequestParam("points") Integer points);
}
//库存服务客户端
@FeignClient("rm-storage")
public interface StorageFeignClient {
@GetMapping("/reduce_storage")
public String reduceStorage(@RequestParam("goodsId") Integer goodsId, @RequestParam("quantity") Integer quantity);
}开发 MallService,定义全局事务范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Service
public class MallService {
@Resource
OrderFeignClient orderFeignClient;
@Resource
PointsFeignClient pointsFeignClient;
@Resource
StorageFeignClient storageFeignClient;
@GlobalTransactional(name = "seata-group-tx-mall", rollbackFor = {Exception.class}) // 全局事务注解,MallService.sale 方法时通知 TC 开启全局事务,sale 方法执行成功自动通知 TC 进行全局提交;sale 方法抛出异常时自动通知 TC 进行全局回滚。
public String sale(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity) {
String orderResult = orderFeignClient.createOrder(orderId,memberId,goodsId,points,quantity);
String pointsResult = pointsFeignClient.addPoints(memberId, points);
String storageResult = storageFeignClient.reduceStorage(goodsId, quantity);
return orderResult + " / " + pointsResult + " / " + storageResult;
}
}开发 MallController 对外暴露 sale 接口提供调用
1
2
3
4
5
6
7
8
9@RestController
public class MallController {
@Resource
private MallService mallService;
@GetMapping("/sale")
public String sale(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity){
return mallService.sale(orderId,memberId,goodsId,points,quantity);
}
}配置 DataSourceProxyConfig,这是所有 TM 与 RM 都要设置的。
1
2
3
4
5
6
7
8
9
10
11
12
13@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
验证
启动 Nacos、TC、TM、3 个 RM ,
正常验证
访问 tm-mall 的 sale 接口。
http://localhost:8001/sale?orderId=6&memberId=1&goodsId=2&points=20&quantity=20
1
2
3
4
5
6## TM端日志
# 启动全局事务
i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.31.107:8091:100622589646344192]
...
# 全局事务已提交
i.seata.tm.api.DefaultGlobalTransaction : [192.168.31.107:8091:100622589646344192] commit status: Committed1
2
3
4
5## RM日志
# 分支事务已提交
i.s.c.r.p.c.RmBranchCommitProcessor : branch commit result:xid=192.168.31.107:8091:100622589646344192,branchId=100622590170632192,branchStatus=PhaseTwo_Committed,result code =Success,getMsg =null
# 清空undo_log表
i.s.r.d.undo.mysql.MySQLUndoLogManager : batch delete undo log size 1异常验证
quantity 设置为 200,超出库存报错,看能否全局回滚。
http://localhost:8001/sale?orderId=6&memberId=1&goodsId=2&points=20&quantity=2001
2
3
4
5
6// 程序报错
java.lang.IllegalStateException: 商品库存不足。
// TM 向 TC 发起全局回滚通知。
i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.31.107:8091:100626590567763968]
i.s.c.rpc.netty.AbstractNettyRemoting : io.seata.core.rpc.netty.TmNettyRemotingClient@2e81af7d msgId:1726, body:globalStatus=Rollbacked,ResultCode=Success,Msg=nullTC 向 RM 下达分支事务回滚通知,RM 收到通知做两件事:第一,根据 undo_log 表生成的反向 SQL,将之前写入的数据撤销;第二,删除 undo_log 数据。
1
2
3i.s.r.d.undo.AbstractUndoLogManager : Flushing UNDO LOG: {"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.31.107:8091:100626590567763968","branchId":100626590894919681...
io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.31.107:8091:100626590567763968 100626590894919681 jdbc:mysql://192.168.31.103:3306/seata-order
i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.31.107:8091:100626590567763968 branch 100626590894919681, undo_log deleted with GlobalFinished
原理
失效

8.分布式锁
用途:控制分布式系统中的不同主机之间对共享资源的访问。
- 同一应用程序多实例下控制只有一个实例执行。
- 不同应用程序只有一个程序修改数据。
应用场景
- 定时拉取上游系统数据,部署了双节点。不用分布式锁,定时任务会执行两次。
- 防止同一用户对同一接口短时间多次点击。
- 秒杀系统的库存管理,避免超卖。
实现方案
zookeeper
介绍:利⽤的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式 锁的特点是⾼⼀致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱。先同步到一半以上从节点,再返回加锁成功;
流程:客户端要获取锁,1.在请求下创建临时顺序子节点,2.若节点最小,则获得了锁,执行业务3.使用完锁,则删除该节点,释放锁。
详细流程:
- 客户端在lock节点下创建临时顺序节点。
- 2.获取lock下面的所有子节点,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁,执行业务代码。使用完锁后,将该节点删除。
- 3.如果自己创建的节点并非lock所有子节点中最小的,找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
- 4.如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤(监听比自己小一个节点的删除事件,判断自己是否是最小节点)。
redis
介绍:利⽤redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是⾼可⽤, 因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦redis中的数据出现了 不⼀致),可能会出现多个客户端同时加到锁的情况 。
框架redisson
流程:
- 尝试获取锁,执行加锁lua脚本。用uuid+自己主线程ID加锁,设置锁过期时间默认30秒
- 若第一步未获取到锁,则去订阅解锁消息,当获取到锁剩余过期时间后,调用信号量方法堵塞住方法,知道被唤醒或等待超时。
- 一旦持有锁的线程释放了锁,就会广播解锁消息。第二步堵塞的线程就会被唤醒并重新尝试获取锁。
- 为了解决锁到期业务可能未执行完的情况,设置定时任务看门狗每隔10s给未完成的业务方法的锁续期30秒。
- 解锁时,判断是不是自己的锁,不是自己的锁不删除。
代码:
1
2
3
4
5redisson.lock();
try{
} finally{
redisson.unlock();
}
使用案例
存在的问题:主从节点不同步,锁丢失。从节点也获取到锁。
服务器4个节点不如3个节点。
持久化时每隔1秒刷新,机器宕机恢复时锁丢失。
解决:
红锁:
三个相等得Redis。
超过半数Redis节点加锁成功才算加速成功。
红锁得坑:
不能配从节点
分布式锁的选择:
Redis锁有时就是会丢失,很严格可以选择性能略低的zookeeper.
参考concurrentHashMap的分段锁
9.用户认证与授权
介绍
认证:也就是说对于每一次访问请求,系统都能判断出访问者是否具有合法的身份标识。(是谁)
AT性)。)而每个资源都有一个拥有者(Resource Owner)。这些资源拥有者所拥有的资源统一存放在资源服务器(Resource Server)中。同时,协议规定需要有一台授权服务器(Authorization Server),即专门用来处理对访问请求进行授权的服务器。
对哪些调用关系进行认证与授权:客户端到服务、从服务到服务。
授权协议OAuth2
OAuth2 协议中把需要访问的接口或服务统称为资源,而每个资源都有一个拥有者(Resource Owner)。这些资源拥有者所拥有的资源统一存放在资源服务器(Resource Server)中。同时,协议规定需要有一台授权服务器(Authorization Server),即专门用来处理对访问请求进行授权的服务器。
OAuth2 协议在客户端程序和资源服务器之间设置了一个授权层,所以客户端程序不能直接访问资源服务器,而是只能先登录授权层。资源拥有者会首先授权给客户端,客户端获得授权之后,向授权服务器申请一个 Token,Token 中就包含了权限范围和有效期。然后,客户端使用这个申请到的 Token 向资源服务器申请获取资源,资源服务器就根据 Token 的权限范围和有效期向客户端开放拥有者的资源。
对应到微服务系统中,服务提供者所充当的角色就是资源服务器,而服务消费者就是客户端。
OAuth 2.0 定义了四种授权方式,即密码模式、授权码模式、简化模式和客户端模式。
JWT
介绍 博客地址TODO
JJWT使用demo
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>创建token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@SpringBootTest
public class JwtTestor {
/**
* 创建Token
*/
@Test
public void createJwt(){
//私钥字符串
String key = "1234567890_1234567890_1234567890";
//1.对秘钥做BASE64编码
String base64 = new BASE64Encoder().encode(key.getBytes());
//2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法
SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());
//3.利用JJWT生成Token
String data = "{\"userId\":123}"; //载荷数据
String jwt = Jwts.builder().setSubject(data).signWith(secretKey).compact();
System.out.println(jwt);
}
}验证token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* 校验及提取JWT数据
*/
@Test
public void checkJwt(){
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxMjN9In0.1p_VTN46sukRJTYFxUg93CmfR3nJZRBm99ZK0e3d9Hw";
//私钥
String key = "1234567890_1234567890_1234567890";
//1.对秘钥做BASE64编码
String base64 = new BASE64Encoder().encode(key.getBytes());
//2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法
SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());
//3.验证Token
try {
//生成JWT解析器
JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
//解析JWT
Jws<Claims> claimsJws = parser.parseClaimsJws(jwt);
//得到载荷中的用户数据
String subject = claimsJws.getBody().getSubject();
System.out.println(subject);
}catch (JwtException e){
//所有关于Jwt校验的异常都继承自JwtException
System.out.println("Jwt校验失败");
e.printStackTrace();
}
}
Spring Cloud Security
实现 OAuth2 协议以及整合 JWT 认证
基于网关的统一用户认证

执行流程
认证中心微服务负责用户认证任务,在启动时从 Nacos 配置中心抽取 JWT 加密用私钥;
用户在登录页输入用户名密码,客户端向认证中心服务发起认证请求
1
2// 请求示例
http://usercenter/login #认证中心用户认证(登录)地址认证中心服务根据输入在用户数据库中进行认证校验,如果校验成功则返回认证中心将生成用户的JSON数据并创建对应的 JWT 返回给客户端
1
2
3
4
5
6
7
8
9
10
11
12
13// 响应数据示例
{
"code": "0",
"message": "success",
"data": {
"user": {
"userId": 1,
"username": "zhangsan",
},
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCIsXCJuYW1lXCI6XCLlvKDkuIlcIixcImdyYWRlXCI6XCJub3JtYWxcIn0ifQ.1HtfszarTxLrqPktDkzArTEc4ah5VO7QaOOJqmSeXEM"
}
}在收到上述 JSON 数据后,客户端将其中 token 数据保存在 localstorage
客户端向具体某个微服务发起新的请求,这个 JWT 都会附加在请求头或者 cookie 中发往 API 网关,网关将 JWT 再次转发给用户认证服务,此时用户认证服务对 JWT 进行验签,验签成功,查询用户认证与授权的详细数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20{
"code": "0",
"message": "success",
"data": {
"user": { #用户详细数据
"userId": 1,
"username": "zhangsan",
"name": "张三",
"grade": "normal"
"age": 18,
"idno" : 130.......,
...
},
"authorization":{ #权限数据
"role" : "admin",
"permissions" : [{"addUser","delUser","..."}]
}
}
}
网关根据路由规则将请求与jwt数据和授权数据转发至具体的微服务。
具体的微服务收到上述 JSON 后,对当前执行的操作进行判断,检查是否拥有执行权限,权限检查通过执行业务代码,权限检查失败返回错误响应。
JWT续签
JWT存在的问题:JWT 生成后失效期是固定的, JWT 的设计本身就不允许生成完全相同的字符串。单靠 JWT 自身特性是无法做到续签。
需解决:很多业务中需要客户端在不改变 JWT 的前提下,实现 JWT 的“续签”功能。
业务问题:10分钟不操作,token失效,跳转登录页面。
解决方案:
- 生成的 JWT 设为“永久生效”。
- 生成后存到redis集合A中,设置Expire 有效期5分钟。过期后转移到另一个集合B中,有效期5分钟。
- 前端发起请求不同时间段的处理。
- 前端1-5分钟的请求。在redis的集合A中找到携带的token,处理业务。
- 前端6-10分钟的请求,
- 在redis的集合A中未找到携带的token,在集合B中找到了,将集合B中的token移到集合A中。处理业务。
- 前端超过10分钟的请求。在redis的集合A中未找到携带的token,在集合B中也没找到。返回到登录页面
微服务缓存设计
软件缓存分类:
- Web应用客户端缓存-浏览器
- 应用层静态资源缓存-前端-CDN、Nginx
- 服务层多级缓存-后端-内存(分布式Redis,进程内EhCache)

客户端缓存
浏览器层面我们主要是对 HTML 中的图片、CSS、JS、字体这些静态资源进行缓存。
示例:

通过HTTP 的 Expires 响应头控制静态图片的有效期。百度 Logo 的过期时间为 2031 年 2 月 8 日 9 时 26 分 31 秒。在这个时间段内,浏览器会将图片以文件形式缓存在本地,再次访问时会看到“from disk cache”的提示,此时浏览器不再产生与服务器的实际请求,会从本地直接读取缓存图片。
应用层缓存
浏览器负责读取Expires响应头, Expires 在应用层设置,也就是 CDN 与 Nginx 中进行设置。
CDN
介绍
CDN 全称是 Content Delivery Network,即内容分发网络,是互联网静态资源分发的主要技术手段。
场景:大量的上海用户同时要访问千里之外的北京服务器的资源,这么长的通信必然带来高延迟与更多不可控因素影响数据传输。
应用:广域的互联网应用,CDN 几乎是必需的基础设施,它有效解决了带宽集中占用以及数据分发的问题。像 Web 页面中的图片、音视频、CSS、JS 这些静态资源,都可以通过 CDN 服务器就近获取。
CDN执行流程

在互联网应用中,因为 CDN 涉及多地域多节点组网,前期投入成本较高,更多的中小型软件公司通常会选择阿里云、腾讯云等大厂提供的 CDN 服务,通过按需付费的方式降低硬件成本。
阿里云、腾讯云 CDN 除了缓存文件之外,还提供了管理后台能为响应赋予额外的响应头。如下所示在阿里云 CDN 后台,就额外设置了 Cache-Control 响应头代表缓存有效期为 1 小时。这里我们额外提一下 Expires 与的 Cache-Control 的区别,Expires 是指定具体某个时间点缓存到期,而 Cache-Control 则代表缓存的有效期是多长时间。Expires 设置时间,Cache-Control 设置时长,根据业务场景不同可以使用不同的响应头。

Nginx
介绍
功能:
- 反向代理
- 负载均衡
- 静态资源缓存与压缩功能
静态资源缓存配置
1 | # 设置缓存目录 |
服务端缓存
部署方式分类:进程内缓存,分布式缓存
进程内缓存
介绍:应用中开辟的一块内存空间,数据在运行时被载入这块内存,通过本地内存的低延迟、高吞吐的特性提高程序的访问速度。
应用场景:进程内缓存在众多 Java 框架内都有广泛应用,例如 Hibernate、Mybatis 框架的一二级缓存、Spring MVC 的页面缓存都是进程内缓存的经典应用场景。
开源实现:如 EhCache、Caffeine。
分布式缓存
介绍:对多个服务数据的热点数据进行集中缓存。
实现:基于 Redis 内存型 NoSQL 数据库,对多个应用的热点数据进行集中缓存。
多级缓存
介绍:进程内缓存结合分布式缓存。
优点:性能高,分摊分布式缓存压力。查询相同数据时直接从本地 EhCache 缓存提取,不再产生新的网络通信,应用查询性能得到显著提高。
缺点:复杂,增加数据一致性问题
场景:
- 缓存的数据是稳定的。例如邮政编码、地域区块、归档的历史数据这些信息适合通过多级缓存减小 Redis 与数据库的压力。
- 瞬时可能会产生极高并发的场景。例如春运购票、双 11 零点秒杀、股市开盘交易等,瞬间的流量洪峰可能击穿 Redis 缓存,产生流量雪崩。这时利用预热的进程内缓存分摊流量,减少后端压力。
- 一定程度上允许数据不一致。例如某博客平台中你修改了自我介绍这样的非关键信息,此时在应用集群中其他节点缓存不一致也并不会带来严重影响,对于这种情况我们采用T+1的方式在日终处理时保证缓存最终一致就可以了。
实现
在 Java 应用层面,只有 EhCache 的缓存不存在时,再去 Redis 分布式缓存获取,如果 Redis 也没有此数据再去数据库查询,数据查询成功后对 Redis 与 EhCahce 同时进行双写更新。

数据一致性问题
不同应用EhCache数据一致性问题。
解决:引入 MQ 消息队列,利用 RocketMQ 的主动推送功能来向其他服务实例以及 Redis 缓存服务发起变更通知。

微服务部署
软件部署发展过程
物理机部署
应用程序安装在物理服务器的操作系统中,应用程序直接通过操作系统获取物理服务器的 CPU、内存、硬盘等资源。
缺点:CPU 闲置、内存过剩等资源浪费。物理机价格高
虚拟机部署
通过 VMWare 或者 VirtualBox 等虚拟化工具,可以将高性能物理服务器切割为若干虚拟机,这些虚拟机拥有自己独立的 CPU、内存、硬盘资源,并且这些资源彼此隔离不允许交叉访问。
运维工程师就可以为不同类型的应用分配不同的资源,如计算密集型的应用就多分配一些 CPU 核数,存储密集型应用就多分配一些内存与硬盘空间,并且这些资源可以在不停机的情况下实现动态调整,让服务器资源得到最大化的利用。
容器化部署
通过将应用程序镜像化,再通过镜像直接生成一个个容器实现应用的快速部署发布;同时容器化技术不再强调资源隔离,所有容器底层通过 Docker 容器引擎与操作系统获取全局共享的物理机资源。
优点:
- 标准化的部署过程。因为容器化关注应用本身,因此创建容器的过程就是部署应用的过程。容器将是标准化的产物,可能容器内部的应用程序功能各不相同,但对运维人员来说创建容器的命令与操作过程都是基本相同的,可以通过脚本快速批量的完成容器的创建。
- 更好的性能。相比虚拟机,容器化并不强调资源隔离,物理机的所有资源对于容器都是共享的,容器与底层资源之间通过 Docker 容器引擎与操作系统进行调度,这中间产生的损耗相比虚拟机小得多。
软件安装包-容器化技术Docker
单独写TODO
介绍
Docker 是一个开源的应用容器引擎,基于 Go 语言开发。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,Docker 经过多年发展已经是容器化技术的标准。
概念
镜像,仓库,Dockerfile,容器,容器编排工具
镜像(Image)
类似于安装包,可以用安装包安装软件程序。
可以使用镜像在任何安装了 Docker 的 Linux 系统上快速部署应用程序。
jar包打成镜像后,可以用镜像在多个服务器快速部署。
仓库
仓库是存放镜像的地方,Docker 提供了 DockerHub 仓库站托管开发者的镜像文件,开发者可以利用 Pull 命令直接从仓库下载镜像到本地部署。
生产环境需要自己搭建HARBOR 仓库。如同代码仓库GitLab。
Dockerfile
描述将应用程序构建为镜像的过程。如:将jar构建为镜像。
运维可以用镜像屏蔽硬件系统差异在不同实例部署多个应用程序。
容器(Container)
安装包安装成功后是软件(应用程序)。
镜像部署成功后就是容器,容器是镜像的实例。
容器编排工具
容器编排工具的典型代表是 Google Kubernetes(K8S) 和 Docker Swarm,容器编排工具用于管理大规模集群中的容器实例。
服务器网络间自动实现互联互通,随着外部用户的访问压力的变化自动进行容器的扩容与收缩。
K8S 允许运维人员通过可视化的方式对容器进行动态调整,同时对所有运行节点也提供了实时监控。

1.20后k8s放弃了docker部分功能,比如网络与volume,国内基本都是1.1x。
DevOps执行流程

执行流程:
- 研发工程师将测试验收后的源码上传到 GitLab 服务器,并合并到生成分支(包含 Dockerfile,用来描述 Docker 镜像的构建过程)。
- 软件工程师或者配置管理员发起 Jekins 的自动化脚本,完成镜像的自动化构建与仓库推送。
- 上线日运维工程师接入 Kubernetes 管理端,发起 Deploy 部署命令,此时生产环境的 K8S 节点会从 HARBOR 仓库抽取最新版本的应用镜像,并在服务器上自动创建容器,最新版本的 Jar 文件在容器创建时也会随之启动开始对外提供服务。
- 抽取新版本源码到 Jekins 服务器,利用 Jekins 服务器安装 Maven 自动完成编译、测试、打包的过程;产出jar文件
- 抽取 Dockerfile 到 Jekins 服务器,利用 Jekins 服务器安装的 Docker 完成镜像的构建工作,在构建过程中需要将上一步生成的 Jar 文件包含在内,在容器创建时自动执行这个 Jar 文件。
- 镜像生成后,还是通过 Jekins 服务器上的 Docker 将新版本镜像推送到 HARBOR 仓库。(HARBOR 用于创建 Docker 镜像的私有仓库)
- 在校验无误后,本次上线宣告成功。
真实环境异常因素:
- 源码编译、打包时产生异常的快速应对机制
- 上线失败如何快速应用回滚
- 镜像构建失败的异常跟踪与补救措施
老项目升级为微服务策略
单体应用改造升级为微服务架构。
痛点
分布式事务54%
全链路跟踪43%
服务平滑上下线33%
面临的问题
- 改造是一步到位还是逐渐迭代?
- 微服务拆分的粒度是什么?
- 如何保证数据一致性?
- 新老交替过程中如何不影响公司业务进展?
策略
- 严禁 Big Bang(一步到位);
- 尽早体现价值;
- 优先分离做前后端;
- 新功能构建成微服务;
- 数据源不混用
- 利用 Spring AOP 开发低侵入的胶水代码;
- 基于 MQ 构建反腐层。
绞杀式升级
介绍:
一个由微服务组成的新应用程序,通过将新功能作为服务,并逐渐从单体应用中提取服务来实现。随着时间的推移,越来越多单体应用内的功能被逐渐剥离为独立的微服务,最终达到消灭单体应用的目的。
优点:
- 升级改造过程中并不需要推翻原有的代码,而是在新老更迭的过程中一步步完成微服务架构的升级改造。
- 立即获得投资回报。
严禁 Big Band
逐步重构单体应用,采用绞杀者应用策略,将应用变为单体与微服务的混合状态,随着时间增加一点点蚕食掉单体应用。
尽早体现价值
按价值的重要性进行排序。
优先分离做前后端
前后端独立部署、扩展与维护。表示层在快速迭代部署时并不影响后端功能,可以轻松进行 A/B 测试。
新功能构建成微服务
例如检索服务。增加了 API Gateway 网关,该网关对前端访问的 URL 进行路由。访问 search 接口,则请求被重定向到新创建的商品检索微服务,通过 ElasticSearch 这种专用的全文检索引擎提供更高级的查询功能;
数据源不混用
可使用 Alibaba Canal 做数据源同步,Canal 是阿里巴巴旗下的开源项目,纯 Java 开发。基于数据库增量日志解析,提供增量数据订阅&消费,可自动实现从 MySQL 数据源向其他数据源同步数据的任务。
旧功能修改
Spring AOP 扩展
1 | @Component("priceServiceAspect") //声明Bean Id |
MQ构建反腐层

分布式sql
不使用join而使用in,不同mysql实例不能使用join