SpringCloud
父工程搭建
创建 Maven 工程
字符编码
注解激活生效
删除项目中的 src 文件夹
修改 porm.xml
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cuc</groupId>
<artifactId>SpringCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 打包方式 -->
<packaging>pom</packaging>
<!--统一管理jar包版本-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!--子模块继承之后,提供作用:锁定版本+子module不用写groupId和version-->
<dependencyManagement>
<dependencies>
<!--springboot2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloudHoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloudalibaba2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency><dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>Maven常用
dependencyManagement
元素来提供一种管理依赖版本号的方式。通常在一个项目的最顶层父POM
中看到 dependencyManagement 元素。它能让所有子项目
引入依赖而不需要显式列出版本号
。只要子项目中没有指定版本,Maven 会向上找,直到找到拥有 dependencyManagement 元素的项目,然后就会使用 dependencyManagement 元素中指定的版本
,包括scope
都取自父pom。dependencyManagement 只是声明版本,并不真正引入。跳过单元测试
点击闪电图标,打包不会将单元测试打进去。
模块构建
微服务提供者
新建 maven 模块
cloud-provider-payment8001
引入依赖
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<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--热部署(可省去)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_springcloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.cuc.springcloud.entities # 所有Entity类所在包主启动
1
2
3
4
5
6
7
8
9
10
11package com.cuc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}建表SQL
1
2
3
4
5CREATE TABLE `payment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`serial` varchar(200) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.cuc.springcloud.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
public class Payment implements Serializable {
private Long id;
private String serial;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.cuc.springcloud.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message){
this(code,message,null);
}
}接口Dao
1
2
3
4
5
6
7
8
9
10
11package com.cuc.springcloud.dao;
import com.cuc.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
public interface PaymentDao {
int create(Payment payment);
Payment getPaymentById(; Long id)
}映射文件
路径:
resource/mapper/PaymentMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mapper namespace ="com.cuc.springcloud.dao.PaymentDao" >
<resultMap id ="BaseResultMap" type ="com.cuc.springcloud.entities.Payment" >
<id column ="id" property ="id" jdbcType ="BIGINT" />
<result column ="serial" property ="serial" jdbcType ="VARCHAR" />
</resultMap>
<insert id ="create" parameterType ="Payment" useGeneratedKeys ="true" keyProperty ="id" >
INSERT INTO payment(SERIAL) VALUES(#{serial});
</insert>
<select id ="getPaymentById" parameterType ="Long" resultMap ="BaseResultMap" >
SELECT * FROM payment WHERE id=#{id};
</select>
</mapper>接口 Service
1
2
3
4
5
6
7
8
9
10package com.cuc.springcloud.service;
import com.cuc.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Param;
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(; Long id)
}Service 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.cuc.springcloud.service.impl;
import com.cuc.springcloud.dao.PaymentDao;
import com.cuc.springcloud.entities.Payment;
import com.cuc.springcloud.service.PaymentService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
public class PaymentServiceImpl implements PaymentService {
private PaymentDao paymentDao;
public int create(Payment payment) {
return paymentDao.create(payment);
}
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}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
37
38
39
40package com.cuc.springcloud.controller;
import com.cuc.springcloud.entities.CommonResult;
import com.cuc.springcloud.entities.Payment;
import com.cuc.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
public class PaymentController {
private PaymentService paymentService;
public CommonResult create({ Payment payment)
int result = paymentService.create(payment);
log.info("插入操作返回结果:"+result);
if(result>0){
return new CommonResult(200,"插入成功",result);
}else{
return new CommonResult(444,"插入失败",null);
}
}
public CommonResult getPaymentById({ Long id)
Payment payment = paymentService.getPaymentById(id);
log.info("查询结果:{}",payment);
if (payment!=null){
return new CommonResult(200,"查询成功",payment);
}else{
return new CommonResult(444,"查询失败",null);
}
}
}测试
客户端消费者
新建 maven 模块
cloud-consumer-order80
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>配置文件
1
2server:
port: 80主启动类
1
2
3
4
5
6
7
8
9
10
11
12package com.cuc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}实体类
将服务提供者
entities
复制过来即可。配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.cuc.springcloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
public class ApplicationContextConfig {
/**
* 是什么?
* RestTemplate提供了多种便捷访问远程Http服务的方法
* 是Spring提供的用于访问Rest服务的 客户端模板工具集
* 使用
* 使用restTemplate访问restful接口非常的简单粗暴无脑
* (url, requestMap, ResponseBean.class)这三个参数分别代表 REST请求地址、请求参数、返回结果。
*/
public RestTemplate restTemplate(){
return new RestTemplate();
}
}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
32package com.cuc.springcloud.controller;
import com.cuc.springcloud.entities.CommonResult;
import com.cuc.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
public class OderController {
public static final String PAYMENT_URL = "http://localhost:8001";
private RestTemplate restTemplate;
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
}
public CommonResult<Payment> getPaymentById({ Long id)
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
}测试
启动 服务提供者和客户端消费者进行测试。
这里不用写不写80端口无所谓,因为浏览器默认访问的就是80端口。
工程重构
在上面两个模块中,实体类完全一样,造成代码冗余。
以下通过新建
cloud-api-commons
项目解决此问题。
新建 maven 模块
cloud-api-commons
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>创建跟两个项目一样的实体类,并将两个项目的实体类删除
打包(clean、intall)
两个项目中引入打好的包
1
2
3
4
5<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>重构完成。
目前工程样图
服务注册中心
Eureka
概述
Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来
实现服务治理
。类似 Dubbo 架构。
具体流程:
- 启动eureka注册中心
- 启动服务提供者,会将自身信息(比如服务地址以别名的方式注册进eureka)
- 消费者在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址
- 消费者获取调用地址后,底层实际利用HttpClient技术实现远程调用
- 消费者获得服务地址后会缓存在本地jvm内存中,默认每隔三十秒更新一次服务调用地址
Eureka 包含两个组件:Eureka Server 和 Eureka Client :
Eureka Server 提供服务注册服务
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client 通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内(默认90秒)没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除。
单机构建
EurekaServer
创建 maven 模块
cloud-eureka-server7001
引入依赖
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<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--引入自己定义的api通用包-->
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--bootwebactuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 7001
eureka:
instance:
hostname: localhost # eureka server 实例名称
client:
# false 表示不向注册中心注册自己
register-with-eureka: false
# false 表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置 与Eureka Server交互的地址, 查询服务和注册服务都需要依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/主启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.cuc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class,args);
}
}启动测试
访问
localhost:7001
8001模块
添加 pom 依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7
8eureka:
client:
# 表示是否将自己注册进 EurekaServer 默认为 true
register-with-eureka: true
# 是否从 EurekaServer 抓取已有的注册信息,默认为 true 。单节点无所谓,集群必须设置为 true 才能配合 ribbon 使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka主启动添加注解
1
测试
先启动7001服务注册中心再启动当前项目
其中红框内对应配置文件的配置:
80模块
添加依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7
8
9
10spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka主启动添加注解
@EnableEurekaClient
测试
先启动7001,再启动8001,最后启动80
访问
localhost/consumer/payment/get/1
进行测试。
集群构建
- 搭建注册中心集群
- 搭建服务提供者集群
注册中心集群
新建 maven 项目
cloud-eureka-server7002
引入依赖
同
cloud-eureka-server7001
添加映射配置
路径
/etc/hosts
(Mac)配置文件
注意:7001以及7002都要修改。
7001:
1
2
3
4
5
6
7
8
9
10server:
port: 7001
eureka:
instance:
hostname: eureka7001.com
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka7002.com:7002/eureka/7002:
1
2
3
4
5
6
7
8
9
10server:
port: 7002
eureka:
instance:
hostname: eureka7002.com
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/主启动类
1
2
3
4
5
6
7
8
9
10
11
12
13package com.cuc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
public class EurekaMain7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7002.class,args);
}
}修改80以及8001配置文件
1
2- defaultZone: http://localhost:7001/eureka
+ defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka测试
先启动7001和7002,然后启动8001,最后启动80即可访问测试。
其中可以通过访问hosts配置的域名映射访问界面(感觉没啥用,就是localhsot加上不同的端口)
服务提供者集群
两个提供相同功能的服务提供者,在注册中心共同使用一个服务别名。
新建 maven 项目
cloud-provider-payment8002
代码都跟8001一样,除了配置文件的端口。注意配置文件中的服务名称是一样的。
修改8001以及8002的Controller
为了方便下面负载均衡的测试,在控制器类中修改代码以看出调用的哪个服务。
负载均衡
1)修改80中的Controller
1
2
3
4- public static final String PAYMENT_URL = "http://localhost:8001";
//通过在eureka上注册过的微服务名称调用
+ public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";2)使用@LoadBalanced注解赋予RestTemplate负载均衡的能力,在80配置类中修改。
测试
启动顺序不多说了,之后浏览器一直访问 http://localhost/consumer/payment/get/1,发现请求会轮流调用两个提供者的接口。
Actuator
微服务信息完善
修改8001配置文件
可以看到注册中心中8001相应名称替换掉了
但是鼠标移上去浏览器还是会显示
好像再配置下面这行就可以,试过没起作用🫠。
1 | eureka: |
Discovery
对于注册进eureka里面的微服务,可以通过服务发现来获取该服务的信息。
在8001的Controller添加如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private DiscoveryClient discoveryClient;
public Object discovery(){
List<String> services = discoveryClient.getServices();
for (String service:services){
System.out.println(service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance:instances){
System.out.println(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}在8001主启动类添加注解
@EnableDiscoveryClient
测试
自我保护
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当EurekaServer节点在短时间内丢失过多微服务实例心跳时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式,不会剔除该服务。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。
在注册中心的配置文件使用:
1 | eureka.server.enable-self-preservation = false # 可以禁用自我保护模式,默认开启 |
在服务提供者的配置文件中也可以修改:
1 | eureka.instance.lease-renewal-interval-in-seconds=30 # 客户端向服务端发送心跳的时间 |
Zookeeper
ZK注册中心
在Linux下搭建zookeeper。
服务提供者
新建 maven 项目
cloud-provider-payment8004
引入依赖
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<dependencies>
<!--SpringBoot整合Web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot整合zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!--zookeeper版本跟搭建的一致-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>配置文件
1
2
3
4
5
6
7
8server:
port: 8004
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.1.101:2181主启动类
1
2
3
4
5
6
7
8
// 该注解用于向使用 consul 或者 zookeeper 作为注册中心时注册服务
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class PaymentController {
private String serverPort;
public String paymentZK(){
return "SpringCloud with Zookeeper"+serverPort+"\t"+ UUID.randomUUID();
}
}测试
现将zookeeper服务启动,再启动8004。启动后会在zookeeper多出 /services 节点
服务消费者
新建 maven 模块
cloud-consumerzk-order80
引入依赖
同服务提供者
配置文件
1
2
3
4
5
6
7
8server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 192.168.1.101:2181主启动
1
2
3
4
5
6
public class OrderZK80 {
public static void main(String[] args) {
SpringApplication.run(OrderZK80.class,args);
}
}配置类
1
2
3
4
5
6
7
8
9
public class ApplicationContextBean {
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderZKController {
public static final String INVOKE_URL = "http://cloud-provider-payment";
private RestTemplate restTemplate;
public String paymentInfo(){
String result = restTemplate.getForObject( INVOKE_URL + "/payment/zk" , String.class );
System.out.println("消费者调用支付服务 (zookeeper)--->result:" + result);
return result;
}
}测试
注意服务在 ZK 创建的节点为临时节点。
Consul
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司 用 Go 语言开发 。下载地址、怎么玩
安装运行Consul
服务提供者
新建 maven 模块
cloud-providerconsul-payment8006
引入依赖
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<dependencies>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>配置文件
1
2
3
4
5
6
7
8
9
10
11
12server:
port: 8006
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}主启动
1
2
3
4
5
6
7
8
public class PaymentMain8006 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class PaymentController {
private String serverPort;
public String paymentInfo(){
return "SpringCloud with Consul: " + serverPort + " \t\t " + UUID. randomUUID ();
}
}运行
服务消费者
新建模块
cloud-consumerconsul-order80
引入依赖
同提供者。
配置文件
1
2
3
4
5
6
7
8
9
10
11
12server:
port: 80
spring:
application:
name: consul-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}主启动
1
2
3
4
5
6
7
8
public class OrderConsulMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class,args);
}
}配置类
1
2
3
4
5
6
7
8
9
public class ApplicationContextBean {
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderConsulController {
public static final String INVOKE_URL = "http://consul-provider-payment" ;
private RestTemplate restTemplate;
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
return result;
}
}运行测试
异同点
组件 | 语言 | CAP | 服务健康检查 | 暴露接口 | SpringCloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
- C:强一致性
- A:可用性
- P:分区容错性
CAP理论关注粒度是数据,而不是系统整体设计。
服务调用
Ribbon
概述
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端
负载均衡
的工具。Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。 Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
- 先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server。
- 再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
Ribbon VS Nginx
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
引入
spring-cloud-starter-netflix-eureka-client
自带了spring-cloud-starter-ribbon
引用。
RestTemplate
上面已经演示过了。get/post 代表请求方法
getForObject/postForObject
返回对象为响应体中数据转化成的对象,基本上可以理解为Json。
getForEntity/postForEntity
返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
IRule
Ribbon核心组件,根据特定算法从服务列表中选取一个要访问的服务。
RoundRobinRule:轮询
RandomRule:随机
RetryRule
先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。
WeightedResponseTimeRule
对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择。
BestAvailableRule
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
AvailabilityFilteringRule
先过滤掉故障实例,再选择并发较小的实例。
ZoneAvoidanceRule
默认规则,复合判断server所在区域的性能和server的可用性选择服务器。
如何替换?
修改 cloud-consumer-order80
新建 package
为什么要新建包?
Ribbon配置类不能放在@ComponentScan所扫描的当前包下以及子包下,也就是不能放在主启动类的目录或子目录下。
否则配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
在myrule包下新建配置类
1
2
3
4
5
6
7
8
public class MySelfRule {
public IRule myRule(){
return new RandomRule(); // 定义为随机
}
}主启动添加注解
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
测试
按顺序启动7001、8001、8002、80。之后多次访问 http://localhost/consumer/payment/get/1 即可看到请求随机分配到8001和8002。
负载均衡算法
原理
==rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标== ,每次服务重启动后rest接口计数从1开始。
如: List [0] instances = 127.0.0.1:8001
List [1] instances = 127.0.0.1:8002
按照轮询算法原理:
当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8002
当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8001
当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8002
当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8001
如此类推……
手写
试着写一个本地负载均衡器。以下均在
cloud-consumer-order80
模块进行改造。
ApplicationContextConfig 去掉注解 @LoadBalanced
新建 LoadBalancer 接口
1
2
3
4
5
6
7
8
9package com.cuc.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}实现接口
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
31package com.cuc.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class MyLB implements LoadBalancer{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement(){
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1 ;
}while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("*****next:"+next);
return next;
}
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}在 OrderController 添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 可以获取注册中心上的服务列表
private DiscoveryClient discoveryClient;
private LoadBalancer loadBalancer;
public CommonResult<Payment> getPaymentLB({ Long id)
List<ServiceInstance> instances = discoveryClient.getInstances( "CLOUD-PAYMENT-SERVICE");
if (instances==null || instances.size()<=0){
return null;
}
ServiceInstance instance = loadBalancer.instances(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri+"/payment/get/"+id,CommonResult.class);
}测试
OpenFeign
概述
Feign 是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可
。
Feign能干什么?
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处, 往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。 所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下, 我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可) ,即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon
利用Ribbon维护了服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是, 通过feign只需要定义服务绑定接口且以声明式的方法 ,优雅而简单的实现了服务调用。
使用
新建
cloud-consumer-feign-order80
引入依赖
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<dependencies>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>配置文件
1
2
3
4
5
6
7
8server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka主启动
1
2
3
4
5
6
7
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}Service
1
2
3
4
5
6
public interface PaymentFeignService {
CommonResult<Payment> getPaymentById(; Long id)
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class OrderFeignController {
private PaymentFeignService paymentFeignService;
public CommonResult<Payment> getPaymentById({ Long id)
return paymentFeignService.getPaymentById(id);
}
}测试
总结
超时控制
演示超时情况:
8001 Controller
1
2
3
4
5
6
public String paymentFeignTimeOut() throws InterruptedException {
// 暂停三秒
TimeUnit.SECONDS.sleep(3);
return serverPort;
}80 Service
1
2
String paymentFeignTimeOut();80 Controller
1
2
3
4
5
public String paymentFeignTimeOut(){
// 默认等待 1 秒
return paymentFeignService.paymentFeignTimeOut();
}测试
Feign客户端默认只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
配置文件开启配置:
在80模块配置
1 | # 设置 feign 客户端超时时间(OpenFeign默认支持 ribbon ) |
日志打印
Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
日志级别:
- NONE:默认,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
使用:
配置日志Bean
1
2
3
4
5
6
7
8
public class FeignConfig {
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}配置文件
1
2
3
4logging:
level:
# feign 日志以什么级别监控哪个接口
com.cuc.springcloud.service.PaymentFeignService: debug测试
Hystrix断路器
问题
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的扇出
。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的雪崩效应
。
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
概述
Hystrix是一个用于处理分布式系统的 延迟
和 容错
的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝), 向调用方返回一个符合预期的、可处理的备选响应(FallBack) , 而不是长时间的等待或者抛出调用方无法处理的异常 ,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
重要概念
1)服务降级
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback。
哪些情况会出发降级?
- 程序运行异常
- 超时
- 服务熔断出发服务降级
- 线程池/信号量打满也会导致服务降级
2)服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
3)服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
案例里都有说明。
案例
构建服务提供者
新建
cloud-provider-hystrix-payment8001
POM
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<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka主启动
1
2
3
4
5
6
7
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PaymentService {
/**
* 正常访问,一切 OK
*/
public String paymentInfo_OK(Integer id){
return "线程池:"+Thread.currentThread().getName()+"paymentInfo_OK,id:"+id+"\t"+"O(∩_∩)O";
}
/**
* 超时访问,演示降级
*/
public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
TimeUnit.SECONDS.sleep(3);
return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id:"+id+"\t"+"O(∩_∩)O耗费 3 秒";
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PaymentController {
private PaymentService paymentService;
private String serverPort;
public String paymentInfo_OK({ Integer id)
String result = paymentService.paymentInfo_OK(id);
return result;
}
public String paymentInfo_TimeOut( Integer id)throws InterruptedException {
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
}
构建服务消费者
创建
cloud-consumer-feign-hystrix-order80
POM
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<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign </artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-consumer-order80</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/主启动
1
2
3
4
5
6
7
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}Service 接口
1
2
3
4
5
6
7
8
9
10
public interface PaymentHystrixService {
String paymentInfo_OK(; Integer id)
String paymentInfo_TimeOut(; Integer id)
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderHystirxController {
private PaymentHystrixService paymentHystrixService;
public String paymentInfo_OK({ Integer id)
return paymentHystrixService.paymentInfo_OK(id);
}
public String paymentInfo_TimeOut({ Integer id)
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
高并发测试
使用 jmeter 并发40000个请求访问 paymentInfo_TimeOut
运行这些请求,然后浏览器访问 80 的 ok 路由,正常情况下会立刻返回数据,但是高并发进行时,要么访问变慢要么访问超时报错(如下)。
服务降级
如何解决?
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,服务降级
- 对方服务(8001)down机了,调用者(80)不能一直卡死等待,服务降级
- 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(例如设有最大等待时间),服务降级
==针对 8001==
修改接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 超时访问,演示降级
*/
public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
TimeUnit.SECONDS.sleep(3);
return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id:"+id+"\t"+"O(∩_∩)O耗费 3 秒";
}
public String paymentInfo_TimeOutHandler(Integer id){
return "/( ㄒ o ㄒ )/调用接口超时或异常"+id;
}主启动添加注解
@EnableCircuitBreaker
测试
通过测试知道处理时间超过2秒会调用paymentInfo_TimeOutHandler函数返回结果,需要注意的是如果没有超时,但是在处理的时候报错,也是会调用paymentInfo_TimeOutHandler函数的。
==针对 80==
服务降级一般放在消费端,👆只是为了说明服务降级也可以用在服务端。
撤回服务端降级代码,使消费者能正常接受
YML
1
2
3feign:
hystrix:
enabled: true主启动添加注解
@EnableHystrix
修改Controller
1
2
3
4
5
6
7
8
9
10
11
12
public String paymentInfo_TimeOut({ Integer id)
return paymentHystrixService.paymentInfo_TimeOut(id);
}
public String paymentInfo_TimeOutHandler(Integer id){
return "/( ㄒ o ㄒ )/调用接口超时或异常"+id;
}测试
==问题及解决==
如果有多个业务需要服务降级,每个业务方法对应一个兜底的方法,会导致代码膨胀。并且兜底方法与业务方法放在一块,耦合度高。
解决代码膨胀:
解决耦合度高:
只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。根据 80 已经有的 PaymentHystrixService 接口,重新新建一个类 PaymentFallbackService 实现该接口,统一为接口里面的方法进行异常处理。
将 80 控制器方法进行还原,使之不出现异常,能正常调用接口。
接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
public class PaymentFallbackService implements PaymentHystrixService{
public String paymentInfo_OK(Integer id) {
return "paymentInfo_OK接口异常";
}
public String paymentInfo_TimeOut(Integer id) {
return "paymentInfo_TimeOut接口异常";
}
}修改Service接口
测试
由上方代码可知当服务接口异常时,会调用实现类里头相应的方法。当然只能对服务提供者(8001)提供的接口错误进行fallback,并不能对自身(80)例如控制器方法错误的fallback。
启动顺序不多说了,启动后,当 8001 的接口出现异常或者处理超时又或者 8001 宕机都会调用实现类里的fallback函数。
服务熔断
==熔断机制概述==
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路
。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。注意跟服务降级不一样。
==案例==
在 8001 的Service接口添加如下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//========服务熔断
public String paymentCircuitBreaker(Integer id){
if (id<0){
throw new RuntimeException(); // 自定义异常
}
return "success";
}
public String paymentCircuitBreaker_fallback(Integer id){
return "false";
}即在时间窗口期内,至少有10次请求过来,并且这些请求中至少有60%请求失败则进行跳闸(使服务不能使用)。
在 8001 的Controller添加如下内容
1
2
3
4
public String paymentCircuitBreaker({ Integer id)
return paymentService.paymentCircuitBreaker(id);
}测试
访问 http://localhost:8001/payment/circuit/33(正确) 或 http://localhost:8001/payment/circuit/-33(错误)
多次访问错误的路径,之后访问正确的路径发现返回false,继续访问正确路径多次后才会返回success。
服务降级 --> 服务熔断 --> 链路恢复
==熔断类型==
熔断关闭
熔断关闭则对服务进行正常调用。
熔断打开
请求不再调用当前服务(服务降级),内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟(默认 5 秒)则进入半熔断状态。
熔断半开
会释放一个请求到原来的服务。如果成功,断路器会关闭,若失败,继续开启并重新计时休眠时间。
==所有配置==
1 |
服务限流
后面alibaba的Sentinel说明。
工作流程
服务监控
除了隔离依赖服务的调用以外,Hystrix还提供了 准实时的调用监控
(Hystrix Dashboard) ,Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
仪表盘9001
新建
cloud-consumer-hystrix-dashboard9001
POM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2server:
port: 9001主启动类
1
2
3
4
5
6
7
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}测试
监控测试
8001需要依赖(之前应该就有)
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>修改 8001 主启动类(注入一个Bean)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
/**
* 服务监控配置
*/
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}监控 8001
Delay
:该参数用来控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗。Title
:该参数对应了头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,可以通过配置该信息来展示更合适的标题。
之后通过访问正确与错误路径可以直观看到断路器的运作。
服务网关
Zuul
不是重点,重点在Gateway。
Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。
API 网关为微服务架构中的服务提供了统一的访问入口,客户端通过API网关访问相关服务。它相当于整个微服务架构中的门面,所有客户端的访问都通过它来进行路由及过滤。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。
学 Gateway 去了🫡
Gateway
概述
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关,但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul, 那就是SpringCloud Gateway 。
路由转发+执行过滤器链
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。 Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等, 在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
能干嘛?
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- … …
三大核心
==Route 路由==
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
==Predicate 断言==
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
==Filter 过滤==
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
配置
新建
cloud-gateway-gateway9527
POM
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<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
Gateway 也可以用Bean注入方式进行配置,不一定使用YML。
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
26server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_route # 路由的 ID ,没有固定规则但要求唯一
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配则进行路由
- id: payment_route2
uri: http://localhost:8001
predicates:
- Path=/payment/feign/timeout
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka主启动类
1
2
3
4
5
6
7
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class,args);
}
}测试
启动7001、cloud-provider-payment8001、9527,之后访问 http://localhost:9527/payment/get/5 进行测试。
新建配置类
演示使用Bean注入方式配置route。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GateWayConfig {
/**
* 当访问地址 http://localhost:9527/guonei 时会自动转发到地址: http://news.baidu.com/guonei
* 当访问地址 http://localhost:9527/guoji 时会自动转发到地址: http://news.baidu.com/guoji
*/
public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
RouteLocatorBuilder.Builder routes = builder.routes();
routes.route("route3",r->r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
public RouteLocator customRouteLocator2(RouteLocatorBuilder builder){
RouteLocatorBuilder.Builder routes = builder.routes();
routes.route("route4",r->r.path("/guoji").uri("http://news.baidu.com/guoji")).build();
return routes.build();
}
}测试
分别访问 http://localhost:9527/guonei 和 http://localhost:9527/guoji
动态路由
以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
修改YML
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
31server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_route
# uri: http://localhost:8001
uri: lb://cloud-payment-service # 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
predicates:
- Path=/payment/get/**
- id: payment_route2
# uri: http://localhost:8001
uri: lb://cloud-payment-service
predicates:
- Path=/payment/feign/timeout
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka测试
启动7001、8001、8002、9527,多次访问 http://localhost:9527/payment/get/5 。会发现轮流调用8001和8002的接口。
Predicate
Predicate就是为了实现一组匹配规则
当启动9527模块控制台:
其中 Path 在上面已经使用过了。
==常用 Predicate==
After Route Predicate
时间格式可通过编写代码获得:
Before Route Predicate
与After相反,给定时间之前才能对路由访问。
Between Route Predicate
代表在给定时间范围内才能对路由访问。
1
2
3
4predicates:
- Path=/payment/get/**
# - After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai]
- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2021-03-25T18:59:06.206+08:00[Asia/Shanghai]Cookie Route Predicate
两个参数:一个cookie名称,一个是正则表达式或属性值。
1
2
3predicates:
- Path=/payment/get/**
- Cookie=username,swjHeader Route Predicate
两个参数:一个是属性名称和一个正则表达式或属性值。
1
- Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 并且值为整数的正则表达式
Host Route Predicate
它通过参数中的主机地址作为匹配规则。 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板。
1
- Host=**.sscarf.com,www.baidu.com
Method Route Predicate
匹配请求方法。
1
- Method=GET
Path Route Predicate
匹配访问路径,之前已经演示了。
Query Route Predicate
匹配参数。支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。
1
- Query=username, \d+ # 要有参数名 username 并且值还要是整数才能路由
Filter
路由过滤器可用于修改进入的HTTP请求(pre)或返回的HTTP响应(post),路由过滤器只能指定路由进行使用。
==种类==
GatewayFilter
31种之多,地址
GlobalFilter
==GatewayFilter 示例==
1 | - AddRequestParameter=X-Request-Id,1024 # 过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 1024 |
其它请查看官网。
==自定义过滤器==
自定义全局 GlobalFilter
1 |
|
Config
概述
分布式配置中心。
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置
。
SpringCloud Config分为 服务端
和客户端
两部分:
服务端也称为分布式配置中心,它是一个独立的微服务应用 ,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口 。
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用 git 来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过 git 客户端工具来方便的管理和访问配置内容。
==能干嘛?==
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露
由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https 访问的形式
服务端配置
在 github 新建仓库 springcloud-config(公开)
克隆到本地
1
git clone git@github.com:keep-out-the-cold/springcloud-config.git
新建文件
在本地仓库新建 config.yml 和 config-test.yml 文件添加一些内容并上传到 github。文件格式必须为UTF-8。
上传:
1
2
3git add .
git commit -m "..."
git push新建模块 cloud-config-center-3344
POM
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<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
仓库不公开还要配置账号密码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://github.com/keep-out-the-cold/springcloud-config.git # 仓库(ssh报错,所以用https)
# 目录
search-paths:
- springcloud-config
default-label: main # 如果是 gitee 则改为 master
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka主启动类
1
2
3
4
5
6
7
public class ConfigCenterMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class,args);
}
}测试(启动7001、3344)
访问 http://localhost:3344/main/config-dev.yml
访问 http://localhost:3344/main/config-111.yml
访问 http://localhost:3344/main/config-test.yml
访问规则如下:
1
2
3
4
5/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml # 上面使用的规则
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties又新增了 cloud-config-center-dev.yml 文件(第二个规则)测试了一下,在测试的时候并没有重启idea项目,将文件push到github后直接浏览器访问就能看到。说明服务不需要重启即可感知到配置的变化并应用新的配置。
客户端配置
新建模块 cloud-config-client-3355
POM
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<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>bootstrap.yml
applicaiton.yml是用户级的资源配置项;bootstrap.yml是系统级的,优先级更加高。
要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的, 因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
label: main # 分支
name: config # 配置文件名称
profile: test # 读取后缀
uri: http://localhost:3344 # 配置中心地址
# 综上,http://localhost:3344/main/config-test.yml
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka主启动类
1
2
3
4
5
6
7
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class ConfigClientController {
// hello为github上对应配置文件里头的配置信息
private String configInfo;
public String getConfigInfo(){
return configInfo;
}
}测试
启动7001、3344、3355,访问 http://localhost:3355/configInfo
问题
当修改了github上的配置文件,服务端(3344)会同步修改,但是客户端(3355)却没有同步修改。
客户端动态刷新
解决上述问题
确保引入下面依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>修改YML
添加如下内容:
1
2
3
4
5
6# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"Controller添加注解
测试
重启3355,修改github上配置文件,访问 http://localhost:3355/configInfo 还是没有变化,需要先
POST
访问 http://localhost:3355/actuator/refresh,再访问才能有变化。还是有点鸡肋💩
Bus
消息总线
概述
Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。
Bus支持两种消息代理:RabbitMQ 和 Kafka
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。 在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
配置RabbitMQ
略
全局广播
演示广播效果,以3355为模板再制作一个3366
新建模块 cloud-config-client-3366
POM
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<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22server:
port: 3366
spring:
application:
name: config-client
cloud:
config:
label: main # 分支
name: config # 配置文件名称
profile: test # 读取后缀
uri: http://localhost:3344 # 配置中心地址
# 综上,http://localhost:3344/main/config-test.yml
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"主启动类
1
2
3
4
5
6
7
public class ConfigClientMain3366 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3366.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConfigClientController {
private String serverPort ;
"${hello}" ) (
private String configInfo ;
public String configInfo(){
return "serverPort:"+serverPort+"\tconfigInfo:"+configInfo;
}
}
设计细想
触发服务端ConfigServer(3344)的/bus/refresh端点,进而利用消息总线刷新所有客户端的配置
给 cloud-config-center-3344 配置中心服务端添加消息总线支持
添加依赖
1
2
3
4
5<!-- 添加消息总线 RabbitMQ 支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7
8
9
10
11
12
13spring:
# rabbitmq 相关配置
rabbitmq:
host: 120.48.54.126
port: 5672
username: admin
password: admin
# rabbitmq 相关配置 , 暴露 bus 刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
给 cloud-config-client-3355 和 cloud-config-client-3366 客户端添加消息总线支持
依赖
同上
配置
同上,需要注意的是客户端的 include 为 “*”,也就包含了 ‘bus-refresh’,所以不用改。
测试
启动
7001、3344、3355、3366
修改 Github 上的配置文件
此时访问 http://localhost:3344/main/config-test.yml 可以看到更改后的内容;
但是访问 http://localhost:3355/configInfo 和 http://localhost:3366/configInfo 则还没有更改。
发送 POST 请求
1
http://localhost:3344/actuator/bus-refresh
测试
再次访问 http://localhost:3355/configInfo 和 http://localhost:3366/configInfo 内容已经刷新。
定点通知
例:只通知 3355 不通知 3366
==公式==
1 | http://localhost:配置中心端口号/actuator/bus-refresh/{destination} |
==测试==
修改 Github 配置文件
POST
访问 http://localhost:3344/actuator/bus-refresh/config-client:3355效果
Stream
消息驱动
概述
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream中binder 对象交互。 通过我们配置来binding(绑定) ,而 Spring Cloud Stream 的 binder 对象负责与消息中间件交互。所以,只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。就像 jdbc 可以操作 mysql、oracle、sqlserver等数据库,就是对各类 MQ 操作的封装。目前仅支持RabbitMQ、Kafka
。Stream中的消息通信方式遵循了发布-订阅模式
。
如果用了两个消息队列的其中一种,因为后面的业务需求,需要往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的, 一大堆东西都要重新推倒重做,因为它跟系统耦合了,这时候 springcloud Stream 就提供了一种解耦合的方式。
==stream凭什么可以统一底层差异?==
通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离。
在没有绑定器这个概念的情况下,SpringBoot 应用要直接与消息中间件进行信息交互的时候, 由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,实现了应用程序与消息中间件细节之间的隔离。 通过向应用程序暴露统一的 Channel 通道,使应用程序不需要再考虑各种不同的消息中间件实现。
==应用模型==
Middleware:各类消息中间件
Binder
用来与中间件连接,不同的 Binder 对应不同的中间件,比如Kafka的实现KafkaMessageChannelBinder,RabbitMQ的实现RabbitMessageChannelBinder。Binder 可以生成 Binding,Binding 用来绑定消息容器的生产者和消费者,它有两种类型,INPUT 和 OUTPUT,INPUT 对应于消费者,OUTPUT 对应于生产者。
Application Core:生产者、消费者
inputs:消息输入通道
outputs:消息输出通道
生产者
新建 cloud-stream-rabbitmq-provider8801
POM
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<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
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
29server:
port: 8801
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: 120.48.54.126
port: 5672
username: admin
password: admin
cloud:
stream:
binders:
defaultRabbit:
type: rabbit
bindings:
output:
destination: studyExchange # 表示要使用的 Exchange 名称定义
content-type: application/json # 设置消息类型
binder: defaultRabbit
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是 30 秒)
lease-expiration-duration-in-seconds: 5 # 超时时间(默认是 90 秒),超时未收到心跳则移除instance
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为 IP 地址主启动类
1
2
3
4
5
6
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}Service 接口
1
2
3public interface IMessageProvider {
String send();
}接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.cuc.springcloud.service.impl;
import com.cuc.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
public class MessageProviderImpl implements IMessageProvider {
private MessageChannel output; // 消息发送管道
public String send() {
String serial = UUID.randomUUID().toString();
this.output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
return serial;
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class SendMessageController {
private IMessageProvider messageProvider;
public String sendMessage(){
return messageProvider.send();
}
}测试
启动 MQ、7001、8801,访问 http://localhost:8801/sendMessage
消费者
新建 cloud-stream-rabbitmq-consumer8802
POM
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<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
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
29server:
port: 8802
spring:
application:
name: cloud-stream-consumer
rabbitmq:
host: 120.48.54.126
port: 5672
username: admin
password: admin
cloud:
stream:
binders:
defaultRabbit:
type: rabbit
bindings:
input:
destination: studyExchange # 表示要使用的 Exchange 名称定义
content-type: application/json # 设置消息类型
binder: defaultRabbit
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是 30 秒)
lease-expiration-duration-in-seconds: 5 # 超时时间(默认是 90 秒),超时未收到心跳则移除instance
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为 IP 地址主启动类
1
2
3
4
5
6
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}业务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.cuc.springcloud.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
public class ReceiveMessageListener {
private String serverPort;
public void input(Message<String> message){
System.out.println(serverPort+"接受到的消息:"+message.getPayload());
}
}测试
分组与持久化
依照8802,clone出来一份运行8803
新建 cloud-stream-rabbitmq-consumer8803
代码同 8802,改下端口号啥的
测试
问题
- 重复消费
- 消息持久化问题(看下面持久化就懂了)
分组
微服务应用放置于同一个 group 中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
修改YML,将 8802、8803 分在不同组
下图为修改位置:
重启项目
可以看到不同组分在不同队列,故 studyExchange 交换机会将消息发送到这两个队列上,从而导致重复消费,默认应该就是分在不同组的。
修改YML,将 8802、8803 分在同一组
测试发现 8002、8003 会
轮询
消费队列上的消息,并没有重复消费。
持久化
- 停止 8802、8803 并去除掉 8802 的分组配置
- 8801 先发送4条消息到 rabbitmq
- 先启动 8802(无分组属性配置),后台没有打印消息
- 再启动 8803(有分组属性配置),后台打出来了MQ上的消息
综上,想要持久化配置分组即可。
Sleuth
分布式请求链路跟踪
概述
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
Spring Cloud Sleuth 提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了 Zipkin。
搭建链路监控
Zipkin
下载 zipkin
运行
1
java -jar zipkin-server-2.23.18-exec.jar
服务提供者,修改
cloud-provider-payment8001
添加依赖
1
2
3
4
5<!-- 包含了 sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
# 采样率值介于 0 到 1 之间,1 表示全部采集
probability: 1添加 Controller
1
2
3
4
public String paymentZipkin(){
return "PaymentZipkin server fall back";
}
服务消费者,修改
cloud-consumer-order80
添加依赖(同上)
添加配置(同上)
添加 Controller
1
2
3
4
5
public String paymentZipkin(){
String result = restTemplate.getForObject("http://localhost:8001/payment/zipkin/", String.class);
return result;
}
测试
启动 7001、8001、80,多次访问 http://localhost/consumer/payment/zipkin
可进一步查看详情。。
Alibaba
为什么出现 Spring Cloud Alibaba?
Spring Cloud Netflix 项目进入维护模式,这意味着将模块置于维护模式, Spring Cloud 团队将不会再向模块添加新功能。
以下均是 Spring Cloud Alibaba 内容。
Nacos
概述
Nacos就是注册中心
+ 配置中心
的组合。一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos = Eureka+Config +Bus
,Nacos 支持AP
和CP
模式的切换。
安装
基于 Mac
下载
解压
1
tar -zxvf 压缩包名称
bin 目录下,启动
单机启动命令:
1
sh startup.sh -m standalone
访问
http://127.0.0.1:8848/nacos ,账号/密码:nacos
bin 目录下,停止
1
sh shutdown.sh
注册中心
服务提供者
新建 cloudalibaba-provider-payment9001
父POM
之前应该导过了。
1
2
3
4
5
6
7
8<!--springcloudalibaba2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>本模块 POM
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<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置 Nacos 地址
management:
endpoints:
web:
exposure:
include: "*"主启动类
1
2
3
4
5
6
7
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
public class PaymentController {
private String serverPort;
public String getPayment({ Integer id)
return "nacos registry,serverPort:"+serverPort+"\tid"+id;
}
}测试
启动 9001,访问 http://localhost:9001/payment/nacos/22 进行测试,同时:
此时 注册中心 和 服务提供者 都已经完成了。
为了下一章节演示 nacos 的负载均衡,参照9001新建9002(略)
服务消费者
新建 cloudalibaba-consumer-nacos-order83
POM
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<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 消费者将要去访问的微服务名称
service-url:
nacos-user-service: http://nacos-payment-provider主启动类
1
2
3
4
5
6
7
8
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class,args);
}
}配置类
1
2
3
4
5
6
7
8
9
public class ApplicationContextBean {
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderNacosController {
private RestTemplate restTemplate;
private String serverURL;
public String paymentInfo({ Integer id)
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}测试
启动 nacos、9001、9002、83,多次访问 http://localhost:83/consumer/payment/nacos/66 ,发现轮询9001和9002,故负载均衡👌。
为什么 nacos 支持负载均衡?
配置中心
基础配置
新建 cloudalibaba-config-nacos-client3377
POM
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<dependencies>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1)bootstrap.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# nacos 配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务注册中心地址
config:
server-addr: localhost:8848 # Nacos 作为配置中心地址
file-extension: yaml # 指定 yaml 格式的配置(仅支持 yaml和 properties)
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml 后缀不能是 yml2)application.yml
1
2
3spring:
profiles:
active: dev # 表示开发环境Nacos 同 springcloud-config 一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
主启动类
1
2
3
4
5
6
7
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
// 使当前类下的配置支持 Nacos 的动态刷新功能。
public class ConfigClientController {
private String configInfo;
public String getConfigInfo(){
return configInfo;
}
}在 Nacos 中添加配置信息
测试
启动 3377,自带动态刷新,修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新。
分类配置
问题:
- 实际开发中,通常一个系统会准备 dev开发环境、test测试环境、prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件?
- 一个大型分布式微服务系统会有很多微服务子项目, 每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境,那怎么对这些微服务配置进行管理?
==Nacos的图形化管理界面==
默认情况: Namespace=public,Group=DEFAULT_GROUP,默认 Cluster 是 DEFAULT
- Namespace 主要用来实现隔离。 比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个 Namespace,不同的Namespace 之间是隔离的。
- Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去
- Service 就是微服务。一个 Service 可以包含多个 Cluster(集群),Nacos 默认 Cluster 是 DEFAULT,Cluster 是对指定微服务的一个虚拟划分。
- Instance,就是微服务的实例。
==三种方案加载配置==
DataID
(Nacos上 具体配置文件)通过在 YML 配置指定配置文件的 DataID 来使不同环境下读取不同的配置。
Group
Namespace
1)新增命名空间
2)回到服务列表查看
3)YML 配置
集群和持久化
Nacos 默认使用嵌入式数据库 derby 实现数据的存储。所以,如果启动多个默认配置下的 Nacos 节点,数据存储是存在一致性问题。 为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持 MySQL 的存储。
==支持三种部署模式:==
- 单机模式 - 用于测试和单机试用。
- 集群模式 - 用于生产环境,确保高可用。
- 多集群模式 - 用于多数据中心场景。
==环境准备==
- 安装好 JDK,需要 1.8 及其以上版本
- 建议: 2核 CPU / 4G 内存 及其以上
- 建议: 生产环境 3 个节点 及其以上
==集群部署==
Linux 下 Nacos + MySQL 配置。部署手册
要求:
1个Nginx + 3个nacos注册中心 + 1个mysql
安装 nginx、nacos、mysql
略
在 nacos/conf 的 application.properties 文件添加如下内容:
1
2
3
4
5spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root在 nacos/conf 下配置 cluster.conf
1
cp cluster.conf.example cluster.conf
cluster.conf 内容如下:
1
2
3120.48.54.126:3333
120.48.54.126:4444
120.48.54.126:5555给三台 nacos 配置端口,取消默认的 8848。ip 不能写 localhost 或 127.0.0.1。
mysql 建表
1)本地 Navicat 连接 Linux 下的 mysql,自行百度
2)创建
nacos_config
数据库(urf8)3)在数据库运行 nacos/conf 目录下的
nacos-mysql.sql
脚本编辑 nacos/bin 目录下的启动脚本
startup.sh
,使它能够接受不同的启动端口添加
o:
,其它字母也行,只要不跟前面的重复:之后可通过一下命令启动指定 nacos:
1
startup.sh -p 端口号
配置 Nginx,由它作为负载均衡器
修改
nginx.conf
:测试
访问 http://120.48.54.126:1111/nacos ,访问报错😓。等哪天Vmware适配了m1,在虚拟机上再试试,也可以试试其它搭建方法。
==集群和持久化很重要==
Sentinel
实现熔断与限流。等同于 Hystrix
安装
下载
启动
1
java -jar sentinel-dashboard-1.8.5.jar
演示工程
新建 cloudalibaba-sentinel-service8401
POM
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<dependencies>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel-datasource-nacos 后续做持久化用到 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
# 默认 8719 端口,假如被占用会自动从 8719 开始依次 +1 扫描 , 直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: "*"主启动类
1
2
3
4
5
6
7
8
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
public class FlowLimitController {
public String testA(){
return "TestA";
}
public String testB(){
return "TestB";
}
}测试
启动 nacos、sentinel、8401,这时候查看sentinel控制台还是空的,因为sentinel采用的懒加载。
先访问一下路由:http://localhost:8401/testA。再访问sentinel控制台:
此时 sentinel8080 正在监控微服务8401。
流控规则
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel 可以针对调用者进行限流,填写微服务名,默认 default(不区分来源)
- 阈值类型/单机阈值
- QPS(每秒钟请求数量):当调用该api的QPS达到阈值时,进行限流
- 线程数:当调用该api的线程数达到阈值的时,进行限流
- 是否集群
- 流控模式:
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,进行限流)【api级别的针对来源】
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据 codeFactor(冷加载因子,默认 3)的值,从阈值 /codeFactor,经过预热时长,才达到预设的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
==流控模式==
直接+快速失败
浏览器在一秒内超过一次
访问 http://localhost:8401/testA ,会抛异常:
关联+直接失败
当与A关联的资源B达到阀值后,限流A
。postman 模拟并发密集访问testB(不然手动速度没那么快):
新建多线程集合组
将访问地址添加进线程组
Run
链路+直接失败
例如有两条请求链路:
1 | /test1/common |
如果只希望统计从 /test2 进入到 /common 的请求,对 /test2 进行限流,则可以这样配置:
自行测试。
==流控效果==
直接失败
Blocked by Sentinel (flow limiting)
Warm Up
QPS一开始为阈值除以coldFactor(默认值为3),一旦触发,经过预热时长后才会达到设定阈值。
上图为例,一开始 QPS=10/3=3,也就是每秒请求数最多3个,超过则限流,但是与此同时,在之后的 5 秒内,QPS 会慢慢上升直到 10。
应用场景
秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。
排队等待
匀速排队,阈值必须设置为QPS。
以下图为例,当QPS为2时,每隔500ms才允许通过下一个请求。其它未通过请求则进行等待。最多等待20秒。
每0.3秒访问一次,一秒请求数超过了2次,因为请求会进行等待,所以不会报错。
可以看到10个请求都成功。
熔断规则
除了流量控制
以外,对调用链路中不稳定的资源进行熔断降级
也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时、异常比例升高、异常数堆积)对这个资源的调用进行限制,让请求快速失败从而避免影响到其它的资源而导致级联
错误。当资源被降级后,在接下来的降级时间窗口之内会对该资源的调用自动熔断(默认行为是抛出 DegradeException
)。
==慢调用比例==
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT
(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长
(statIntervalMs)内请求数目大于设置的最小请求数目
,并且慢调用的比例大于阈值
,则接下来的熔断时长
内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复
状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
Sentinel默认统计的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置
==异常比例==
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当单位统计时长
(statIntervalMs)内请求数目大于设置的最小请求数目
,并且异常的比例大于阈值
,则接下来的熔断时长
内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
==异常数==
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当单位统计时长
内的异常数目超过阈值
之后会自动进行熔断。经过熔断时长
后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
热点规则
热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作 。
1 |
|
上面的抓图代表第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用 dealHandler_testHotKey 支持方法。
试着去访问:
1 | http://localhost:8401/testHotKey?p1=aaa |
==参数例外项==
期望p1参数当它是某个特殊值时,它的限流值和平时不一样,当p1的值等于5时,它的阈值可以达到200。
系统规则
系统规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均RT、入口QPS 和并发线程数几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用系统维度的,而不是资源维度,并且仅对入口流量生效
。入口流量指的是进入应用的流量,比如 WEB 服务或 Dubbo 服务端接受的请求,都属于入口流量。
- Load 自适应(仅对Linux/Unix-like 机器生效):当系统的 load 作为启发指标,进行自适应系统保护。当系统 load 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
@SentinelResource
按资源名称限流
8401 新增 Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
public class RateLimitController {
public CommonResult byResource(){
return new CommonResult(200,"按资源名称限流测试 OK",new Payment(2022L,"serial001"));
}
public CommonResult handleException(BlockException exception){
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t服务不可用");
}
}添加流控规则
测试
每秒超过1次访问:
按URL地址限流
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
Controller
1
2
3
4
5
public CommonResult byUrl(){
return new CommonResult(200,"按 url 限流测试 OK");
}配置流控规则
测试
每秒超过一次访问:
面临的问题
- 系统默认的,没有体现自己的业务要求
- 依照现有条件,自定义的处理方法又和业务代码耦合在一块
- 每个业务方法都添加一个兜底的,那代码膨胀加剧
- 全局统一的处理方法没有体现
自定义限流处理逻辑
自定义限流处理类
1
2
3
4
5
6
7
8
9
10
11package com.cuc.springcloud.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.cuc.springcloud.entities.CommonResult;
public class CustomerBlockHandler {
public static CommonResult handleException2(BlockException exception){
return new CommonResult(2022,"定义的限流处理");
}
}Controller
1
2
3
4
5
6
7
8// 自定义处理逻辑
public CommonResult customerBlockHandler(){
return new CommonResult(200,"客户自定义限流处理逻辑");
}自行测试
属性说明
注意:注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
注解包含以下属性:
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为EntryType.OUT
)blockHandler
/blockHandlerClass
:blockHandler
对应处理BlockException
的函数名称,可选项。blockHandler 函数访问范围需要是public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为BlockException
。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定blockHandlerClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。fallback
/fallbackClass
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
服务熔断功能
sentinel 整合 ribbon+openFeign
服务提供者
新建 cloudalibaba-provider-payment9003
POM
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<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"主启动类
1
2
3
4
5
6
7
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class,args);
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PaymentController {
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
}
public CommonResult<Payment> paymentSQL({ Long id)
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort:" + serverPort, payment);
return result;
}
}同理再新建 cloudalibaba-provider-payment9004
略
服务消费者
新建 cloudalibaba-consumer-nacos-order84
POM
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<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.cuc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719主启动类
1
2
3
4
5
6
7
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class,args);
}
}配置类
1
2
3
4
5
6
7
8
9
public class ApplicationContextConfig {
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CircleBreakerController {
private static final String SERVICE_URL ="http://nacos-payment-provider";
private RestTemplate restTemplate;
public CommonResult<Payment> fallback({ Long id)
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
if (id==4){
throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常 ....");
}else if (result.getData()==null){
throw new NullPointerException("NullPointerException, 该 ID 没有对应记录 , 空指针异常");
}
return result;
}
}测试
Ribbon系列
修改 Controller
测试
再次修改Controller
此时
fallback
与blockHandler
同时存在配置流控规则
测试
访问 http://localhost:84/consumer/fallback/4 ,刚开始走抛异常逻辑,若每秒请求超过一次,则走限流逻辑。
继续修改
测试
Feign系列
添加依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>YML添加配置
1
2
3
4# 激活 Sentinel 对 Feign 的支持
feign:
sentinel:
enabled: trueService 接口
1
2
3
4
5
6
public interface PaymentService {
public CommonResult<Payment> paymentSQL(; Long id)
}接口实现
1
2
3
4
5
6
7
public class PaymentFallbackService implements PaymentService{
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult(444,"服务降级返回",new Payment(id,null));
}
}Controller
1
2
3
4
5
6
7
private PaymentService paymentService;
public CommonResult<Payment> paymentSQL({ Long id)
return paymentService.paymentSQL(id);
}主启动类添加注解
@EnableFeignClients
测试
启动84,启动后将所有服务提供者都关掉,之后访问 http://localhost:84/consumer/openfeign/1
规则持久化
一旦重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
修改 cloudalibaba-sentinel-service8401
添加依赖(之前就有了)
1
2
3
4<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>YML 添加配置
1
2
3
4
5
6
7
8
9
10
11spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
data-id: cloudalibaba-sentinel-service
group-id: DEFAULT_GROUP
data-type: json
rule-type: flowNacos 新增配置
1
2
3
4
5
6
7resource:资源名称;
limitApp:来源应用;
grade:阈值类型,0表示线程数,1表示QPS;
count:单机阈值;
strategy:流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
clusterMode:是否集群。启动8401刷新sentinel(如果没有,先快速访问被限流的地址,直到限流)
停止8401,sentinel上的规则消失
重启8401,规则还是没有,快速访问8401直到限流,刷新sentinel,规则又出现了。(sentinel懒加载)
Seata
分布式问题
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局数据的一致性没法保证。
概述
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
- XID:全局唯一的事务ID
- TC:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
- TM:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
- RM:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
==处理过程==
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
==怎么玩?==
- 本地 @Transactional
- 全局 @GlobalTransactional
安装
下载(本次版本0.9.0)
解压并修改conf目录下的
file.conf
配置文件主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息
1)service模块
2)store模块
mysql5.7 数据库新建库 seata
seata 库执行 conf 目录的 db_store.sql 脚本
修改 conf 目录下的 registry.conf
先启动Nacos,再启动 seata-server
bin 目录下执行:
1
sh seata-server.sh
数据库准备
这里会创建三个服务,订单服务、库存服务、账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
创建数据库
1
2
3seata_order
seata_storage
seata_account建表
1
2
3
4
5
6
7
8
9USE seata_order;
CREATE TABLE t_order(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
count INT(11) DEFAULT NULL COMMENT '数量',
money DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
status INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;1
2
3
4
5
6
7
8
9USE seata_storage;
CREATE TABLE t_storage(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
product_id BIGINT(11) DEFAULT NULL COMMENT '产品id',
total INT(11) DEFAULT NULL COMMENT '总库存',
used INT(11) DEFAULT NULL COMMENT '已用库存',
residue INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);1
2
3
4
5
6
7
8
9USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);按照上述3库分别建回滚日志表
conf 目录的 db_undo_log.sql 脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
微服务准备
订单模块
新建 seata-order-service2001
POM
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<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>seata-all</groupId>
<artifactId>io.seata</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
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
27server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组名称需要与 seata-server 中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下
Domain
1)CommonResult
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message){
this(code,message,null);
}
}2)Order
1
2
3
4
5
6
7
8
9
10
11
12
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status;
}OrderDao 接口
1
2
3
4
5
6
7
8
public interface OrderDao {
// 创建订单
void create(Order order);
// 修改订单状态
void update(; Long userId, Integer status)
}OrderMapper.xml
resources/mapper 目录下
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
<mapper namespace ="com.cuc.springcloud.dao.OrderDao" >
<resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="count" property="count" jdbcType="INTEGER" />
<result column="money" property="money" jdbcType="DECIMAL" />
<result column="status" property="status" jdbcType="INTEGER" />
</resultMap>
<insert id="create">
INSERT INTO `t_order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
</insert>
<update id="update">
UPDATE `t_order`
SET status = 1
WHERE user_id = #{userId} AND status = #{status};
</update>
</mapper>Service
1)OrderService
1
2
3
4public interface OrderService {
// 创建订单
void create(Order order);
}2)StorageService
1
2
3
4
5
6
public interface StorageService {
// 减库存
CommonResult decrease(; Long productId, Integer count)
}3)AccountService
1
2
3
4
5
6
public interface AccountService {
// 扣减账户余额
CommonResult decrease(; Long userId, BigDecimal money)
}OrderService 实现类
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
public class OrderServiceImpl implements OrderService {
private OrderDao orderDao;
private StorageService storageService;
private AccountService accountService;
//第二个参数表示发生任何异常都进行回滚
public void create(Order order) {
log.info("-------> 下单开始");
// 创建订单
orderDao.create(order);
// 远程调用库存服务扣减库存
log.info("------->order-service 中扣减库存开始");
storageService.decrease(order.getProductId(),order.getCount());
log.info("------->order-service 中扣减库存结束");
// 远程调用账户服务扣减余额
log.info("------->order-service 中扣减余额开始");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("------->order-service 中扣减余额结束");
// 修改订单状态为已完成
log.info("------->order-service 中修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("------->order-service 中修改订单状态结束");
log.info( "-------> 下单结束");
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
public class OrderController {
private OrderService orderService;
public CommonResult create(Order order){
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}配置类
1)MyBatisConfig
1
2
3
4
public class MyBatisConfig {
}2)DataSourceProxyConfig
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
45package com.cuc.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 使用 Seata 对数据源进行代理
*/
public class DataSourceProxyConfig {
private String mapperLocations;
public DataSource druidDataSource(){
return new DruidDataSource();
}
public DataSourceProxy dataSourceProxy(DataSource dataSource){
return new DataSourceProxy(dataSource);
}
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}主启动类
1
2
3
4
5
6
7
8
// 取消数据源的自动创建
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class,args);
}
}
库存模块
新建 seata-storage-service2002
POM
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<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>seata-all</groupId>
<artifactId>io.seata</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下
Domain
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message){
this(code,message,null);
}
}1
2
3
4
5
6
7
8
9
10
public class Storage {
private Long id;
private Long productId; // 产品ID
private Integer total; // 总库存
private Integer used; // 已用库存
private Integer residue; // 剩余库存
}Dao接口及实现
1
2
3
4
public interface StorageDao {
void decrease("count" ) Integer count); Long productId, (
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<mapper namespace ="com.cuc.springcloud.dao.StorageDao" >
<resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="INTEGER" />
<result column="used" property="used" jdbcType="INTEGER" />
<result column="residue" property="residue" jdbcType="INTEGER" />
</resultMap>
<update id="decrease">
UPDATE t_storage
SET used = used + #{count}, residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>Service接口及实现
1
2
3public interface StorageService {
void decrease(Long productId, Integer count);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.cuc.springcloud.service.impl;
import com.cuc.springcloud.dao.StorageDao;
import com.cuc.springcloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
public class StorageServiceImpl implements StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
private StorageDao storageDao;
public void decrease(Long productId, Integer count) {
LOGGER.info( "------->storage-service 中扣减库存开始 " );
storageDao.decrease(productId,count);
LOGGER.info( "------->storage-service 中扣减库存结束 " );
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
public class StorageController {
private StorageService storageService ;
public CommonResult decrease(Long productId,Integer count){
storageService.decrease(productId, count);
return new CommonResult(200," 扣减库存成功!");
}
}Config
1
2
3
4
public class MyBatisConfig {
}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
45package com.cuc.springcloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 使用 Seata 对数据源进行代理
*/
public class DataSourceProxyConfig {
private String mapperLocations;
public DataSource druidDataSource(){
return new DruidDataSource();
}
public DataSourceProxy dataSourceProxy(DataSource dataSource){
return new DataSourceProxy(dataSource);
}
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}主启动
1
2
3
4
5
6
7
8
public class SeataStorageServiceApplication2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageServiceApplication2002.class,args);
}
}
账户模块
新建 seata-account-service2003
POM
同上
YML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下
Domain
CommonResult 同上
1
2
3
4
5
6
7
8
9
10
11
public class Account {
private Long id;
private Long userId; // 用户id
private BigDecimal total; // 总额度
private BigDecimal used; // 已用额度
private BigDecimal residue; // 剩余额度
}Dao接口及实现
1
2
3
4
public interface AccountDao {
void decrease("money" ) BigDecimal money); Long userId, (
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<mapper namespace ="com.cuc.springcloud.dao.AccountDao" >
<resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Account">
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="DECIMAL" />
<result column="used" property="used" jdbcType="DECIMAL" />
<result column="residue" property="residue" jdbcType="DECIMAL" />
</resultMap>
<update id="decrease">
UPDATE t_account
SET residue = residue - #{money}, used = used - #{money}
WHERE user_id = #{userId}
</update>
</mapper>Service接口及实现
1
2
3public interface AccountService {
void decrease("money" ) BigDecimal money); Long userId, (
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountServiceImpl implements AccountService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
private AccountDao accountDao;
public void decrease(Long userId, BigDecimal money) {
LOGGER .info( "------->account-service 中扣减账户余额开始 " );
accountDao .decrease(userId,money);
LOGGER .info( "------->account-service 中扣减账户余额结束 " );
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
public class AccountController {
private AccountService accountService;
public CommonResult decrease("money" ) BigDecimal money){ Long userId, (
accountService.decrease(userId, money);
return new CommonResult(200,"扣减账户余额成功!");
}
}Config
同上
主启动
1
2
3
4
5
6
7
8
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class,args);
}
}
测试
启动 nacos、seata、2001、2002、2003,访问 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
之后可去数据库查看相应数据。
修改 2003,添加如下内容:
重启2003,再次访问:
提示超时错误,查看数据库,会发现错误之前的操作回滚了,数据库的数据并没有被修改。
也就是说注解了@GlobalTransactional,方法里头的任意微服务调用出错,会将方法中所有数据库操作进行回滚。