参考网站:
CSDN:https://blog.csdn.net/u011863024/article/details/114298282
哔哩哔哩:https://www.bilibili.com/video/BV18E411x7eT?p=28
SpringCloud基本知识
- SpringCloud基于HTTP协议,和Dubbo最本质的区别(dubbo的核心是基于RPC)
- 注册中心:Eureka
- 客户端负载均衡:Ribbon
- 声明式远程方法调用:Feign
- 服务降级,熔断:Hystrix
- 网关:Zuul
SpringBoot和SpringCloud关系
- SpringBoot是基础
- SpringCloud要基于SpringBoot开发
SpringCloud和Dubbo对比
-
核心:
-
- Dubbo 底层基于RPC
- SpringCloud底层基于RestFul,也可以说是基于Http。
-
其他区别:
-
- SpringCloud相对于Dubbo功能更加全面
- SpringCloud是一个一站式的解决方案
- SpringCloud能够天然的基于Spring全家桶开发
SpringCloud目标基础测试环境
一,创建父工程
- SpringCloud
- 父工程很重要,里面定义了boot 和 cloud的版本信息
- 配置依赖管理:
<!-- 统一管理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>8.0.23</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.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>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.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>
<version>2.3.5.RELEASE</version>
</plugin>
</plugins>
</build>
二,服务提供者
- 构建Module:cloud-provider-payment8001
- 添加pom依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 写yml配置文件
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
- 主启动
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
- 数据库
CREATE 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
- 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment {
private long id;
private String serial;
}
@Data
@AllArgsConstructor
@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
@Mapper
public interface PaymentMapper {
int create(Payment payment);
Payment getPaymentById(Long id);
}
- service
@Service
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService{
@Resource
private PaymentMapper paymentMapper;
@Override
public int create(Payment payment) {
return paymentMapper.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentMapper.getPaymentById(id);
}
}
- controller
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping("/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
if (result>0){
return new CommonResult(200,"插入成功",result);
}else {
return new CommonResult(400,"插入失败",null);
}
}
@GetMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
Payment payment = paymentService.getPaymentById(id);
if (payment != null){
return new CommonResult(200,"查询成功",payment);
}else {
return new CommonResult(400,"查询失败",null);
}
}
}
- 测试
get
create
三,热部署
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- 添加插件到父工程
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.5.RELEASE</version>
</plugin>
</plugins>
</build>
- 开启自动编译选项
- 跟新
ctrl+shift+alt+/
- 重启IDEA
四,服务消费者
- 构建Module:cloud-consumer-order80
- 添加pom依赖:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 写yml
server:
port: 80
- 主启动
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
- 把服务提供者的entity复制过来
- 首说:RestTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
官网地址:
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
使用:
使用restTemplate访问restful接口非常的简单粗暴无脑。(url, requestMap,ResponseBean.class)这三个参数分别代表REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
- config配置类
@Configuration
public class ApplicationConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
- 编写Controller
@RestController
@RequestMapping("/consumer")
public class PaymentController {
@Resource
private RestTemplate restTemplate;
private static final String PAYMENT_URL="http://localhost:8001";
@RequestMapping("/create")
public CommonResult create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
}
@RequestMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
}
- 测试
- 真的插入成功了吗?我们看一下数据库
解决方法:在服务提供者的controller方法上加上@RequestBody注解
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping("/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
if (result>0){
return new CommonResult(200,"插入成功",result);
}else {
return new CommonResult(400,"插入失败",null);
}
}
@GetMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
Payment payment = paymentService.getPaymentById(id);
if (payment != null){
return new CommonResult(200,"查询成功",payment);
}else {
return new CommonResult(400,"查询失败",null);
}
}
}
五,没有:RunDashboard
新版本2019.3里面的名字改成了Services,不是原来的Dashboard。
打开方式:
六,工程重构
系统中有重复部分,在每个工程中都含有entity造成了代码冗余
解决方法:新建cloud-commons-api模块,把重复代码进行整合
- 创建cloud-commons-api模块
- 添加pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
- 复制entity
- 使用 maven命令clean install
- 订单80和支付8001分别改造
- 删除各自的原先有过的entities文件夹
- 各自黏贴POM内容
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 项目结构
Eureka服务注册与发现
一,Eureka基础知识
1,什么是服务治理
Spring Cloud封装了Netflix公司开发的Eureka模块来实现服务治理
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
2,什么是服务注册
Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到FEureka Seve并维持心跳连接。这样系统的维护人员就可以通Eureka Server 来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址)
3,Eureka两组件
Eureka包含两个组件: Eureka Server和Eureka Client
Eureka Server
提供服务注册服务
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
EurekaClient
通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
二,单机Eureka构建步骤
1,构建服务注册中心
IDEA生成eurekaServer端服务注册中心类似物业公司
- 建Module:cloud-eureka-server7001
- 添加pom依赖
<dependencies>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<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>
- 写yml
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名字
client:
register-with-eureka: false #表识不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
- 主启动
@EnableEurekaServer
@SpringBootApplication
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class,args);
}
}
- 测试
- 访问:http://localhost:7001/
- 结果页面
2,构建服务提供者
EurekaClient端cloud-provider-payment8001将注册进EurekaServer成为服务提供者provider.
- 改model:cloud-provider-payment8001
- 改pom:添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 改yml:添加
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
- 改主启动:添加@EnableEurekaClient
@EnableEurekaClient
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
- 测试
- 先要启动EurekaServer,在启动本项目
- http://localhost:7001/
- 微服务注册名配置说明
7. 自我保护机制
EurekaClient端cloud-consumer-order80将注册进EurekaServer成为服务消费者consumer.
- 修改model:cloud-consumer-order80
- 改pom:添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 改yml
spring:
application:
name: cloud-consumer-order
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
- 改主启动:添加@EnableEurekaClient
@EnableEurekaClient
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
- 测试
- 先要启动EurekaServer,在启动本项目
- http://localhost:7001/
三,集群Eureka构建步骤
Eureka集群原理说明
问题:微服务RPC远程服务调用最核心的是什么
高可用,试想你的注册中心只有一个only one,它出故障了那就呵呵(―v一)"了,会导致整个为服务环境不可用,所以解决办法:搭建Eureka注册中心集群,实现负载均衡+故障容错
相互观望,相互注册.
EurekaServer集群环境构建步骤
1,构建服务注册中心集群
- 参考cloud-eureka-server7001,新建cloud-eureka-server7002.
- 修改pom:
<dependencies>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<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>
- 修改映射配置
- 找到C:\Windows\System32\drivers\etc路径下的hosts文件,
- 添加端口号
- 127.0.0.1 eureka7001.com
- 127.0.0.1 eureka7002.com
- 改yml
相互观望,相互注册.
7001
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://eureka7001.com:7002/eureka/
7002
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名字
client:
register-with-eureka: false #表识不向注册中心注册自己
fetch-registry: false #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/ #设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
- 主启动(复制cloud-eureka-server7001的主启动类到7002即可)
- 测试:
- http://eureka7001.com:7001/
- http://eureka7002.com:7002/
- 将订单服务80微服务发布到上面2台Eureka集群配置中
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
- 测试01
- 先要启动EurekaServer,7001/7002服务
- 再要启动服务提供者provider,8001服务
- 再要启动消费者,80
- http://localhost/consumer/get/1
2,构建服务提供者集群
支付服务提供者8001集群环境构建
- 参考cloud-provider-payment8001,新建cloud-provider-payment8002.
- 改POM
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 写YML
8001
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
8002
server:
port: 8002
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
- 修改8001/8002的Controller
- 主启动(复制cloud-provider-payment8001的主启动类到7002即可)
- 订单服务访问地址不能写死
- 使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
@Configuration
public class ApplicationConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
- 测试01
- 先要启动EurekaServer,7001/7002服务
- 再要启动服务提供者provider,8001服务
- 再要启动消费者,80
- http://localhost/consumer/get/1
3,actuator微服务信息完善
主机名称:服务名称修改
- 修改cloud-provider-payment8002:YML
instance:
instance-id: payment8001
server:
port: 8002
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: payment8002
prefer-ip-address: true
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
- 访问:http://localhost:7001/
- 效果:
访问信息有ip信息提示
- 修改cloud-provider-payment8001:YML
prefer-ip-address: true
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: payment8001
prefer-ip-address: true
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
- 访问:http://localhost:7001/
- 效果:
健康检查: localhost:8001/actuator/health
4,服务发现Discovery
对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
- 修改cloud-provider-payment8002的Controller
@GetMapping("/discoveryClient")
public Object getDiscoveryClient(){
List<String> services = discoveryClient.getServices();
for (String service : services) {
System.out.println("services = " + services);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
System.out.println("instance.getInstanceId() = " + instance.getInstanceId());
System.out.println("instance.getUri() = " + instance.getUri());
System.out.println("instance.getPort() = " + instance.getPort());
}
return this.discoveryClient;
}
- 8002主启动类:@EnableDiscoveryClient
@EnableEurekaClient
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain8002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8002.class,args);
}
}
- 先要启动EurekaServer,7001/7002服务
- 再启动8001主启动类,需要稍等一会
- 访问:http://localhost:8001/payment/discovery
- 后台输出
5,Eureka自我保护
故障现象
- 概述
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
- 如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
EMERGENCY!EUREKAMAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.
RENEWALS ARE LESSER THANTHRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE
导致原因
- 为什么会产生Eureka自我保护机制?
为了防止EurekaClient可以正常运行,但是与EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
- 什么是自我保护模式?
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微务本身其实是健康的,此时本不应该注销这个微服务。Eureka过“自我保护模式”来解决这个问题——当EurekaServer节点在时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存,属于CAP里面的AP分支
怎么禁止自我保护(一般生产环境中不会禁止自我保护)
- 修改:注册中心eureakeServer端7001
- 出厂默认,自我保护机制是开启的
- 使用eureka.server.enable-self-preservation = false可以禁用自我保护模式
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000
- 关闭效果
生产者客户端eureakeClient端8001
- 修改:yml
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
#心跳检测与续约时间
#开发时没置小些,保证服务关闭后注册中心能即使剔除服务
instance:
instance-id: payment8001
prefer-ip-address: true
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
- 测试
- 7001和8001都配置完成
- 先启动7001再启动8001
- 先关闭8001
- 马上被删除了
Zookeeper服务注册与发现
Zookeeper安装
SpringCloud整合Zookeeper代替Eureka
1,注册中心Zookeeper
- zookeeper是一个分布式协调工具,可以实现注册中心功能
- 关闭Linux服务器防火墙后启动zookeeper服务器
- zookeeper服务器取代Eureka服务器,zk作为服务注册中心
2,服务提供者
新建cloud-provider-payment8004
pom
<dependencies>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zookeeper-discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 8004
#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.38.144:2181
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class,args);
}
}
controller
@Slf4j
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/zk")
public String paymentzk(){
return "springcloud with zookeeper:"+serverPort+"\t"+ UUID.randomUUID().toString();
}
}
3,服务消费者
新建cloud-consumerzk-order80
pom
<dependencies>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zookeeper-discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yaml
server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 192.168.38.144:2181
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class OrderZkMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderZkMain80.class,args);
}
}
config
ApplicationConfig
@Configuration
public class ApplicationConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller
@Slf4j
@RestController
public class OrderZkController {
public static final String INVOME_URL = "http://cloud-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/zk")
public String payment (){
String result = restTemplate.getForObject(INVOME_URL+"/payment/zk",String.class);
return result;
}
}
4,测试
启动zookeeper
启动8004
启动80
查看zookeeper
访问:http://localhost/consumer/payment/zk
Consul服务注册与发现
1,Consul简介
1,是什么
https://www.consul.io/intro/index.html
Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司用Go语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了—种完整的服务网格解决方案。
它具有很多优点。包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议支持跨数据中心的WAN集群提供图形界面跨平台,支持Linux、Mac、Windows
2,能干嘛
- 服务发现
- 提供HTTP和DNS两种发现方式
- 健康监测
- 支持多种协议,HTTP、TCP、Docker、Shell脚本定制化
- KV存储
- key , Value的存储方式
- 多数据中心
- Consul支持多数据中心
- 可视化Web界面
3,去哪下
https://www.consul.io/downloads.html
4,怎么玩
https://www.springcloud.cc/spring-cloud-consul.html
2,安装并运行Consul
下载完成后只有一个consul.exe文件,硬盘路径下双击运行,查看版本信息
使用开发模式启动
consul agent -dev
通过以下地址可以访问Consul的首页:http://localhost:8500
结果页面
3,服务提供者
新建cloud-providerconsul-payment8006
pom
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-consul-discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yaml
server:
port: 8006
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain8006 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class,args);
}
}
controller
@Slf4j
@RestController
public class PaymentConsulController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/consul")
public String paymentzk(){
return "springcloud with zookeeper:"+serverPort+"\t"+ UUID.randomUUID().toString();
}
}
验证测试
访问:http://localhost:8006/payment/consul
4,服务消费者
新建cloud-consumerconsul-order80
pom
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-consul-discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yaml
server:
port: 80
spring:
application:
name: consul-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class OrderConsulMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class,args);
}
}
config
@Configuration
public class ApplicationConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
cntroller
@Slf4j
@RestController
public class OrderConsulController {
public static final String INVOME_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/consul")
public String payment (){
String result = restTemplate.getForObject(INVOME_URL+"/payment/consul",String.class);
return result;
}
}
5,验证测试
访问: http://localhost:8500/ui/dc1/services
访问测试地址: localhost/consumer/payment/consul
6,三个注册中心异同点
1,CAP
- C:Consistency(强一致性)
- A:Availability(可用性)
- P:Partition tolerance(分区容错)
- CAP理论关注粒度是数据,而不是整体系统设计的策略
2,经典CAP图
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:
- CA-单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
- CP-满足一致性,分区容忍必的系统,通常性能不是特别高。
- AP–满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
3,AP(Eureka)
AP架构
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。结论:违背了一致性C的要求,只满足可用性和分区容错,即API
4,CP(Zookeeper/Consul)
CP架构
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性结论:违背了可用性A的要求,只满足一致性和分区容错,即CP
Ribbon负载均衡服务调用
一,概述
是什么
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出LoadBalancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
能干嘛模式:LB(负载均衡)
- LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。
- 集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx)由该设施负责把访问请求通过某种策略转发至服务的提供方;
- 进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
- Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
前面我们讲解过了80通过轮询负载访问8001/8002
一句话:负载均衡+RestTemplate调用
二,Ribbon负载均衡演示
架构说明
Ribbon在工作时分成两步
- 第一步先选择EurekaServer ,它优先选择在同一个区域内负载较少的server
- 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
- 其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
POM
spring-cloud-starter-netflix-eureka-client2.2.1自带了Ribbon
二说RestTemplate的使用
- 官网
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
- getForObject方法/getForEntity方法
//返回对象为响应体中数据转化成的对象,基本上可以理解为Json
@RequestMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
//返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
@RequestMapping("/getForEntity/{id}")
public CommonResult getForEntity(@PathVariable Long id) {
ResponseEntity<CommonResult> forEntity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
//响应状态码
if (forEntity.getStatusCode().is2xxSuccessful()){
//返回实体信息
return forEntity.getBody();
}else {
return new CommonResult(444,"操作失败");
}
}
三,Ribbon核心组件IRule
IRule:根据特定算法从服务列表中选取一个要访问的服务
对象 | 说明 |
---|---|
com.netflix.loadbalancer.RoundRobinRule | 轮询 |
com.netflix.loadbalancer.RandomRule | 随机 |
com.netflix.loadbalancer.RetryRule | 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试 |
WeightedResponseTimeRule | 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择 |
BestAvailableRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
AvailabilityFilteringRule | 先过滤掉故障实例,再选择并发较小的实例 |
ZoneAvoidanceRule | 默认规则,复合判断server所在区域的性能和server的可用性选择服务器 |
如何替换
- 修改cloud-consumer-order80
- 注意配置细节
官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
-
新建package:com.myrule
-
上面包下新建MySelfRule规则类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//定义为随机
}
}
- 主启动类添加@RibbonClient
@RibbonClient(value = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
@EnableEurekaClient
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
- 测试:http://localhost/consumer/get/1
四,Ribbon负载均衡算法
原理初探
负载均衡算法: rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE’);
如:
- List [0]instances = 127.0.0.1:8002
- List [1] instances = 127.0.0.1:8001
8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
- 当总请求数为1时:1 %2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
- 当总请求数位2时:2%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
- 当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
- 当总请求数位4时:4%2=0对应下标位置为0,则获得服务地址为127.0.0.1:8002
- 如此类推…
RoundRobinRule源码
/*
*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.loadbalancer;
import com.netflix.client.config.IClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The most well known and basic load balancing strategy, i.e. Round Robin Rule.
*
* @author stonse
* @author Nikos Michalakis <nikos@netflix.com>
*
*/
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
自定义负载均衡器
- 7001/7002集群启动
- 8001/8002微服务改造controller
@GetMapping(value = "/lb")
public String getPaymentLB(){
return serverPort;
}
- 80订单微服务改造
- ApplicationContextBean去掉@LoadBalanced
- 编写LoadBalanced接口
public interface LoadBalancer {
//收集服务器总共有多少台能够提供服务的机器,并放到list里面
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
- 编写LoadBalanced实现类:MyLB
@Component
public class MyLB implements LoadBalancer{
private AtomicInteger atomicInteger =new AtomicInteger(0);
//坐标
public Integer getAtomicInteger(){
int current;
int next;
do {
current = atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
//第一个参数是期望值,第二个参数是修改值是
}while (!this.atomicInteger.compareAndSet(current,next));
System.out.println(next);
return next;
}
@Override //得到机器的列表
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
//得到服务器的下标位置
int index = getAtomicInteger() % serviceInstances.size();
ServiceInstance serviceInstance = serviceInstances.get(index);
return serviceInstance;
}
}
- OrderController
@RestController
@RequestMapping("/consumer")
public class PaymentController {
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
private LoadBalancer lb = new MyLB();
//private static final String PAYMENT_URL="http://localhost:8001";
private static final String PAYMENT_URL="http://CLOUD-PAYMENT-SERVICE";
@RequestMapping("/create")
public CommonResult create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
}
@RequestMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
@GetMapping(value = "/lb")
public String getPaymentLB(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null){
return null;
}
ServiceInstance instances1 = lb.instances(instances);
URI uri = instances1.getUri();
System.out.println("uri = " + uri);
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
}
- 测试:http://localhost/consumer/lb
OpenFeign服务接口调用
一,概述
OpenFeign是什么
Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可
GitHub:https://github.com/spring-cloud/spring-cloud-openfeign
能干嘛
- Feign能干什么
- Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
- Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
Feign和OpenFeign两者区别
Feign | OpenFeign |
---|---|
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 I | OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 |
//feign
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
//openFeign
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
二,OpenFeign使用步骤
新建cloud-consumer-feign-order80
pom
<!--openfeign-->
<dependencies>
<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.spring.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
主启动类:添加@EnableFeignClients注解
@EnableFeignClients
@SpringBootApplication
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}
业务类:接口+注解@FeignClient("")
- 新建PaymentFeignService接口并新增注解@FeignClient
@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignSerivce {
@PostMapping("/payment/create")
public CommonResult create(@RequestBody Payment payment);
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
}
- 控制层Controller
@RestController
@RequestMapping("/consumer")
public class OrderController {
@Resource
private PaymentFeignSerivce paymentFeignSerivce;
@RequestMapping("/create")
public CommonResult create(Payment payment) {
return paymentFeignSerivce.create(payment);
}
@RequestMapping("/get/{id}")
public CommonResult getPaymentById(@PathVariable Long id) {
return paymentFeignSerivce.getPaymentById(id);
}
}
测试
- 先启动2个eureka集群7001/7002
- 再启动2个微服务8001/8002
- 启动OpenFeign启动
- http://localhost/consumer/get/31
- Feign自带负载均衡配置项
执行流程
三,OpenFeign超时控制
超时设置,故意设置超时演示出错情况
- 服务提供方8001故意写暂停程序
@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeout(){
try { TimeUnit.SECONDS.sleep(3); }catch (Exception e) {e.printStackTrace();}
return serverPort;
}
- 服务消费方80添加超时方法PaymentFeignService
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
- 服务消费方80添加超时方法OrderFeignController
@RequestMapping(value = "/feign/timeout")
public String paymentFeignTimeout(){
return paymentFeignSerivce.paymentFeignTimeout();
}
- 测试
- 访问:http://localhost/consumer/feign/timeout
- 错误页面
OpenFeign默认等待一秒钟,超过后报错
默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
YML文件里需要开启OpenFeign客户端超时控制
#设置feign客户端超时时间(openFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
#设置feign客户端超时时间(openFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
四,OpenFeign日志打印功能
日志打印功能是什么
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中 Http请求的细节。说白了就是对Feign接口的调用情况进行监控和输出
日志级别
值 | 描述 |
---|---|
NONE | 默认的,不显示任何日志; |
BASIC | 仅记录请求方法、URL、响应状态码及执行时间; |
HEADERS | 除了BASIC中定义的信息之外,还有请求和响应的头信息; |
FULL | 除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。 |
配置日志bean
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
YML文件里需要开启日志的Feign客户端
logging:
level:
#feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
logging:
level:
com.kun.service.PaymentFeignService: debug
后台日志查看
Hystrix断路器
一,概述
1,分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
1,服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
2,Hystrix是什么
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
3,能干嘛
服务降级:fallback
服务熔断:break
接近实时的监控:flowlimit
。。。。。。
4,官网资料
https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hystrix官宣,停更进维
https://github.com/Netflix/Hystrix
二,Hystrix重要概念
1,服务降级
- 服务器忙,请稍候再试,不让客户端等待并立刻返回一个友好提示,fallback
- 哪些情况会触发降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
2,服务熔断
- 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
- 就是保险丝:服务的降级->进而熔断->恢复调用链路
3,服务限流
- 秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
三,hystrix案例
1,构建正确提供者模块
- cloud-provider-hystrix-payment8001
- pom
<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.spring.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
- yml
server:
port: 8001
eureka:
client:
register-with-eureka: true #表识不向注册中心注册自己
fetch-registry: true #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
# defaultZone: http://eureka7002.com:7002/eureka/ #设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://localhost:7001/eureka
# server:
# enable-self-preservation: false
spring:
application:
name: cloud-provider-hystrix-payment
# eviction-interval-timer-in-ms: 2000
- 主启动
package com.kun;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
- 业务层
@Service
public class PaymentService {
//成功
public String paymentInfo_OK(Integer id){
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"哈哈哈" ;
}
//失败
public String paymentInfo_TimeOut(Integer id){
int timeNumber = 3;
try { TimeUnit.SECONDS.sleep(timeNumber); }catch (Exception e) {e.printStackTrace();}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id: "+id+"\t"+"呜呜呜"+" 耗时(秒)"+timeNumber;
}
}
- 视图层/控制层
@RestController
@RequestMapping("/payment")
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:"+result);
return result;
}
@GetMapping("/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*******result:"+result);
return result;
}
}
-
正常测试
-
启动eureka7001
-
启动cloud-provider-hystrix-payment8001
-
访问
-
http://localhost:8001/payment/hystrix/ok/31
-
每次调用耗费5秒钟:http://localhost:8001/payment/hystrix/timeout/31
-
-
上述module均OK:以上述为根基平台,从正确->错误->降级熔断->恢复
-
2,构建正确消费者模块
- cloud-consumer-feign-hystrix-order80
- pom
<dependencies>
<!--新增hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--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.spring.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
- yml
server:
port: 80
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
spring:
application:
name: cloud-provider-hystrix-payment
- 主启动
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
- 业务类
@Component
@FeignClient("CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@RequestMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@RequestMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
- 视图层/控制层
@RestController
@RequestMapping("/consumer")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@RequestMapping("/payment/hystrix/ok/{id}")
public String testHystrixOk(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_OK(id);
}
@RequestMapping("/payment/hystrix/timeout/{id}")
public String testHystrixTimeout(@PathVariable("id") Integer id) throws InterruptedException {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
3、Hystrix解决问题
provider服务 8001超时了,调用者80不能一直卡死等待,必须有服务降级
provider服务 8001 宕机了,调用者80不能一直卡死等待,必须有服务降级
provdier服务 8001OK,调用者80自己出现了故障或有自我要求(自己的等待时间小于服务提供者)
四,服务降级
1,服务降级配置
@HystrixCommand
注解
1,对8001 提供端进行改造
对80消费端改造的话 亦是如此
- 设置自身调用超时时间的峰值,峰值内可以正常运行,如果在峰值外就执行备用方法。
- 如果不设置峰值的话,直接在方法里抛出 10 / 0 by zero 异常 也会调用备用方法
@Slf4j
@RestController
@RequestMapping("/payment")
@DefaultProperties(defaultFallback = "globalHandler")
public class PaymentController {
// 三秒以内执行完毕就是正常的业务逻辑,如果超过三秒 就去执行备用方法
@HystrixCommand(fallbackMethod = "testHystrixTimoutFallback",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")})
@GetMapping("/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
int num = 10 /0;
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*******result:"+result);
return result;
}
// 兜底方法!
public String testHystrixTimoutFallback(@PathVariable("id") Integer id) throws InterruptedException {
return paymentService.paymentInfo_TimeOut(id) + "降级服务方法执行!";
}
}
2,主启动类激活
@EnableCircuitBreaker
你在消费端 进行了降级 就在消费端添加该注解,反之…
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
2,全局服务降级
@DefaultProperties
解决的问题:
- 解决了每个方法都需要有兜底的方法的痛处
- 解决了兜底方法和业务逻辑混在一起的问题
解决代码膨胀
- 解决每个方法都要有兜底方法问题
@DefaultProperties(defaultFallback = “方法名”)
标注在类上,表示没有指定@HystrisCommand(fallbackMethod="方法名")
的方法就是用@DefaultProperties(defaultFallback="方法名")
所指定的做备用方法。
注意:就算使用全局降级配置 也需要在方法上添加@HystrisCommand
注解
1:1每个方法配置一个服务降级的方法,技术上可以,实际中不可用
1:N 除了个别重要核心业务有专属,其他普通的可以通过
@DefaultProperties(defaultFallback= "")
统一跳转到统一处理结果页面
@Slf4j
@RestController
@RequestMapping("/payment")
@DefaultProperties(defaultFallback = "globalHandler")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@HystrixCommand(fallbackMethod = "testHystrix")
@GetMapping("/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
int num = 10 /0;
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:"+result);
return result;
}
// 三秒以内执行完毕就是正常的业务逻辑,如果超过三秒 就去执行备用方法
@HystrixCommand
@GetMapping("/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
int num = 10 /0;
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*******result:"+result);
return result;
}
public String testHystrix(@PathVariable("id") Integer id){
return "客户端80 服务降级启动!";
}
public String globalHandler(){
return "全局兜底方法!!";
}
}
3,服务降级:客户端调用服务端,服务端宕机了
本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系
只需要为Feign客户端定义的远程调用接口添加一个服务降级处理的实现类即可实现解耦合
未来面临的异常
- 运行
- 超时
- 宕机
1,定义远程调用接口实现类
@Service
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentInfo_OK(Integer id) {
return "对方服务端以宕机!无法使用!";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "对方服务端以宕机!无法使用!";
}
}
2,远程调用接口中@FeignClient注解添加fallback属性
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@RequestMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@RequestMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
3,yaml配置文件添加这一句
feign:
hystrix:
enabled: true #如果处理自身的容错就开启。开启方式与生产端不一样。
4,测试
5,总结
- 创建远程调用接口实现类,并对其进行[降级]实现
- 给@FeignClient 添加fallback属性
- 配置文件注意要启动 feign.hystrix
五,服务熔断
1、熔断机制概述
熔断机制式应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,自动恢复调用链路。
熔断状态: 开启 关闭 半开启
在SpringCloud框架中,熔断机制通过Hystrix实现Hystrix会监控微服务间调用的状况。
当失败的调用到一定阈值,缺省时5秒内20此调用失败,就会启动熔断机制,熔断机制的注解是@HystrixCommand
大神论文:https://martinfowler.com/bliki/CircuitBreaker.html
2,实操
注解中的配置解释:在一个10秒钟的窗口期,如果有10个请求 60%都失败了 就熔断
1,对8001服务端的controller进行改造 增加熔断机制
@HystrixCommand(fallbackMethod = "testRongDuan_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //时间范围
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")}//失败率达到多少后跳闸
)
@RequestMapping("/test/hystrix/rongduan/{id}")
public String testRongDuan(@PathVariable Integer id){
if (id < 0){
// 这样做的目的是让他去执行兜底方法
throw new RuntimeException("id 不能为负数");
}
return "服务执行成功! id: " + id;
}
public String testRongDuan_fallback(@PathVariable Integer id){
return "id 不能为负数,降级服务执行………… id:" + id;
}
2,测试
- 在web页面 输入参数为负数会进入到 兜底方法,如果输入负数的次数 【在一个10秒钟的窗口期,如果有10个请求 60%都失败了 就熔断】符合这个要求,那么就会触发熔断机制,然后你再输入正数都不会执行成功了!
- 慢慢的他自己会检测到后台输入好几个正数了,就会自动关闭熔断
3,总结
1,熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置始终一般为MTTR(平均故障处理时间),当打开时长达到所设始终则进入半熔断状态。
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认位当前服务恢复正常,关闭熔断
2,断路器在什么情况下开始起作用
涉及到断路器的三个重要因素:快照时间窗,请求总数阈值,错误百分比阈值
1:快照时间窗:
- 断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
2:请求总数阈值:
- 在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认为20,意味着在10秒内,如果该Hystrix命令的调用次数不足20次,即使所有的请求都超时或者其他原因失败了,断路器都不会打开。
3:错误百分比阈值:
- 当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。
3,断路器开始或者关闭条件
- 当满足一定的阈值的时候(默认是10秒内超过20个请求次数)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 到达以上阈值,断路器将会开启
- 当开启的时候,所有的请求都不会进行转发。
- 一段时间后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器关闭,如果失败,继续开启
4,断路器打开之后
- 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级的fallback方法,通过断路器,实现了自动的发现错误并将降级逻辑升级为主逻辑,减少响应延迟的效果。
- 原来的主逻辑要如何恢复?
- 对于这一问题mhystrix也为我们实现了自动恢复功能。
- 当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑。
- 当休眠时间窗到期,断路器将进入半开状态,释放给一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合。
- 主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
4,ALL配置
HystrixCommandProperties
六,服务限流
在spring cloud alibaba Sentinel时再说!
七,Hystrix工作流程
https://github.com/Netflix/Hystrix/wiki/How-it-Works
八,服务监控:HystrixDashboard
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard)Hystrix会持续的记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix通过
hystrix-metrics-event-stream
项目实现了对以上指标的监控,Spring Cloud提供了Hystrix Dashboard的整合,对监控内容转化成可视化页面。
1、搭建HystrixDashboard
Hystrix 做服务监控还需要创建一个模块,而阿里巴巴的sentinel 直接给你要给网站就能使用
- 创建新的模块 could-consumer-hystrix-dashboard9001
- pom
<dependencies>
<!--新增hystrix dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</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.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>
- yml
server:
port: 9001
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:7001/eureka
spring:
application:
name: cloud-consumer-hystrix-dashboard
- 主启动
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
- 需要监控的服务都需要添加以下该依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 测试
浏览器输入 http://localhost:9001/hystrix
进入下面页面
2、监控8001提供端
8001提供端 一定要有下面两个依赖
<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>
新版本Hystrix 需要在主启动类【MainHystrix8001】中指定监控路径
不然就会报错
在8001提供方的主启动类中添加以下代码
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
在浏览器输入 http://localhost:8001/payment/hystrix/rongduan/-1 测试8001接口
发现豪猪哥的监控页面 发生了变化
3,如何查看仪表盘
Gateway新一代网关
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories
一、Gateway概述
1、Gateway是什么
gateway 官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用Zuul网关;但在2.x版本中,zuul的升级就是一直跳票,SpringCloud最后自己研发了一个网关代替Zuul,那就是 SpringCloud Gateway ,gateway是zuul 1.x版本的替代。
Gateway是在Spring生态系统之上架构的API网关服务,基于Spring 5,Spring Boot2 和Project Reactor技术。
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
SpringCloud Gateway作为Spring cloud生态系统中的网关,目标是代替 Zuul,在SpringCloud2.0以上版本中,没有对新版本的Zuul 2.0以上实现最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty,【说穿了就是 SpringCloud Gateway是异步非阻塞式】
2、Gateway能干什么
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
3、微服务架构中网关在哪里
4、SpringCloud Gateway具有的特征
- 基于Spring Frameword5 ,Project Reactor 和 SpringBoot 2.0进行构建;
- 动态路由:能够匹配任何请求属性
- 可以对路由指定Predicate(断言)和Filter(过滤器)
- 集成Hystrix的断路器功能;
- 集成Spring Cloud的服务发现功能
- 易于编写的Predicate(断言)和Filter(过滤器)
- 请求限流功能;
- 支持路径重写
5、SpringCloud Gateway与zuul的区别
在SpringCloud Finchley 正式版之前(现在H版),SpringCloud推荐的网关是Netflix提供的zuul。
-
Zuul1.x 是一个基于阻塞 I/O的API网关
-
Zuul1.x 基于Servlet2.5使用阻塞架构它不支持任何长连接 (如WebSocket)Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。
-
Zuul 2.x理念更加先进,像基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul2.x的性能较Zuul 1.x有较大的提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求次数)是Zuul的1.6倍。
-
Spring Cloud Gateway建立在Spring Framework 5、project Reactor和Spring Boot2 之上,使用非阻塞API
-
Spring Cloud Gateway 还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验。
二、Gateway的三大核心概念
1、Route路由
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
2、Predicate 断言
开发人员可以匹配Http请求中的所有内容(例如请求头或者请求参数),如果请求参数与断言相匹配则进行路由。
3、Filter 过滤
指的是Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
4、总结
- web 请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制
- predicate 就是我们的匹配条件
- filter:就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标的uri,就可以实现一个具体的路由了。
三、Spring Cloud Gateway工作流程
- 客户端向Spring Cloud Gateway发出请求,然后再Gateway Handler Mapping 中找到与请求相匹配的路由,将其发生到Gateway Web Handler
- Handler 再通过指定的的过滤器链来讲请求发送到我们实际的服务执行业务逻辑,然后返回。
- 过滤器之间使用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”) 或之后(“post”)执行业务逻辑。
- Filter在“pre”类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。
- 在“post”类型的过滤器可以做响应内容、响应头的修改,日志的输出,流量监控等有者非常重要的作用。
核心逻辑:路由转发+执行过滤链
四、入门配置
1、创建cloud-gateway-gateway-9527 模块
2、pom
做网关不需要添加 web starter
<dependencies>
<!--新增gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<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>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>
3、yaml 配置
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-gateway
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:7001/eureka
4、主启动类
@EnableDiscoveryClient
@EnableEurekaClient
@SpringBootApplication
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class,args);
}
}
5、测试
启动网关前访问:http://localhost:8001/payment/get/1
启动网关后访问:http://localhost:9527/payment/get/1
五、Gateway的网关配置
两种配置:
1、在配置文件中yaml中配置
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
2、代码中注入RouteLocator的bean
官网网站:https://spring.io/projects/spring-cloud-gateway
官网案例:
@SpringBootApplication
public class DemogatewayApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
}
自己写一个
通过9527网关访问到外网的百度新闻网址
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
RouteLocatorBuilder.Builder routes = builder.routes();
routes.route("path_rote_atguigu", r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
测试:
- 访问:http://localhost:9527/guonei
六、配置动态路由
相当于给网关配置一个负载均衡,因为看上面的配置把8001写死了
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
开启动态路由:spring.cloud.gateway.discovery.locator.enabled:true;
在添加uri的时候,开头是 lb://微服务名
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-gateway
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:7001/eureka
测试
- 因为开启了8001和8002两个端口,所以网关负载均衡的效果是 8001/8002切换
七、Predicate 断言的使用
gateway启动时打印的信息
Spring Cloud Gateway 将路由匹配作为Spring WebFlux Handler Mapping基础架构的一部分。Spring Cloud Gateway 包括许多内置的Route Predicate 工厂,所有的这些Predicate都和Http请求的不同属性匹配,多个Route Predicate可以进行组合。
Spring Cloud Gateway 创建route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route,SpringCloud Gateway包含许多内置的Route Predicate Factories.
所有的 这些谓词都匹配Http的请求的各种属性,多种谓词工厂可以组合,并通过逻辑and
官网对gateway的断言每个都写了栗子:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-after-route-predicate-factory
1、常用的断言
常用的Route Predicate
After Route Predicate
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
- 匹配该断言时间之后的 uri请求
Before Route Predicate
-
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
-
Before=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
Between Route Predicate
- Between=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] , 2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
Cookie Route Predicate
不带cookies访问
带上cookies访问 - Cookie=username,atguigu #并且Cookie是username=zhangshuai才能访问
Cookie Route Predicate 需要两个参数,一个时Cookie name,一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上就不执行
Header Route Predicate
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行;
Host Route Predicate
- Host=**.atguigu.com
Method Route Predicate
- Method=GET
Path Route Predicate
Query Route Predicate
- Query=username, \d+ #要有参数名称并且是正整数才能路由
2、小总结
- 说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
#- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
#- Cookie=username,zhangshuai #并且Cookie是username=zhangshuai才能访问
#- Header=X-Request-Id, \d+ #请求头中要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.atguigu.com
#- Method=GET
#- Query=username, \d+ #要有参数名称并且是正整数才能路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
八、Filter的使用
1、Filter是什么
路由过滤器可用于修改进入的Http请求和返回的Http响应,路由过滤器只能过滤指定路由进行使用。
Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories
声明周期 :
-
pre 在业务逻辑之前
-
post 在业务逻辑之后
种类:
-
单一的:GatewayFilter
-
全局的:GlobalFilter
2、自定义过滤器
1,两个接口
impiemerts GlobalFilter,Ordered
2.2 能干吗
全局日志记录
统一网关鉴权
2.3 示例代码
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("*********come in MyLogGateWayFilter: "+new Date());
String username = exchange.getRequest().getQueryParams().getFirst("username");
if(StringUtils.isEmpty(username)){
log.info("*****用户名为Null 非法用户,(┬_┬)");
//给人家一个回应
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
SpringCloud config分布式配置中心
一、分布式配置中心概述
1、分布式系统面临的—配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的
服务。由于每个服务都需要必要的配置信息才能够运行,所以一套集中式的,动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer【配置中心】来解决这个问题,我们每一个微服务自己都带着一个application.yaml
,上百个配置文件的管理就要管理上百个application.yaml
2、配置中心是什么
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置
官网: https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/
由于SpringCloud Config默认使用Git来存储配置文件,最推荐与github整合。
3、配置中心怎么用
SpringCloud Config 分为服务端和客户端两部分。
服务端也成为分布式配置中心,他是一个独立的微服务应用,用来连接配置服务器并未客户端提供获取配置信息,加密/解密等信息访问接口。
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样既有助于对环境配置进行版本管理,并且可以通过git客户端来方便的管理和访问配置内容。
4、配置中心能干什么
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署比如 dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务回想配置中心统一拉去配置自己的信息。
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置。
- 将配置信息以Rest接口的形式暴露。
二、Config服务端配置与测试
1、搭建
-
在github上创建一个springcloud_config的新仓库
-
获得新仓库的地址:
- https://gitee.com/ialittle/sprincloud-config.git
-
本地硬盘目录上新建git仓库并clone
- 此时在本地E盘符下 E:\springCloud\cloud2020\springcloud_config
自己创建文件:
该文件可以从周阳老师的github仓库克隆一份,复制到自己的仓库 https://github.com/zzyybs/springcloud-config
config-dev.yaml
config-prod.yaml
config-test.yaml
表示多个环境的配置文件
保存格式必须是 utf-8
- 新建Module模块 cloud-config-center-3344,它就是Cloud的配置中心模块 cloudConfig Center
- pom添加依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-config-server -->
<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>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
- yaml 配置文件
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
# Github上面git仓库的地址
uri: https://gitee.com/ialittle/sprincloud-config.git
force-pull: true #设置强行pull拉取
username: ialittle
password: zyk13343721614 #填写你自己密码
# 搜索的目录
search-paths:
- sprincloud-config
# 分支
label: master
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
- 主启动类
@EnableConfigServer
@EnableConfigServer
@EnableEurekaClient
@SpringBootApplication
public class ConfigServerMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigServerMain3344.class,args);
}
}
- 修改hosts文件,增加映射
使用火绒的安全工具
127.0.0.1 config-3344.com
- 测试通过config微服务是否可以从GitHub上获取配置内容
成功实现了通过SpringCloud Config 通过github中获取配置信息
访问:http://localhost:3344/master/config-dev.yml
2、读取配置规则
1 /{label}/{application}-{profile}.yml
(最推荐使用这种方式)
① 读取master分支
- http://localhost:3344/master/config-dev.yaml
- http://localhost:3344/master/config-test.yaml
- http://localhost:3344/master/config-prod.yaml
② 读取dev分支
- http://localhost:3344/dev/config-dev.yaml
- http://localhost:3344/dev/config-dev.yaml
- http://localhost:3344/dev/config-dev.yaml
2 /{application}-{profile}.yml
- http://localhost:3344/config-dev.yaml
- http://localhost:3344/config-test.yaml
- http://localhost:3344/config-prod.yaml
没有了分支那一项,是因为在application.yaml中配置了 label: master
所以会先去读取master的 (就算不配label 也会先去master寻找)
3 /{application}-{profile}[/{label}]
这样读取的是 JSON串
- http://localhost:3344/config/dev/master
- http://localhost:3344/config/test/master
- http://localhost:3344/config/prod/master
4 总结
- label:分支
- name[application]:服务名
- profiles:环境
5,链接gitee的三种方式
由于连接git分两种:一种为共有没有访问权限密码的,一种使用账号密码登录,一种采用ssh登录,
- 完全公开,无密码访问配置:
server:
port: 3344 #设置端口
spring:
application:
name: config-server #设置名称
cloud:
config:
server:
git:
uri: git@gitee.com:lonecloud/xxx.git #设置git仓库地址
force-pull: true #设置强行pull拉取
- 采用账号密码访问登录
server:
port: 3344 #设置端口
spring:
application:
name: config-server #设置名称
cloud:
config:
server:
git:
uri: git@gitee.com:lonecloud/xxx.git #设置git仓库地址
force-pull: true #设置强行pull拉取
username: lonecloud
password: password #填写你自己密码
- 采用SSH无密码登录
最坑的则是第三个,如果您的主机配置了ssh则直接采用(一)方案即可,如果没有配置则需要生成对应的ssh key,将其复制到此处,既可访问
server:
port: 3344
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: git@gitee.com:lonecloud/xxx.git
ignoreLocalSshSettings: true
force-pull: true
privateKey: | #这个地方复制你的RSA密码,记得这里有个| 别忘了
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
三、客户端配置和测试
1、搭建
1,新建模块
cloud-config-client-3355
2,pom
<dependencies>
<!-- 客户端的config 依赖 -->
<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>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
3,yml
简单理解:bootstrap.yaml 是用来读取并使用github上公共的配置的,而application.yaml是模块自用的
application.yaml
是用户级的资源配置项
bootstrap.yaml
是系统级的,优先级更高。
SpringCloud会创建一个 “Bootstrap Context”,作为Spring应用的"Application Context"的父上下文,初始化的时候,“Bootstrap Context”负责从外部源加载配置属性并且解析属性,这两个上下文共享一个从外部获取的"Environment"[环境]
BootStrap 属性有高优先级,默认情况下,他们不会被本地配置覆盖,“Bootstrap Context” 和“Application Context” 有着不同的约定,所以新增一个“bootstrap.yaml”文件保证“Bootstrap Context” 和“Application Context”配置的分离
要将Client模块下的 application.yml文件改为bootstrap.yml,这是很关键的。
因为bootstrap.yaml是要比application.yaml先加载的,bootstrap.yaml优先级高于application.yaml
server:
port: 3355
spring:
cloud:
config:
label: master # 哪个分支
name: config # 什么名字
profile: dev # 名字-xxx 什么环境
uri: http://localhost:3344 # 配置中心地址
# 上面的进行拼接后位: http://localhost:3344/master/config-dev.yaml
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
4,主启动类
@EnableEurekaClient
@SpringBootApplication
public class ConfigConsumerMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigConsumerMain3355.class,args);
}
}
5,业务类
这里的@Value("${config.info}’)是从github上的配置文件中获取到的
@RestController
public class ConfigController {
/**
* 这里取的值是从github上取回来的
*/
@Value("${config.info}")
private String configInfo;
@RequestMapping("/config/info")
public String test(){
return configInfo;
}
}
6,测试
成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息
四、客户端的动态刷新
1、问题:分布式的动态刷新问题
- Linux运维修改了GitHub上的配置文件内容做调整。
- 刷新3344,发现ConfigServer配置中心立刻响应。
- 刷新3355,发现ConfigClient客户端没有任何响应。
- 3355没有变化除非自己重新启动或者重新加载。
- 难道每次运维修改配置文件,客户端都要重新启动?噩梦?
2、步骤
1,修改3355客户端模块
2,添加依赖 图形化监控
<!--图形化监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2,修改yaml,暴露监控端口
# 暴漏监控端点
management:
endpoints:
web:
exposure:
include: "*"
3,controller层添加@RefreshScope
注解
@RefreshScope
@RestController
public class ConfigController {
/**
* 这里取的值是从github上取回来的
*/
@Value("${config.info}")
private String configInfo;
@RequestMapping("/config/info")
public String test(){
return configInfo;
}
}
4,运维修改github之后 给3355发一个post请求
目的是告诉3355 github上的配置文件已经修改了,请重新加载
curl -X POST “http://localhost:3355/actuator/refresh”
发送的地址为 :http://localhost:3355/actuator/refresh 【注意是post请求】
使用cmd命令 :curl -X POST “http://localhost:3355/actuator/refresh”
5,测试
- 修改github上的配置文件内容 为 3
- 先访问3344配置中心:http://localhost:3344/master/config-dev.yaml 获得结果为3
- 在访问3355:http://localhost:3355/test/config/info 获得结果为3
3、存留问题
虽然这种方式解决了不重启3355客户端的问题,但是如果有100个机器需要重新加载配置文件呢
所以这就需要使用 消息总线来进行广播。
SpringCloud Bus 消息总线
一,概述
1,是什么
Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新.
2,能干嘛
Spring Ccloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道
3,为何被称为总线
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播─些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
https://www.bilibili.com/video/av55976700?from=search&seid=%201%20501007591%205728605208
二,RabbitMQ环境配置
1,安装Erlang,下载地址:
http://erlang.org/download/otp_win64_21.3.exe
2,安装RabbitMQ,下载地址:
https://dl.bintray.com/rabbitmq/all/rabbitmq-server/3.7.14/rabbitmq-server-3.7.14.exe
3,进入RabbitMQ安装目录下的sbin目录
4,输入以下命令启动管理功能
rabbitmq-plugins enable rabbitmq_management
5,访问地址查看是否安装成功
http://localhost:15672/
6,输入账号密码并登录: guest guest
三,Bus动态刷新全局广播
必须先具备良好的RabbitMQ环境先
1,新建3366
演示广播效果,增加复杂度,再以3355为模板再制作一个3366
2,设计思想
利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置(更加推荐)
图二的架构显然更加合适,图一不适合的原因如下
- 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新职责
- 破坏了微服务各节点的对等性
- 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改
3,服务端添加消息总线支持
给cloud-config-center-3344配置中心服务端添加消息总线支持
POM
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
yml
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
# Github上面git仓库的地址
uri: https://gitee.com/ialittle/sprincloud-config.git
force-pull: true #设置强行pull拉取
username: ialittle
password: 你猜 #填写你自己密码
# 搜索的目录
search-paths:
- sprincloud-config
# 分支
label: master
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
#rabbitmq相关配置
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
4,客户端添加消息总线支持
给cloud-config-center-3355客户端添加消息总线支持
给cloud-config-center-3366客户端添加消息总线支持
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
yml
server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
label: master # 哪个分支
name: config # 什么名字
profile: dev # 名字-xxx 什么环境
uri: http://localhost:3344 # 配置中心地址
# 上面的进行拼接后位: http://localhost:3344/master/config-dev.yaml
#rabbitmq相关配置15672是web管理界面的端口;5672是NQ访问的端口
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
# 暴漏监控端点
management:
endpoints:
web:
exposure:
include: "*"
5,测试
1,修改Github上配置文件增加版本号
2,发送Post请求
curl -X POST “http://localhost:3344/actuator/bus-refresh”
一次发送,处处生效
四,Bus动态刷新定点通知
1,需求
不想全部通知,只想定点通知。只通知3355,不通知3366
2,指令格式
公式:http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server并通过destination参数类指定需要更新配置的服务或实例
3,测试
我们这里以刷新运行在3355端口上的config-client为例。只通知3355,不通知3366
cmd发送指令:curl -X POST “http://localhost:3344/actuator/bus-refresh/config-client:3355”
4,总结
SpringCloud Stream消息驱动
现在一个很项目可能分为三部分:
前端--->后端---->大数据
而后端开发使用消息中间件,可能会使用RabbitMq
而大数据开发,一般都是使用Kafka,
那么一个项目中有多个消息中间件,对于程序员,因为人员都不友好
1,消息驱动概述
1,是什么
屏蔽底层消息中间件的差异,降低切换版本,统一消息的编程模型
官网
https://spring.io/projects/spring-cloud-stream#overview
https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/
Spring Cloud Stream中文指导手册:https://m.wang1314.com/doc/webapp/topic/20971999.html
2,设计思想
1,标准MQ
- 生产者/消费者之间靠消息媒介传递信息内容
- Message
- 消息必须走特定的通道
- 消息通道MessageChannel
- 消息通道里的消息如何被消费呢,谁负责收发处理
- 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器订阅
2,为什么用Cloud Stream
stream凭什么可以统一底层差异
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder
-
INPUT对应于消费者
-
OUTPUT对应于生产者
Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播
- 在RabbitMQ就是Exchange
- 在kafka中就是Topic
3,Spring Cloud Stream标准流程套路
- Binder
- 很方便的连接中间件,屏蔽差异
- Channel
- 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel对队列进行配置
- Source和Sink
- 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入
4,编码API和常用注解
2,案例说明
RabbitMQ环境已经OK
1,工程中新建三个子模块
- cloud-stream-rabbitmq-provider8801,作为生产者进行发消息模块
- cloud-stream-rabbitmq-consumer8802,作为消息接收模块
- cloud-stream-rabbitmq-consumer8803,作为消息接收模块
3,消息驱动之生产者
1,新建Module
cloud-stream-rabbitmq-provider8801
pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
主启动
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
业务类
发送消息接口
public interface IMessageProvider
{
public String send();
}
发送消息接口实现类
@EnableBinding(Source.class)
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; //消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("========serial:"+serial);
return null;
}
}
controller
@RestController
public class SendMessageController {
@Resource
private IMessageProvider iMessageProvider;
@GetMapping("/sendMessage")
public String send(){
iMessageProvider.send();
return null;
}
}
2,测试
启动7001eureka
启动rabbitmq
启动8801
访问:http://localhost:8801/sendMessage
4,消息驱动之消费者
1,新建Module
cloud-stream-rabbitmq-consumer8802
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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>
yml
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
主启动
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
controller
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenercontroller {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
System.out.println("消费者1号,------>接收到的消息:"+message.getPayload()+"\t serverPort:"+serverPort);
}
}
2,测试8801发送8802接收消息
http://localhost:8801/sendMessage
5,分组消费与持久化
依照8802,clone出来一份运行8803
1,分组
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
8802/8803都变成相同组,group两个相同
1,8802修改YML
group: A_Group
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: A_Group #<----------------------------------------关键
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
2,8803修改YML
group: B_Group
server:
port: 8803
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: A_Group #<----------------------------------------关键
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8803.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
3,测试
启动
- RabbitMQ
- 7001
- 8801
- 8802
- 8803
访问: localhost:8801/sendMessage
消费者1号
消费者2号
结论
同一个组的多个微服务实例,每次只会有一个拿到
2,持久化
就是当服务挂了,怎么消费没有消费的数据??
这里,先将8802移除A组,
然后将02,03服务关闭
此时生产者开启,发送3条消息
此时重启02,03
可以看到,当02退出A组后,它就获取不到在它宕机的时间段内的数据
但是03重启后,直接获取到了宕机期间它没有消费的数据,并且消费了
总结:
也就是,当我们没有配置分组时,会出现消息漏消费的问题
而配置分组后,我们可以自动获取未消费的数据
SpringCloud Sleuth分布式请求链路追踪
一,概述
为什么会出现这个技术?需要解决哪些问题?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成─条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
是什么
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案
在分布式系统中提供追踪解决方案并且兼容支持了zipkin
https://github.com/spring-cloud/spring-cloud-sleuth
二,搭建链路监控步骤
1,zipkin
SpringCloud从F版起已不需要自己构建Zipkin server了,只需要调用jar包即可
1,下载地址
https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/2.12.9/zipkin-server-2.12.9-exec.jar
zipkin-server-2.12.9.exec.jar
2,运行控制台
java -jar zipkin-server-2.12.9.exec.jar
访问:http://localhost:9411/zipkin/
3,术语
完整的调用链路
表示一请求链路,一条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来
上图what
一条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来
整个链路的依赖关系如下:
名词解释
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
span:表示调用链路来源,通俗的理解span就是一次请求信息
2,服务提供者
1,服务提供者
修改cloud-provider-payment8001
pom
<dependencies>
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 8001
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率值介于0到1之间,1则表示全部采集
probability: 1
datasource:
type: com.alibaba.druid.pool.DruidDataSource #数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动包
url: jdbc:mysql://localhost:3306/db_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
mybatis:
mapperLocations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.kun.entity #所有的entity别名类所在的包
controller
@GetMapping("/payment/zipkin")
public String paymentZipkin()
{
return "hi ,i'am paymentzipkin server fall back,welcome to atguigu,O(∩_∩)O哈哈~";
}
2,服务消费者(调用方)
修改cloud-consumer-order80
pom
<dependencies>
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
yaml
server:
port: 80
spring:
application:
name: cloud-consumer-order
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率值介于0到1之间,1则表示全部采集
probability: 1
eureka:
client:
register-with-eureka: true #表示是否将自己注册进Eurekaserver默认为true。
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka #入住Eurekaserver的地址
controller
// ====================> zipkin+sleuth
@GetMapping("/payment/zipkin")
public String paymentZipkin()
{
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
3,测试
依次启动zipkin/eureka7001/8001/80
打开浏览器访问:http:localhost:9411
会出现以下界面
查看
查看依赖关系
原理
SpringCloud Alibaba入门简介
一,是什么
SpringCloud Alibaba是阿里巴巴集团开源的一套微服务架构解决方案。
微服务架构是为了更好的分布式系统开发,将一个应用拆分成多个子应用,每一个服务都是可以独立运行的子工程。其中涵盖了非常多的内容,包括:服务治理、配置管理、限流降级以及对阿里开源生态(Dubbo、RocketMQ等)支持的N多组件。
二,相关组件
- Sentinel:阿里巴巴开源产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- RocketMQ:Apache RocketMQ™ 基于 Java 的高性能、高吞吐量的分布式消息和流计算平台。
- Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
- Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
三,主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
四,版本说明
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
项目的版本号格式为 x.x.x 的形式,其中 x 的数值类型为数字,从 0 开始取值,且不限于 0~9 这个范围。项目处于孵化器阶段时,第一位版本号固定使用 0,即版本号为 0.x.x 的格式。
由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且 spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟 SpringBoot 版本号一致的版本:
- 1.5.x 版本适用于 Spring Boot 1.5.x
- 2.0.x 版本适用于 Spring Boot 2.0.x
- 2.1.x 版本适用于 Spring Boot 2.1.x
- 2.2.x 版本适用于 Spring Boot 2.2.x
五,去哪下
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
Nacos服务注册和配置中心
一,Nacos简介
1,nacos是什么?
Nacos 是一个集服务动态发现、服务配置、服务元数据及流量管理于一体的管理中心,能帮助我们更好的发现、配置和管理微服务。
Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理,如:
- Kubernetes Service
- gRPC & Dubbo RPC Service
- Spring Cloud RESTful Service
Nacos 可与spring,springboot,springcloud,dubbo,docke, k8s一起使用,详细使用教程可参考官网资料:
2,能干嘛
- 替代Eureka做服务注册中心
- 替代Config做服务配置中心
3,去哪下
二,安装并运行Nacos
本地Java8+Maven环境已经OK
解压安装包,直接运行bin目录下的startup.cmd
如果报错话
编辑startup.cmd
rem set MODE="cluster"
set MODE="standalone"
命令运行成功后直接访问http://localhost:8848/nacos
登录:默认账号密码都是nacos
三,Nacos作为服务注册中心演示
1,基于Nacos的服务提供者
1,新建Module
cloudalibaba-provider-payment9001
2,POM
父POM
!--spring cloud alibaba 2.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
<dependencies>
<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.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>
3,YML
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
4,主启动
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}
5,业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
6,测试
访问:http://lcoalhost:9001/payment/nacos/1
为了下一章节演示nacos的负载均衡,参照9001新建9002
或者取巧不想新建重复体力劳动,直接拷贝虚拟端口映射
2,基于Nacos的服务消费者
1,新建Module
cloudalibaba-consumer-nacos-order83
2,POM
<dependencies>
<!--SpringCloud ailibaba nacos -->
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<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>
为什么nacos支持负载均衡
3,YML
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
4,主启动
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class,args);
}
}
5,业务类
config
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
controller
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id)
{
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
6,测试
nacos控制台
访问:http://localhost:83/consumer/payment/nacos/13
83访问9001/9002,轮询负载OK
3,服务注册中心对比
各种注册中心对比
1,Nacos全景图所示
2,Nacos和CAP
3,切换
Nacos支持AP和CP模式的切换
四,Nacos作为服务配置中心演示
1,Nacos作为配置中心-基础配置
1,新建Module
cloudalibaba-config-nacos-client3377
2,POM
<dependencies>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<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.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>
3,YML
why配置两个
Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心地址
config:
server-addr: localhost:8848 #配置中心地址
file-extension: yaml #指定yaml格式的配置
application
spring:
profiles:
active: dev
4,主启动
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class,args);
}
}
5,业务类
Controller
@RefreshScope
@RestController
public class ConfigClientController {
@Value("${config.info}") //config:info
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
@RefreshScope
通过Spring Cloud 原生注解@RefreshScope实现配置自动更新:
6,在Nacos中添加配置信息
Nacos中的匹配规则
1,理论
Nacos中的dataid的组成格式与SpringBoot配置文件中的匹配规则
2,实操
1,配置新增
DataId:nacos-config-client-dev.yaml
#内容
config:
info: nacos-config-client-dev.yaml version=1
2,Nacos界面配置对应
设置DataId
公式:
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
prefix默认为spring.application.name的值
spring.profile.active既为当前环境对应的profile,可以通过配置项spring.profile.active 来配置
file-exetension为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension 配置
小总结说明:
7,测试
启动前需要在nacos客户端-配置管理-配置管理栏目下有没有对应的yaml配置文件
运行cloud-config-nacos-client3377的主启动类
调用接口查看配置信息:http://localhost:3377/config/info
8,自带动态刷新
修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新
2,Nacos作为配置中心-分类配置
问题
1,多环境多项目管理
问题1:
- 实际开发中,通常—个系统会准备
- dev开发环境
- test测试环境
- prod生产环境。
- 如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:
- 一个大型分布式微服务系统会有很多微服务子项目,
- 每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…
- 那怎么对这些微服务配置进行管理呢?
2,Nacos的图形化管理界面
1,配置管理
2,命名空间
3,Namespace+Group+Data ID三者关系
4,Case
1,DataID方案
指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置
1,默认空间+默认分组+新建dev和test两个DataID
新建dev配置DataID
新建test配置DataID
2,通过spring.profile.active属性就能进行多环境下配置文件的读取
3,测试
访问:http://localhost:3377/config/info
2,Group方案
通过Group实现环境区分
1,新建Group
2,YML
在config下增加一条group的配置即可。可配置为DEV_GROUP或TEST_GROUP
3,Namespace方案
1,新建dev/test的Namespace
2,回到服务管理-服务列表查看
3,按照域名配置填写
4,YML
五,Nacos集群和持久化配置
一,官网说明
https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html
官网架构图(写的(┬_┬))
上图官网翻译,真实情况
说明
按照上述,我们需要mysql数据库
官网说明:https://nacos.io/zh-cn/docs/deployment.html
重点说明
再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql
二,Nacos持久化配置解释
Nacos默认自带的是嵌入式数据库derby
https://github.com/alibaba/nacos/blob/develop/config/pom.xml
1,derby到mysql切换配置步骤
nacos-server-1.1.4\nacos\conf目录下找到sql脚本:nacos-mysql.sql
nacos-server-1.1.4\nacos\conf目录下找到application.properties
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow
启动nacos,可以看到是个全新的空记录界面,以前是记录进derby
六,Linux版Nacos+MySQL生产环境配置
SpringCloud Alibaba Sentinel实现熔断与限流
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
一,Sentinel简介
Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel 具有以下特性:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的开源生态:
Sentinel 的历史
- 2012 年,Sentinel 诞生,主要功能为入口流量控制。
- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
- 2018 年,Sentinel 开源,并持续演进。
- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对 Service Mesh 场景也推出了 Envoy 集群流量控制支持,以解决 Service Mesh 架构下多语言限流的问题。
- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel 功能和设计理念
流量控制
什么是流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
流量控制设计理念
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
熔断降级
什么是熔断降级
除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。
Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配,并且对于一些使用了 ThreadLocal 的场景来说会有问题(如 Spring 事务)。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
- 针对慢调用和异常对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以根据响应时间和异常等不稳定因素来快速对不稳定的调用进行熔断。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新渐进式地恢复。
系统自适应保护
Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
Sentinel 是如何工作的
Sentinel 的主要工作机制如下:
- 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
- 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
- Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。
二,安装Sentinel控制台
下载地址:https://github.com/alibaba/Sentinel/releases
百度网盘:
启动
java -jar sentinel-dashboard-1.7.0.jar 默认端口8080
默认账号密码都是sentinel
三,初始化演示工程
1,启动Nacos8848
启动Nacos8848成功:http://localhost:8848/nacos/#/login
2,新建Module
cloudalibaba-sentinel-service8401
3,POM
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</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>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</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>
4,YML
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
management:
endpoints:
web:
exposure:
include: '*'
5,主启动
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class,args);
}
}
6,业务类
@RestController
public class FlowLimitController
{
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
7,启动Sentinel8080
java -jar sentinel-dashboard-1.7.0
8,启动微服务8401
9,启动8401微服务
启动8401微服务后查看sentienl控制台
空空如也,啥都没有
10,Sentinel采用的懒加载说明
执行一次访问即可
http://localhost:8401/testA
http://localhost:8401/testB
sentinel8080正在监控微服务8401
四,流控规则
1,基本介绍
-
资源名:唯一名称,默认请求路径
-
针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
-
阈值类型/单机阈值:
- QPS(每秒钟的请求数量)∶当调用该api的QPS达到阈值的时候,进行限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
-
是否集群:不需要集群
-
流控模工:直接:
- api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
-
流控效里:
- 快速失败:直接失败,抛异常
- Wam up:根据codeFactor (冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
2,流控模式
1,直接(默认)
直接->快速失败(系统默认)
配置及说明
测试
快速点击访问http://localhost:8401/testA
思考???
直接调用默认报错信息,技术方面OK but,是否应该有我们自己的后续处理?类似有一个fallback的兜底方法?
2,关联
1,是什么?
- 当关联的资源达到阈值时,就限流自己
- 当与A关联的资源B达到阈值后,就限流自己
- B惹事,A挂了
2,配置A
3,postman模拟并发密集访问testB
1,访问testB成功
2,postman里新建多线程集合组
3,将访问地址添加进新线程组
大批量线程高并发访问B,导致A失效了
运行后发现testA挂了
3,链路
3,流控效果
直接->快速失败(默认的流控处理)
1,预热
公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
官网:
默认coldFactor为3,即请求QPS从threshold/3开始,经预热时长逐渐升至设定的QPS阈值。
https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81—%E5%86%B7%E5%90%AF%E5%8A%A8
2,Warmup配置
4,测试:
多次点击http://localhost:8401/testB
刚开始不行,后续慢慢OK
5,应用场景
如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值
2,排队等待
匀速排队,阈值必须设置为QPS
官网:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接节来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求,
测试
五,降级规则
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException).
Sentinel的断路器是没有半开状态的
半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。具体可以参考Hystrix
1,降级策略实战
1,RT
2,测试
代码
@GetMapping("/testD")
public String testD()
{
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
log.info("testD 测试RT");
return "------testD";
}
配置
jmeter压测
结论
2,异常比例
测试
代码
@GetMapping("/testD")
public String testD()
{
log.info("testD 测试RT");
int age = 10/0;
return "------testD";
}
配置
jmeter
结论
3,异常数
是什么
异常数是按照分钟统计的
测试
代码
@GetMapping("/testE")
public String testE()
{
log.info("testE 测试异常数");
int age = 10/0;
return "------testE 测试异常数";
}
配置
jmeter
六,热点key限流
1,基本介绍
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
2,承上启下复习start
@SentinelResource
兜底方法
分为系统默认和客户自定义,两种
之前的case,限流出问题后,都是用sentinel系统默认的提示: Blocked by Sentinel (flowlimiting)
我们能不能自定?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?
结论
从HystrixCommand到@sentinelResource
3,代码
修改Controller
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
//int age = 10/0;
return "------testHotKey";
}
//兜底方法
public String deal_testHotKey (String p1, String p2, BlockException exception){
return "------deal_testHotKey,o(╥﹏╥)o";
}
4,配置
配置Sentinel
方法testHostKey里面第一个参数只要QPS超过每秒1次,马上降级处理
5,测试
- error:http://localhost:8401/testHotKey?p1=abc
- error:http://localhost:8401/testHotKey?p1=abc&p2=33
- right:http://localhost:8401/testHotKey?p2=abc
6,参数例外项
1,特殊情况
- 超过1秒钟一个后,达到阈值1后马上被限流
- 我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样
- 假如当p1的值等于5时,它的阈值可以达到200
2,配置
3,测试
- error:http://localhost:8401/testHotKey?p1=3
- 当p1不等于5的时候,阈值就是平常的1
- right:http://localhost:8401/testHotKey?p1=5
- 当p1等于5的时候,阈值变为100
4,前提条件
热点参数的注意点,参数必须是基本类型或者String
七,系统规则
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
各项配置参数说明
配置
测试
访问该服务器下的网站每秒只能执行一次:
八,@SentinelResource
1,按资源名称限流+后续处理
1,Module
修改cloudalibaba-sentinel-service8401,pom文件
新增pom
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2,业务类
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试oK", new Payment(202L, "seriale01"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName());
}
}
3,配置流控规则
4,测试
1秒钟点击1下,OK
超过上述问题,疯狂点击,返回了自己定义的限流处理信息,限流发送
2,按照Url地址限流+后续处理
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
1,controller
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl()
{
return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));
}
2,Sentinel控制台配置
3,测试
疯狂点击http://localhost:8401/rateLimit/byUrl
3,上面兜底方法面临的问题
- 系统默认的,没有体现我们自己的业务要求。
- 衣照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
- 每个业务方法都添加一个兜底的,那代码膨胀加剧。
- 全局统—的处理方法没有体现。
4,客户自定义限流处理逻辑
1,自定义限流处理类
创建customerBlockHandler类用于自定义限流处理逻辑
public class CustomerBLockHandler {
public static CommonResult handlerException(BlockException exception){
return new CommonResult(444,"按客户自定义,global" ,new Payment(2020L,"serial0o3----1"));
}
public static CommonResult handlerException2(BlockException exception){
return new CommonResult(444,"按客户自定义,global" ,new Payment(2020L,"serial0o3----2"));
}
}
2,RateLimitController
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBLockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler()
{
return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));
}
3,Sentinel控制台配置
4,测试
启动微服务后先调用一次:http://localhost:8401/rateLimit/customerBlockHandler
测试后我们自定义的出来了
5,更多注解属性说明
九,服务熔断功能
sentinel整合ribbon+openFeign+fallback
1,Ribbon系列
1,服务提供者9003/9004
新建cloudalibaba-provider-payment9003/9004
pom
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<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>
yml
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
controller
@RestController
public class PaymentController {
@Value("${server.port}")
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"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment);
return result;
}
}
2,服务消费者84
新建cloudalibaba-consumer-nacos-order84
pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.kun.cloud</groupId>
<artifactId>cloud-commons-api</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.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>
yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
service-url:
nacos-user-service: http://nacos-payment-provider
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
config
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
controller
@Slf4j
@RestController
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback") //没有配置
//@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
//@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id, CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
//blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
}
2,Feign系列
1,修改84模块
pom
openfeign
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
yml
对Feign的支持
#对Feign的支持
feign:
sentinel:
enabled: true
主启动
@添加@EnableFeignClients启动Feign的功能
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
新增service
接口
带@FeignClient注解的业务接口
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
实现类
@Component
public class PaymentFallbackService implements PaymentService{
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
controller
// OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
测试
访问: localhost:84/consumer/paymentSQL/1
测试84调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级,不会被耗死
3,熔断框架比较
十,规则持久化
一旦我们重启应用,Sentinel规则将消失,生产环境需要将配置规则进行持久化
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上Sentinel上的流控规则持续有效
1,修改8401模块
cloudalibaba-sentinel-service8401
pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yml
添加Nacos数据源配置
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataid: ${spring.application.name}
groupid: DEFAULT_GROUP
data-type: json
rule-type: flow
2,添加Nacos业务规则配置
内容解析
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
启动8401后刷新sentinel发现业务规则有了
快速访问测试接口: localhost:8401/rateLimit/byUrl
停止8401再看sentinel
重新启动8401再看sentinel
多次调用后,重新配置出现了,持久化验证通过
多次调用: localhost:8401/rateLimit/byUrl
SpringCloud Alibaba Seata处理分布式事务
1,分布式事务问题
1,分布式前
单机单库没这个问题
- 从1:1 -> 1:N -> N: N
2,分布式之后
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
-
仓储服务:对给定的商品扣除仓储数量。
-
订单服务:根据平购需求创建订单。
-
帐户服务:从用户帐户中扣除余额。
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
2,Seata简介
1,是什么
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
官网地址:http://seata.io/zh-cn/
2,能干嘛
一个典型的分布式事务过程
1,分布式事务处理过程的-ID+三组件模型
1,Transaction ID XID
全局唯一的事务ID
2,3组件概念
1,Transaction Coordinator(TC)
事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
2,Transaction Manager™
控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
3,Resource Manager(RM)
控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;
3,处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
3,去哪下
发布说明:https://github.com/seata/seata/releases
4,怎么玩
本地@Transactional
全局@GlobalTransactional
SEATA的分布式交易解决方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMofK39f-1628474544800)(SpringBoot%20Alibaa.assets/%E5%9B%BE%E5%83%8F.bmp)]
3,Seata-Server安装
1,官网地址:
http://seata.io/zh-cn/
2,环境准备
seata:0.9.0
mysql:5.7
nacos:1.1.4
jdk:1.8
3,下载地址
http://seata.io/zh-cn/blog/download.html
4,配置
1,修改file.conf
seata-server-0.9.0.zip解压到指定目录并修改conf目录下的file.conf配置文件
1,service模块
2,store模块
3,新建库seata
建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
db_store.sql
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
`xid` varchar(128) not null,
`transaction_id` bigint,
`status` tinyint not null,
`application_id` varchar(32),
`transaction_service_group` varchar(32),
`transaction_name` varchar(128),
`timeout` int,
`begin_time` bigint,
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`xid`),
key `idx_gmt_modified_status` (`gmt_modified`, `status`),
key `idx_transaction_id` (`transaction_id`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
2,修改registry.conf
6.修改seata-server-0.9.0\seata\conf目录下的registry.conf配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PDl3uYW-1628474544803)(SpringBoot%20Alibaa.assets/
)]
5,启动
先启动Nacos端口号8848
再启动seata-server
- softs\seata-server-0.9.0\seata\bin
- seata-server.bat
访问nacos:http://localhost:8848/nacos
安装成功
4,数据库准备
订单/库存/账户业务数据库准备
1,分布式事务业务说明
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单–>扣库存–>减账户(余额)
2,创建业务数据库
- seata_order: 存储订单的数据库
- seata_storage:存储库存的数据库
- seata_account: 存储账户信息的数据库
1,创建业务数据库
建表SQL
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
按照上述3库分别建对应业务表
seata_order库下建t_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 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
seata_storage库下建t_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=2 DEFAULT CHARSET=utf8;
INSERT INTO t_storage VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
seata_account库下建t_account表
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`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=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account VALUES('1','1','1000','0','1000')
SELECT * FROM t_account;
2,建对应的回滚日志表
按照上述3库分别建对应的回滚日志表
seata-server-0.9.0\seata\conf目录下的db_undo_log.sql
CREATE 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;
3,最终效果
5,微服务准备
订单/库存/账户业务微服务准备
1,业务需求
下订单->减库存->扣余额->改(订单)状态
2,新建订单Order-Module
seata-order-service2001
pom
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<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>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</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>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
yml
server:
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
username: root
password: 1111111
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
实体类
CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
Order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
Dao接口及实现
OrderDao
@Mapper
public interface OrderDao
{
//新建订单
void create(Order order);
//修改订单状态,从零改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
resources文件夹下新建mapper文件夹后添加,OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.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接口及实现
OrderService
public interface OrderService{
void create(Order order);
}
OrderServiceImpl
@Service
@Slf4j
public class OrderServiceImpl implements OrderService
{
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
*/
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order){
log.info("----->开始新建订单");
//新建订单
orderDao.create(order);
//扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//修改订单状态,从零到1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了");
}
}
StorageService
@FeignClient(value = "seata-storage-service")
public interface StorageService{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
AccountService
@FeignClient(value = "seata-account-service")
public interface AccountService{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
Controller
OrderController
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
Config配置
MyBatisConfig
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
DataSourceProxyConfig
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
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();
}
}
主启动
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动创建的配置
public class SeataOrderMainApp2001{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6381"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
apollo {
app.id = "fescar-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
3,新建库存Storage-Module
seata-order-service2002
pom
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
yml
server:
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
username: root
password: 111111
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
实体类
CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
Storage
@Data
public class Storage {
private Long id;
// 产品id
private Long productId;
//总库存
private Integer total;
//已用库存
private Integer used;
//剩余库存
private Integer residue;
}
Dao接口及实现
StorageDao
@Mapper
public interface StorageDao {
//扣减库存信息
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
resources文件夹下新建mapper文件夹后添加,StorageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.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接口及实现
StorageService
public interface StorageService {
// 扣减库存
void decrease(Long productId, Integer count);
}
StorageServiceImpl
@Slf4j
@Service
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
//扣减库存
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
System.out.println(productId+"==="+count);
storageDao.decrease(productId, count);
log.info("------->storage-service中扣减库存结束");
}
}
Controller
@RestController
public class StorageController {
@Resource
private StorageService storageService;
//扣减库存
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200, "扣减库存成功!");
}
}
Config配置
MyBatisConfig
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
DataSourceProxyConfig
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
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();
}
}
主启动
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageServiceApplication2002.class, args);
}
}
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#vgroup->rgroup
vgroup_mapping.fsp_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6381"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
apollo {
app.id = "fescar-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
4,新建账户Account-Module
seata-order-service2003
pom
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
yml
server:
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
username: root
password: 1111111
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
实体类
CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
Account
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
//用户id
private Long userId;
//总额度
private BigDecimal total;
//已用额度
private BigDecimal used;
//剩余额度
private BigDecimal residue;
}
Dao接口及实现
AccountDao
@Mapper
public interface AccountDao {
//扣减账户余额
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
resources文件夹下新建mapper文件夹后添加,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.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接口及实现
AccountService
public interface AccountService {
//扣减账户余额
void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
AccountServiceImpl
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
AccountDao accountDao;
//扣减账户余额
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("------->account-service中扣减账户余额开始");
//模拟超时异常,全局事务回滚
//try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
accountDao.decrease(userId, money);
log.info("------->account-service中扣减账户余额结束");
}
}
Controller
AccountController
@RestController
public class AccountController {
@Resource
AccountService accountService;
//扣减账户余额
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣减账户余额成功!");
}
}
Config配置
MyBatisConfig
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
DataSourceProxyConfig
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
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();
}
}
主启动
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003
{
public static void main(String[] args)
{
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称
default.grouplist = "127.0.0.1:8091"
enableDegrade = false
disable = false
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
## transaction log store
store {
## store mode: file、db
mode = "db"
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
lock {
## the lock store mode: local、remote
mode = "remote"
local {
## store locks in user's database
}
remote {
## store locks in the seata's server
}
}
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
## metrics settings
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
4,测试
下订单->减库存->扣余额->改(订单)状态
1,数据库初始情况
2,正常下单
http://localhost:2001/order/create?userId=1&producrId=1&counr=10&money=100
数据库情况
3,超时异常
AccountServiceImpl添加超时
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
AccountDao accountDao;
//扣减账户余额
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("------->account-service中扣减账户余额开始");
//模拟超时异常,全局事务回滚
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
accountDao.decrease(userId, money);
log.info("------->account-service中扣减账户余额结束");
}
}
测试后
-
故障情况
- 当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1
- 而且由于feign的重试机制,账户余额还有可能被多次扣减
-
添加@GlobalTransactional
- OrderServiceImpl@GlobalTransactional
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
* 注释掉 @GlobalTransactional 的时候,需要注意下方这个方法里面手动模拟了延时,也需要注释掉
* com.atguigu.springcloud.alibaba.service.impl.AccountServiceImpl#decrease(java.lang.Long, java.math.BigDecimal)
*/
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
System.out.println(order);
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
下单后数据库数据并没有任何改变
记录都添加不进来
6,Seata之原理简介
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
1,再看TC/TM/RM三大组件
2,AT模式如何做到对业务的无侵入
1,是什么
2,一阶段加载
在一阶段,Seata 会拦截“业务SQL”,
- 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
- 执行“业务SQL”更新业务数据,在业务数据更新之后,
- 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
3,二阶段提交
二阶段如是顺利提交的话,
因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
4,二阶段回滚
阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。