谈谈你对微服务的理解
微服务架构下的一整套解决方案
- 服务注册与发现
- 服务调用
- 服务熔断
- 负载均衡
- 服务降级
- 服务消息队列
- 配置中心
- 服务网关
- 服务监控
- 全链路追踪
- 自动化构建部署
- 服务定时任务调度操作
Spring Cloud
分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶
下面一张图是京东的促销架构
阿里的架构图:
京东物流的架构图:
基础服务:
Spring Cloud技术栈
这是原来2020年以前的微服务方案
但是随着Eureka等组件的闭源,后续的一些解决方案也有了新的替换产品
Spring Cloud版本选型
SpringBoot2.X版 和 SpringCloud H版
SpringBoot官方已经强烈推荐 2.X版
SpringCloud采用英国伦敦地铁站的名称来命名,并由地铁站名称字母A-Z一次类推的形式发布迭代版本
SpringCloud是由许多子项目组成的综合项目,各子项目有不同的发布节奏,为了管理SpringCloud与各子项目的版本依赖关系,发布了一个清单,其中包括了某个SpringCloud版对应的子项目版本,为了避免SpringCloud版本号与子项目版本号混淆,SpringCloud版采用了名称而非版本号命名。例如Angel,Brixton。当SpringCloud的发布内容积累到临界点或者一个重大BUG被解决后,会发布一个Service releases版本,俗称SRX版本,比如 Greenwich.SR2就是SpringCloud发布的Greenwich版本的第二个SRX版本
SpringBoot和SpringCloud版本约束
SpringBoot和SpringCloud的版本选择也不是任意的,而是应该参考官网的约束配置
地址:https://spring.io/projects/spring-cloud
版本对应:https://start.spring.io/actuator/info
关于Cloud各种组件的停更/升级/替换
停更引发的升级惨案
- 被动修复Bugs
- 不再接受合并请求
- 不再发布新版本
明细条目
- 服务调用
- Eureka
- Zookeeper
- Consul
- Nacos (推荐)
- 服务调用
- Feign
- OpenFeign (推荐)
- Ribbon
- LoadBalancer
- 服务降级
- Hystrix
- resilience4j
- sentienl (推荐)
- 服务网关
- Zuul
- Zuul2
- Gateway(推荐)
- 服务配置
- Config
- Nacos(推荐)
- 服务总线
- Bus
- Nacos(推荐)
Clound升级
环境搭建
1,创建父工程,pom依赖
-
建一个空工程---------------------------约定 > 配置 > 编码
-
File——》new Project——》mavan
-
Mavan选安装的3.5及以上的
-
设置字符集
-
注解生效激活
* -
java编译版本选8
-
文件过滤
2.父工程的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.springboot</groupId>
<artifactId>clound2020</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 统一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子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>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
- demo示意图:
3.创建子模块,pay模块
- 微服务模块构建步骤
- 建立module
- 改POM
- 改YML
- 主启动
- 业务类
- 建库----》payment
- 新建业务类
* Payment
package com.atguigu.springcloud.entities;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}
CommonResult
package com.atguigu.springcloud.entities;
@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);
}
}
PaymentService
package com.atguigu.springcloud.service;
public interface PaymentService
{
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}
PaymentServiceImpl
package com.atguigu.springcloud.service.impl;
@Service
public class PaymentServiceImpl implements PaymentService
{
@Resource
private PaymentDao paymentDao;
public int create(Payment payment)
{
return paymentDao.create(payment);
}
public Payment getPaymentById(Long id)
{
return paymentDao.getPaymentById(id);
}
}
PaymentController
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class PaymentController
{
@Resource
private PaymentService paymentService;
@PostMapping(value = "/payment/create")
public CommonResult create( Payment payment)
{
int result = paymentService.create(payment);
log.info("*****插入结果:"+result);
if(result > 0)
{
return new CommonResult(200,"插入数据库成功",result);
}else{
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
Payment payment = paymentService.getPaymentById(id);
if(payment != null)
{
return new CommonResult(200,"查询成功 ",payment);
}else{
return new CommonResult(444,"没有对应记录,查询ID: "+id,null);
}
}
}
4.配置application.yml
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包
- 创建mapper-------》mapper/*.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.dao.PaymentDao">
<!-- useGeneratedKeys 插入成功返回值大于0,返回keyProperty-->
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values(#{serial});
</insert>
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>
<!-- 实际开发建议用resultMap,映射解决不规范-->
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id=#{id};
</select>
</mapper>
- 测试(postMan)
4.配置热部署
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- 在父工程的pom添加插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
- 开启自动编译的权限
4 跟新值----------------ctrl+shift+alt+/------->repository
- . 重启idea测试--------这里我就不这么弄了,我用的方法:
- 添加热部署依赖
- 编写代码ctrl+f9实现热部署
5.创建cloud-consumer-order80子模块
- 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.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>
- 创建主启动类
- 编写业务类
-
创建与cloud-provider-payment8001相同的entities
-
RestTemplate官网
- RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率
-
ApplicationContextConfig
package com.atguigu.springclound.config; @Configuration public class ApplicationContextConfig { @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } }
-
OrderController
package com.atguigu.springclound.controller; @RestController @Slf4j public class OrderController { public static final String PAYMENT_URL = "http://localhost:8001"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/payment/create") public CommonResult<Payment> create(Payment payment) { return restTemplate.postForObject(PAYMENT_URL +"/payment/create",payment, CommonResult.class); } @GetMapping("/consumer/payment/get/{id}") public CommonResult<Payment> getPayment(@PathVariable("id") Long id) { return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class ); } }
-
- Run Dashboars如果不自动出来的请观看P13
6.工程重构
- 将cloud-provider-payment8001和cloud-consumer-order80的entities抽出到cloud-api-commons
- 对cloud-api-commons模块clean—install
- cloud-provider-payment8001和cloud-consumer-order80引入依赖
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
- 启动测试即可
服务注册与发现,Eureka:
前面我们没有服务注册中心,也可以服务间调用,为什么还要服务注册?
当服务很多时,单靠代码手动管理是很麻烦的,需要一个公共组件,统一管理多服务,包括服务是否正常运行,等
Eureka用于服务注册,目前官网已经停止更新
1.引入pom依赖
<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--boot 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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
2.配置yaml
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
3.主启动类
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer //服务注册中心
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}
4.测试------http://localhost:7001/
5.其他服务注册到eureka:
- 主启动类添加@EnableEurekaClient注解
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
- 在cloud-provider-payment8001POM添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 修改yaml配置文件,以下是添加的内容
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
- 将cloud-consumer-order80注册进去,步骤和上面一样,下面只记录下yaml添加的内容
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
- 测试结果如下
集群版eureka:
原理
1,就是pay模块启动时,注册自己,并且自身信息也放入eureka
2.order模块,首先也注册自己,放入信息,当要调用pay时,先从eureka拿到pay的调用地址
3.通过HttpClient调用
并且还会缓存一份到本地,每30秒更新一次
EurekaServer集群环境构建
- 修改映射配置添加进hosts文件,找到 C:\Windows\System32\drivers\etc路径下的hosts文件添加如下内容
- 建一个cloud-eureka-server7002模块,与cloud-eureka-server7001一样
- 配置文件yaml
cloud-eureka-server7001
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
defaultZone: http://eureka7002.com:7002/eureka/
#单机就是7001自己
# defaultZone: http://eureka7001.com:7001/eureka/
cloud-eureka-server7002
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
defaultZone: http://eureka7001.com:7001/eureka/
#单机就是7001自己
# defaultZone: http://eureka7001.com:7001/eureka/
- 浏览器测试即可:http://eureka7002.com:7001/和http://eureka7002.com:7002/
- 将cloud-provider-payment8001和cloud-consumer-order80注册进去----修改yaml即可
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# 单击版
# defaultZone: http://localhost:7001/eureka
# 集群版
defaultZone: http://eureka7001.com:7001//eureka,http://eureka7002.com:7002//eureka
- 创建支付服务提供者8001集群,直接复制8001创建模块cloud-provider-payment8002即可
- 修改controller
@RestController
@Slf4j
public class PaymentController
{
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment)
{
int result = paymentService.create(payment);
log.info("*****插入结果:"+result);
if(result > 0)
{
return new CommonResult(200,"插入数据库成功,serverPort: "+serverPort,result);
}else{
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
Payment payment = paymentService.getPaymentById(id);
if(payment != null)
{
return new CommonResult(200,"查询成功,serverPort: "+serverPort,payment);
}else{
return new CommonResult(444,"没有对应记录,查询ID: "+id,null);
}
}
- 启动测试:
- http://eureka7002.com:7001/
- http://eureka7002.com:7002/
- http://localhost:8001/payment/get/4
- http://localhost:8002/payment/get/4
- http://localhost/consumer/payment/get/4
- 80的访问需要加上@LoadBalanced注解,否则会报错,因为服务不知道调用哪个
- http://localhost/consumer/payment/get/4
- 修改服务主机名和ip在eureka的web上显示与及鼠标一上去显示ip
- 比如修改payment模块 :
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 单击版
# defaultZone: http://localhost:7001/eureka
# 集群版
defaultZone: http://eureka7001.com:7001//eureka,http://eureka7002.com:7002//eureka
instance: #新加的
instance-id: payment8002 #新加的
prefer-ip-address: true #新加的
- 点击即可跳转到安全监控信息
- eureka服务发现:(向外暴露微服务信息)
- 向外暴露接口
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/payment/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();//拿到所有的注册信息
for (String element : services) {
log.info("*****element: " + element);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-P服务名所有的注册信息
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return this.discoveryClient;
}
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 //主启动类
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
-
http://localhost:8001/payment/discovery
- Eureka自我保护(好死不如赖活着)
一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存
- Eureka自我保护(好死不如赖活着)
- 关闭7001的自我保护机制(改回单击模式玩)
#cloud-eureka-server7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# 单击版
defaultZone: http://eureka7001.com:7001//eureka
# 集群版 集群指向其它eureka
# defaultZone: http://eureka7002.com:7002//eureka
server:
#关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000
#cloud-provider-payment8001
instance:
instance-id: payment8001
prefer-ip-address: true
#客户端向服务端发送的时间间隔单位为秒(默认30秒)
lease-renewal-interval-in-seconds: 1
# 服务端在收到最后一次心跳所等的上限时间,超时剔除服务(默认90秒)
lease-expiration-duration-in-seconds: 2
此时启动erueka和payment8001.此时如果直接关闭了payment8001,那么erueka会直接删除其注册信息
Zookeeper服务注册与发现:
- Zookeeper替换Eureka
- Zookeeper是什么
Zookeeper是一个分布式协调工具,可以实现注册中心功能
关闭Linux服务器防火墙后,启动Zookeeper服务器,Zookeeper服务器取代Eureka服务器,zk作为服务注册中心。
搭建Zookeeper注册中心
cloud-provider-payment8004注册进Zookeeper
- 在linux用docker启动Zookeeper
- 健cloud-provider-payment8004模块
引入依赖
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper3.5.3-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.4.9版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</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>
主启动类 ,创建order消费模块注册到zk
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
配置文件
#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 8004
#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.1.106:2181
控制层
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/zk")
public String paymentzk()
{
return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString();
}
}
- http://localhost:8004/payment/zk
- 进入docker查看
- docker ps查看zk的容器id
- docker exec -it 容器id /bin/bash进入内部
- 进入bin执行 ./zkCli.sh, 后查看服务ls /services
- 查看流水号ls /services/cloud-provider-payment
- 查看基本信息 get /services/cloud-provider-payment/3836e183-de5d-45a6-8517-3f2638cbc482
思考
服务已经成功注册到Zookeeper客户端,那么注册上去的节点被称为临时节点,还是持久节点?
首先Eureka有自我保护机制,也就是某个服务下线后,不会立刻清除该服务,而是将服务保留一段时间
Zookeeper也一样在服务下线后,会等待一段时间后,也会把该节点删除,这就说明Zookeeper上的节点是临时节点。
cloud-consumerzk-order80注册进Zookeeper
- 创建module
- 引入依赖
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.4.9版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</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>
- 创建主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class OrderZKMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderZKMain80.class, args);
}
}
3.配置yaml
server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
#注册到zookeeper地址
zookeeper:
connect-string: 192.168.1.106:2181
Consul
简介官网
Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司用Go语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能中的每一个都可以根据需要单独使用,也可以一起使用构建全方位的服务网路,总之Consul提供了一种完整的服务网络解决方案。
它具有很多优点,包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议,支持跨数据中心的WAN集群,提供图形化界面,跨平台,支持Linux,MAC,Windows
安装Consul
- 到https://www.consul.io/downloads.html下载
- 解压后cmd就到加压后的目录,consul --version可查看版本
- consul agent -dev启动访问即可----http://localhost:8500/ui/dc1/services
创建cloud-providerconsul-payment8006模块
- 引入依赖
<dependencies>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-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>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
- 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class, args);
}
}
- 配置文件
###consul服务端口号
server:
port: 8006
spring:
application:
name: consul-provider-payment
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
- controller
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/consul")
public String paymentConsul()
{
return "springcloud with consul: "+serverPort+"\t "+ UUID.randomUUID().toString();
}
}
- 测试访问-----http://localhost:8500/ui/dc1/services(下图看出已经注册进去)
http://localhost:8006/payment/consul
创建cloud-consumerconsul-order80模块
- 引入依赖
<dependencies>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-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>
- 配置yaml
###consul服务端口号 cloud-consumerconsul-order80
server:
port: 80
spring:
application:
name: cloud-consumer-order
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
- 主启动类
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class OrderConsulMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
- config
package com.atguigu.springcloud.config;
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
- controller
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class OrderConsulController
{
public static final String INVOKE_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping(value = "/consumer/payment/consul")
public String paymentInfo()
{
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
return result;
}
}
- 测试,像cloud-consumerconsul-order80一样测试即可
http://localhost/consumer/payment/consul
http://localhost:8500/ui/dc1/services
总结
组件名 | 语言 | 健康检查 | 对外暴露接口 | CAP | Spring Clou集成 |
---|---|---|---|---|---|
Eureka | Java | 可配支持 | HTTP | AP | 已集成 |
Consul | Go | 支持 | HTTP/DNS | CP | 已集成 |
Zookeeper | Java | 支持 | 客户端 | CP | 已集成 |
CAP理论
Availability:高可用
Consistency:强一致性
Partition Tolerance:分区容错性
CAP理论关注粒度是数据,而不是整体系统设计的策略
因此现在的微服务架构要么是 CP 要么是 AP,也就是P一定需要保证,最多只能较好的同时满足两个
CAP理论的核心:一个分布式系统不可能同时很好的满足:一致性,可用性和分区容错性这个三个需求
因此,根据CAP原理将NoSQL数据库分成了满足CA原则,满足CP原则,满足AP的三大类
- CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太满足
- CP:满足一致性,分区容错性,通常性能不是特别高
- AP:满足可用性,分区容错性,通常对一致性要求低一些
部分情况下,我们对数据一致性的要求没有这么高,比如蘑菇博客的点赞和浏览记录,都是每隔一段时间才写入数据库的。
AP架构
Eureka是AP架构
因为同步原因出现问题,而造成数据没有一致性
当出现网络分区后,为了保证高可用,系统B可以返回旧值,保证系统的可用性
结论:违背了一致性C的要求,只满足可用性和分区容错性,即AP
CP架构
Zookeeper和Consul是CP架构
当出现网络分区后,为了保证一致性,就必须拒绝请求,否者无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错性,即CP
Ribbon实现负载均衡
Ribbon目前已经进入了维护模式,但是目前主流还是使用Ribbon
Spring Cloud想通过LoadBalancer用于替换Ribbon
概念
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端,负载均衡的工具
简单的说,Ribbon是NetFlix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供了一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
LB负载均衡是什么
Load Balance,简单来说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。
- 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡
Nginx是服务器负载均衡,客户端所有的请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后,缓存到JVM本地,从而在本地实现RPC远程调用的技术。
一句话就是:RIbbon = 负载均衡 + RestTemplate调用
Ribbon工作原理
Ribbon其实就是一个软负载均衡的客户端组件,它可以和其它所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。
Ribbon在工作时分成两步
- 首先先选择EurekaServer,它优先选择在同一个区域内负载较少的Server
- 再根据用户的指定的策略,从Server取到服务注册列表中选择一个地址
- 其中Ribbon提供了多种策略:比如轮询,随机和根据响应时间加权
引入Ribbon
新版的Eureka已经默认引入Ribbon了,不需要额外引入
<!--Eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
RestTemplate–官网
主要方法为:
- reseTemplate.getForObject
- reseTemplate.posttForObject
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
@GetMapping("/consumer/payment/getForEntity/{id}") //与上一接口一样效果
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
if (entity.getStatusCode().is2xxSuccessful()) {
// entity.getHeaders();entity.getStatusCodeValue();
return entity.getBody();
} else {
return new CommonResult<>(444, "操作失败");
}
Ribbon核心组件IRule
Ribbon默认是使用轮询作为负载均衡算法
IRule根据特定算法从服务列表中选取一个要访问的服务,IRule是一个接口
public interface IRule {
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
然后对该接口,进行特定的实现
负载均衡算法
IRule的实现主要有以下七种
- RoundRobinRule:轮询
- RandomRule:随机
- RetryRUle:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用服务
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择的权重越大,越容易被选择
- BestAvailableRule:会先过滤掉由于多次访问故障而处于短路跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,在选择并发较小的实例
- ZoneAvoidanceRule:默认规则,符合判断server所在区域的性能和server的可用性选择服务器
默认负载均衡算法替换
官网警告:自定义的配置类不能放在@ComponentScanner所扫描的当前包下以及子包下,否者我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。eg:
- 然后我们创建自定义Rule接口
package com.atguigu.myrule;
@Configuration
public class MySelfRule
{
@Bean
public IRule myRule()
{
return new RandomRule();//定义为随机
}
}
- 主启动类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
手写Ribbon负载均衡算法
原理
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启后rest接口计数从1开始。
假设现在有2台机器,同时 List = 2 instance(也就是服务注册列表中,有两台)
1 % 2 = 1 -> index = list.get(1)
2 % 2 = 0 -> index = list.get(0)
3 % 2 = 1 -> index = list.get(1)
....
这就是轮询的原理,即
我们查看RandomRule的源码发现,其实内部就是利用的取余的技术,同时为了保证同步机制,还是使用了AtomicInteger原子整型类
public class RandomRule extends AbstractLoadBalancerRule {
public RandomRule() {
}
@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
Server server = null;
while(server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int index = this.chooseRandomInt(serverCount);
server = (Server)upList.get(index);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}
server = null;
Thread.yield();
}
}
return server;
}
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
RoundRobinRule
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;
}
手写负载均衡算法
原理 + JUC(CAS+自旋锁)
- 在cloud-provider-payment8001和cloud-provider-payment8002的controller加上:
@GetMapping(value = "/payment/lb")
public String getPaymentLB()
{
return serverPort;
}
- 首先需要在RestTemplate的配置上将 @LoadBalanced注解删除
- 编写接口
package com.atguigu.springclound.lb;
public interface LoadBalancer {
// 获取注册的一个实例
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
- 编写实现类
package com.atguigu.springclound.lb;
@Component
public class MyLB implements LoadBalancer
{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement()
{
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("*****第几次访问,次数next: "+next);
return next;
}
//负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances)
{
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
- 在com.atguigu.springclound.controller.OrderController注入LoadBalancer
@Resource
private LoadBalancer loadBalancer;
5.继续编写com.atguigu.springclound.controller.OrderControlle
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB()
{
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() <= 0)
{
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
- 测试即可:(结果即是轮询)
OpenFeign实现服务调用
关于Feign的停更,目前已经使用OpenFeign进行替换
概述
Feign是一个声明式WebService客户端。使用Feign能让编写WebService客户端更加简单。OpenFeign->Github
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring Cloud对feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
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的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等,OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方法生产实现类,实现类中做均衡并调用其它服务 |
spring-cloud-starter-feign | spring-cloud-starter-openfeign |
OpenFeign使用步骤
1.引入依赖
<!--openfeign-->
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--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>
<!--一般基础通用配置-->
<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>
2.配置yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
3.主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
4.service
package com.atguigu.springcloud.service;
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
5.controller
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
return paymentFeignService.getPaymentById(id);
}
}
6.测试
- 先启动2个Eureka集群7001、7002
- 在启动俩个微服务8001、8002
- 启动OpenFegn
- http://localhost/consumer/payment/get/4
图解
OpenFeign的超时控制
服务提供者需要超过3秒才能返回数据,但是服务调用者默认只等待1秒,这就会出现超时问题。
制造超时效果
1. cloud-provider-payment8001
//package com.atguigu.springcloud.controller;
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout()
{
// 业务逻辑处理正确,但是需要耗费3秒钟
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return serverPort;
}
2. cloud-consumer-feign-order80
//package com.atguigu.springcloud.service;
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
//package com.atguigu.springcloud.controller;
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout()
{
// OpenFeign客户端一般默认等待1秒钟
return paymentFeignService.paymentFeignTimeout();
}
3.测试
http://localhost/consumer/payment/feign/timeout
这是因为默认Feign客户端只等待一秒钟,但是服务端处理需要超过3秒钟,导致Feign客户端不想等待了,直接返回报错,这个时候,消费方的OpenFeign就需要增大超时时间
cloud-consumer-feign-order80中的yml设置等待时间
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中定义的信息之外,还有请求和相应的正文及元数据
controller
package com.atguigu.springcloud.config;
//cloud-consumer-feign-order80
import feign.Logger;
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
修改YML文件
logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
测试-----下面控制台输出
2020-06-28 22:44:08.295 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] ---> GET http://CLOUD-PAYMENT-SERVICE/payment/get/4 HTTP/1.1
2020-06-28 22:44:08.295 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] ---> END HTTP (0-byte body)
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] <--- HTTP/1.1 200 (6ms)
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] connection: keep-alive
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] content-type: application/json
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] date: Sun, 28 Jun 2020 14:44:08 GMT
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] keep-alive: timeout=60
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] transfer-encoding: chunked
2020-06-28 22:44:08.301 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById]
2020-06-28 22:44:08.302 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] {"code":200,"message":"查询成功,serverPort: 8001","data":{"id":4,"serial":"0028"}}
2020-06-28 22:44:08.302 DEBUG 40576 --- [p-nio-80-exec-5] c.a.s.service.PaymentFeignService : [PaymentFeignService#getPaymentById] <--- END HTTP (87-byte body)
2020-06-28 22:45:18.013 INFO 40576 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
Hystrix断路器
Hystrix官宣停更,官方推荐使用:resilence4j替换,同时国内Spring Cloud Alibaba 提出了Sentinel实现熔断和限流
概述
分布式面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败(网络卡顿,网络超时)
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 雪崩效应
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩
HyStrix的诞生
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝
),向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常
,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中蔓延,乃至雪崩。
Hystrix作用
- 服务降级
- 服务熔断
- 接近实时的监控(Hystrix Dashboard)
- 。。。。
Hystrix重要概念
服务降级
fallback,假设对方服务不可用了,那么至少需要返回一个兜底的解决方法,即向服务调用方返回一个符合预期的,可处理的备选响应
。
例如:服务繁忙,请稍后再试,不让客户端等待并立刻返回一个友好的提示,fallback
哪些情况会触发降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务熔断
break,类比保险丝达到了最大服务访问后,直接拒绝访问,拉闸断电,然后调用服务降级的方法并返回友好提示
一般过程:服务降级 -> 服务熔断 -> 恢复调用链路
服务限流
flowlimit,秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队
,一秒钟N个,有序进行
Hystrix案例
构建:cloud-provider-hystrix-payment8001
- 提示:将cloud-eureka-server7001改为单机版
1.引入依赖
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--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>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</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>
2.yml配置
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
defaultZone: http://eureka7001.com:7001/eureka
3.启动类添加Hystrix注解
package com.atguigu.springcloud;
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
4.Service层
package com.atguigu.springcloud.service;
import cn.hutool.core.util.IdUtil;
rvice
public class PaymentService
{
/**
* 正常访问,肯定OK
*/
public String paymentInfo_OK(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
public String paymentInfo_TimeOut(Integer id)
{
int timeNumber =3;
try { TimeUnit.SECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
}
5.Controller层
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class PaymentController
{
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_OK(id);
log.info("*****result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*****result: "+result);
return result;
}
}
6.测试
- http://localhost:8001/payment/hystrix/ok/4
- http://localhost:8001/payment/hystrix/timeout/4
7.高并发测试------Jmete
- 官网下载zip包,解压即可用,找到bin下的jmeter.bat启动即可
- 使用说明
以上相当于2万个请求
我们会发现当线程多的时候,会直接卡死,甚至把其它正常的接口都已经拖累
这是因为我们使用20000个线程去访问那个延时的接口,这样会把该微服务的资源全部集中处理 延时接口,而导致正常的接口资源不够,出现卡顿的现象。
同时tomcat的默认工作线程数被打满,没有多余的线程来分解压力和处理。
服务消费者加入
构建:cloud-consumer-feign-hystrix-order80
1.引入依赖
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--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>
<!--一般基础通用配置-->
<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>
2.yml配置
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
3.主启动类—首先启动 hystrix,@EnableHystrix
package com.atguigu.springcloud;
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80
{
public static void main(String[] args)
{
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
4.Service层
package com.atguigu.springcloud.service;
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" )
public interface PaymentHystrixService
{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
5.Controller层
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
public class OrderHystirxController
{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
6.测试
- http://localhost/consumer/payment/hystrix/ok/4
- 压测再测
- 8001同一层次的其他接口服务被困死,因为tomcat线程里面的工作线程已经被挤占完毕,80此时调用8001,客户端访问响应缓慢,转圈圈
解决方案
原因
超时导致服务器变慢,超时不再等待
出错,宕机或者程序运行出错,出错要有兜底
解决
- 对方服务8001超时了,调用者80不能一直卡死等待,必须有服务降级
- 对方服务8001宕机了,调用者80不能一直卡死,必须有服务降级
- 对方服务8001正常,调用者自己出故障或者有自我要求(自己的等待时间小于服务提供者),自己处理降级
服务降级
使用新的注解 @HystrixCommand
同时需要在cloud-provider-hystrix-payment8001主启动类上新增:@EnableCircuiteBreaker
设置8001自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级fallback
package com.atguigu.springcloud.service;
/**
* 超时访问
*
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(Integer id) {
int timeNumber = 5;
try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace();}
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id:" + id + "\t" +
"O(∩_∩)O哈哈~ 耗时(秒)";
}
/**
* 兜底的解决方案
* @param id
* @return
*/
public String paymentInfo_TimeOutHandler(Integer id){
return "线程池:" + Thread.currentThread().getName() + " 8001系统繁忙请稍后再试!!,id:" + id + "\t"+"我哭了!!";
}
上述的方法就是当在规定的3秒内没有完成,那么就会触发服务降级,返回一个兜底的解决方案
同时不仅是超时,假设服务内的方法出现了异常,也同样会触发兜底的解决方法,例如下面的代码,我们制造出一个除数为0的异常。
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(Integer id) {
int timeNumber = 10 / 0;
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id:" + id + "\t" +
"O(∩_∩)O哈哈~ 耗时(秒)";
}
上述说的是服务提供方的降级,服务消费者也需要设置服务降级的处理保护,也就是对客户端进行保护
也就是说服务降级,既可以放在客户端,也可以放在服务端,一般而言是放在客户端进行服务降级的
首先主启动类设置:@EnableHystrix
(cloud-consumer-feign-hystrix-order80)
配置过的devtool热部署对java代码的改动明显,但是对@HystrixCommand内属性的修改建议重启微服务
然后修改yml开启hystrix
#cloud-consumer-feign-hystrix-order80
feign:
hystrix:
enabled: true
服务消费端降级
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id)
{
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
- 测试http://localhost/consumer/payment/hystrix/timeout/4
虽然服务方的业务处理逻辑需要三秒,但是服务方的峰值为5秒。但是消费方只愿等1.5秒,所以消费方调用自己的兜底方法
目前问题
目前异常处理的方法,和业务代码耦合,这就造成耦合度比较高
解决方法就是使用统一的服务降级方法
方法1:
除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = "")
,这样通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量
可以在Controller处设置 @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand // 这个方法也会走全局 fallback
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
int age = 10/0; //方法前挂了,跟后面挂了两种
return paymentHystrixService.paymentInfo_TimeOut(id);
}
//下面是全局fallback方法
public String payment_Global_FallbackMethod(){
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
- 测试:http://localhost/consumer/payment/hystrix/timeout/4
方法2:
我们现在还发现,兜底的方法 和 我们的业务代码耦合在一块比较混乱
我们可以在feign调用的时候,增加hystrix的服务降级处理的实现类,这样就可以进行解耦
格式:@FeignClient(fallback = PaymentFallbackService.class)
我们要面对的异常主要有
- 运行
- 超时
- 宕机
需要新建一个FallbackService实现类,然后通过实现类统一为feign接口里面的方法进行异常处理
feign接口
package com.atguigu.springcloud.service;
@Component
@FeignClient(value = "cloud-provider-hystrix-payment", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
/**
* 正常访问
*
* @param id
* @return
*/
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
/**
* 超时访问
*
* @param id
* @return
*/
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
实现类
package com.atguigu.springcloud.service;
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "--- PaymentFallbackService fall paymentInfo_OK vack ,/(ㄒoㄒ)/~~";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "--- PaymentFallbackService fall paymentInfo_TimeOut, /(ㄒoㄒ)/~~";
}
}
这个时候,如果我们将服务提供方进行关闭,但是我们在客户端做了服务降级处理,让客户端在服务端不可用时,也会获得提示信息,而不会挂起耗死服务器
- 关闭服务端8001
服务熔断
服务熔断也是服务降级的一个 特例
熔断概念
熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应状态
当检测到该节点微服务调用响应正常后,恢复调用链路
在Spring Cloud框架里,熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定的阈值,缺省是5秒内20次调用失败,就会启动熔断机制,熔断机制的注解还是 @HystrixCommand
来源,微服务提出者马丁福勒:详细内容
这个简单的断路器避免了在电路打开时进行保护调用,但是当情况恢复正常时需要外部干预来重置它。对于建筑物中的断路器,这是一种合理的方法,但是对于软件断路器,我们可以让断路器本身检测底层调用是否再次工作。我们可以通过在适当的间隔之后再次尝试protected调用来实现这种自重置行为,并在断路器成功时重置它
熔断器的三种状态:打开,关闭,半开
这里提出了 半开的概念,首先打开一半的,然后慢慢的进行恢复,最后在把断路器关闭
降级 -> 熔断 -> 恢复
这里我们在服务提供方 8001,增加服务熔断
这里有四个字段
// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
// 请求次数
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
// 时间窗口期/时间范文--多久恢复一>次尝试
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
// 失败率达到多少后跳闸
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
首先是是否开启熔断器,然后是在一个时间窗口内,有60%的失败,那么就启动断路器,也就是10次里面,6次失败,完整代码如下:
Service
//cloud-provider-hystrix-payment8001
package com.atguigu.springcloud.service;
/**
* 在10秒窗口期中10次请求有6次是请求失败的,断路器将起作用
* @param id
* @return
*/
@HystrixCommand(
fallbackMethod = "paymentCircuitBreaker_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")// 失败率达到多少后跳闸
}
)
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("*****id不能是负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号:" + serialNumber;
}
Controller
package com.atguigu.springcloud.controller;
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}
- localhost:8001/payment/circuit/4
- http://localhost:8001/payment/circuit/-4
当然狂发错误请求的时候,错误率到达60%以上,闸门打开,即使是正确的请求,也会调用回调,不会走主逻辑
当断路器被打开的时候,即使是正确的请求,该方法也会被断路
总结
熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达所设时钟则进入半熔断状态
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断
断路器启动条件
涉及到断路器的三个重要参数:快照时间窗,请求总阈值,错误百分比阈值
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数阈值:在快照时间窗口内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果hystrix的调用总次数不足20次,即使所有请求都超时或者其他原因失败,断路器都不会打开。
- 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,并且有15次发生了超时异常,也就是超过了50的错误百分比,在默认设定的50%阈值情况下,这时候就会将断路器打开
开启和关闭的条件
- 当满足一定阈值的时候(默认10秒内超过20个请求)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 到达以上阈值,断路器将会开启
- 当开启的时候,所有请求都不会进行转发
- 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启,重复4和5
断路器开启后
- 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback,通过断路器,实现了自动的发现错误并将降级逻辑切换为主逻辑,减少相应延迟的效果。
- 原来的主逻辑如何恢复?
- 对于这个问题,Hystrix实现了自动恢复功能,当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续保持打开状态,休眠时间窗重新计时。
服务限流
后面讲解Sentinel的时候进行说明
Hystrix工作流程
蓝色:调用路径
红色:返回路径
完整的请求路线:
- 选择一个Hystrix注册方式
- 二选一即可
- 判断缓存中是否包含需要返回的内容(如果有直接返回)
- 断路器是否为打开状态(如果是,直接跳转到8,返回)
- 断路器为健康状态,判断是否有可用资源(没有,直接跳转8)
- 构造方法和Run方法
- 将正常,超时,异常的消息发送给断路器
- 调用getFallback方法,也就是服务降级
- 无异直接返回正确结果
服务监控HystrixDashboard
概述
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形化的形式展示给用户,包括每秒执行多少请求,成功多少请求,失败多少,Netflix通过Hystrix-metrics-event-stream项目实现了对以上指标的监控,SpringCloud也提供了HystrixDashboard整合,对监控内容转化成可视化页面
搭建
1.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>
2.配置yml
server:
port: 9001
3.主启动类
package com.atguigu.springcloud;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001
{
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
4.访问:http://localhost:9001/hystrix
5.同时,最后我们需要注意,每个服务类想要被监控的,都需要在pom文件中,添加一下注解
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
同时在服务提供者的启动类上,需要添加以下的内容
//cloud-provider-hystrix-payment8001
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
* 此配置是为了服务监控而配置,与服务容错本身无观,springCloud 升级之后的坑
* ServletRegistrationBean因为springboot的默认路径不是/hystrix.stream
* 只要在自己的项目中配置上下面的servlet即可
* @return
*/
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean<HystrixMetricsStreamServlet> registrationBean = new ServletRegistrationBean<>(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
6.使用监控
我们需要使用当前hystrix需要监控的端口号,也就是使用 9001 去监控 8001
,即使用hystrix dashboard去监控服务提供者的端口号
然后我们运行
http://localhost:8001/payment/circuit/31
就能够发现Hystrix Dashboard能够检测到我们的请求
假设我们访问错误的方法后
http://localhost:8001/payment/circuit/-31
我们能够发现,此时断路器处于开启状态,并且错误率百分100
如何看懂图
首先是七种颜色
每个颜色都对应的一种结果
然后是里面的圆
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康程度从
绿色 < 黄色 < 橙色 <红色,递减
该实心圆除了颜色变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大,所以通过该实心圆的展示,就可以快速在大量的实例中快速发现故障实例和高压力实例
曲线:用于记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势
服务网关
前言
zuul目前已经出现了分歧,zuul 升级到 Zuul2的时候出现了内部分歧,并且导致Zuul的核心人员的离职,导致Zuul2一直跳票,等了两年,目前造成的局面是Zuul已经没人维护,Zuul2一直在开发中
目前主流的服务网关采用的是Spring Cloud 社区推出了 Gateway
概念
Zuul官网
Zuul是所有来自设备和web站点到Netflix流媒体应用程序后端的请求的前门。作为一个边缘服务应用程序,Zuul的构建是为了支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。
Gateway
Cloud全家桶有个很重要的组件就是网关,在1.X版本中都是采用Zuul网关,但在2.X版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloudGateway,一句话Gateway是原来Zuul 1.X 版本的替代品
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2 和 Project Reactor等技术。Gateway旨在提供一种简单而且有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断,限流,重试等。
Spring Cloud Gateway 是Spring Cloud的一个全新项目,作为Spring Cloud生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.X非Reactor模式的老版本,而为了提高网关的性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式
通信框架Netty
。
Spring Cloud Gateway的目标提供统一的路由方式,且基于Filter链的方式提供了网关基本的功能,例如:安全,监控、指标 和 限流。
能做啥
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
使用场景
网关可以想象成是所有服务的入口
为什么选用Gateway
目前已经有了Zuul了,为什么还要开发出Gateway呢?
一方面是因为Zuul 1.0已经进入了维护阶段,而且Gateway是Spring Cloud团队研发的,属于亲儿子
,值得信赖,并且很多功能Zuul都没有用起来,同时Gateway也非常简单便捷
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了Zuul 2.X,但是Spring Cloud没有整合计划,因为NetFlix相关组件都进入维护期,随意综合考虑Gateway是很理想的网关选择。
Gateway特性
基于Spring Framework 5,Project Reactor 和Spring boot 2.0 进行构建
- 动态路由,能匹配任何请求属性
- 可以对路由指定Predicate(断言) 和 Filter(过滤器)
- 集成Hystrix的断路器功能
- 集成Spring Cloud服务发现功能
- 易于编写Predicate 和 Filter
- 请求限流功能
- 支持路径重写
Spring Cloud Gateway 和 Zuul的区别
在Spring Cloud Gateway Finchley正式版发布之前,Spring Cloud推荐网关是NetFlix提供的Zuul
- Zuul 1.X 是一个基于阻塞IO的API Gateway
- Zuul 1.x
基于Servlet 2.5使用阻塞架构
,它不支持任何场连接,Zuul的设计模式和Nginx比较像,每次IO操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能较差。 - Zuul 2.X理念更先进,想基于Netty非阻塞和支持长连接,但Spring Cloud目前还没有整合。Zuul 2.X的性能相比于1.X有较大提升,在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
- Spring Cloud Gateway建立在Spring 5,Spring Boot 2.X之上,使用
非阻塞API
- Spring Cloud Gateway
还支持WebSocke
t,并且与Spring紧密集成拥有更好的开发体验。
Spring Cloud 中所集成的Zuul版本,采用的是Tomcat容器
,使用的还是传统的Servlet IO处理模型
Servlet的生命周期中,Servlet由Servlet Container进行生命周期管理。
Container启动时构建servlet对象,并调用servlet init()进行初始化
Container运行时接收请求,并为每个请求分配一个线程,(一般从线程池中获取空闲线程),然后调用Service
container关闭时,调用servlet destory()
销毁servlet
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入Servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下,这种网络模型是适用的,但是一旦高并发(Jmeter测试),线程数就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大),严重影响了请求的处理时间。在一些简单业务场景下,不希望为每个Request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下Servlet模型没有优势。
所以Zuul 1.X是基于Servlet之上的一种阻塞式锤模型,即Spring实现了处理所有request请求的Servlet(DispatcherServlet)并由该Servlet阻塞式处理,因此Zuul 1.X无法摆脱Servlet模型的弊端
WebFlux框架
传统的Web框架,比如Struts2,Spring MVC等都是基于Servlet API 与Servlet容器基础之上运行的,但是在Servlet 3.1之后有了异步非阻塞的支持,而WebFlux是一个典型的非阻塞异步的框架,它的核心是基于Reactor的相关API实现的,相对于传统的Web框架来说,它可以运行在如 Netty,Undertow 及支持Servlet3.1的容器上。非阻塞式 + 函数式编程(Spring5必须让你使用Java8)
Spring WebFlux是Spring 5.0引入的新的响应式框架,区别与Spring MVC,他不依赖Servlet API,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。
三大核心概念
Route 路由
路由就是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为True则匹配该路由
Predicate 断言
参考的Java8的 java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容,例如请求头和请求参数,如果请求与断言想匹配则进行路由
Filter 过滤
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
Gateway工作流程
Web请求通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行了一些精细化的控制。
Predicate就是我们的匹配条件,而Filter就可以理解为一个无所不能的拦截器,有了这两个元素,在加上目标URL,就可以实现一个具体的路由了。
客户端向Spring Cloud Gateway发出请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。
Handler在通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求前(pre)或之后(post)执行业务逻辑
。
Filter在 Pre 类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。
在 Post类型的过滤器中可以做响应内容,响应头的修改,日志的输出,流量监控等有着非常重要的作用。
Gateway的核心逻辑:路由转发 + 执行过滤链
入门配置
1. 引入依赖
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</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>
2.修改YML
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 (lb:负载均衡)
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
3.访问
在添加网关之前,我们的访问是
http://localhost:8001/payment/get/31
添加网关之后,我们的访问路径是
http://localhost:9527/payment/get/31
这么做的好处是慢慢淡化我们真实的IP端口号
4.路由匹配
5.路由配置的两种方式
- 在配置文件yml中配置
- 代码中注入RouteLocator的Bean
package com.atguigu.springcloud.config;
@Configuration
public class GateWayConfig {
// 配置了一个id为route-name的路由规则,当访问地址 http://localhost:9527/guonei时,会自动转发到
// 地址 http;//news.baidu.com/guonei
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path route atguigu",
r ->r.path("/guonei").uri("https://www.baidu.com")).build();
return routes.build();
}
}
6.通过微服务名实现动态路由
默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
首先需要开启从注册中心动态创建路由的功能,利用微服务名进行路由
yml中的配置
URL换成服务名
uri: lb://CLOUD-PAYMENT-SERVICE
Predicate的使用
概念
断言,路径相匹配的进行路由
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分
Spring Cloud Gateway包括许多内置的Route Predicate 工厂,所有这些Predicate都与Http请求的不同属性相匹配,多个Route Predicate工厂可以进行组合
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route,SpringCloudGateway包含许多内置的RoutePredicateFactores。
所有这些谓词都匹配Http请求的不同属性。多种谓词工厂可以组合,并通过逻辑 and
常用的Predicate
- After Route Predicate:在什么时间之后执行
-
Before Route Predicate:在什么时间之前执行
-
Between Route Predicate:在什么时间之间执行
-
Cookie Route Predicate:Cookie级别
常用的测试工具:
- jmeter
- postman
- curl
// curl命令进行测试,携带Cookie CMD黑窗口 curl http://localhost:9527/payment/lb --cookie "username=zzyy"
-
Header Route Predicate:携带请求头
-
Host Route Predicate:什么样的URL路径过来
-
Method Route Predicate:什么方法请求的,Post,Get
-
Path Route Predicate:请求什么路径
- Path=/api-web/**
-
Query Route Predicate:带有什么参数的
ALLyml
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 (lb:负载均衡)
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] #设置时间后启动路由
#- Before=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] #设置时间前启动路由
#- Cookie=username,zzyy #需要带着Cokkie访问
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#-Host=**.atguigu.com #匹配该规则才能访问
#- Method=Get #设置请求方式 以该方式才能访问
#- Query=username,\d #必须要收参数名username并且值为整数才能访问
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
Filter的使用
概念
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用
Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生的
Spring Cloud Gateway Filter
生命周期:only Two:pre,Post
种类:Only Two
GatewayFilter
GlobalFilter
自定义过滤器
主要作用:
- 全局日志记录
- 统一网关鉴权
需要实现接口:implements GlobalFilter, Ordered
全局过滤器代码如下:
package com.atguigu.springcloud.filter;
@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 uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null)
{
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);//设为未接受
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder()
{
return 0;
}//越小优先级越高
}
启动测试即可
访问路径必须带有uname参数,否则访问不了
分布式配置中心SpringCloudConfig
概述
面临的问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中式,动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,原来四个微服务,需要配置四个application.yml,但需要四十个微服务,那么就需要配置40份配置文件,我们需要做的就是一处配置,到处生效。
所以这个时候就需要一个统一的配置管理
是什么
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用提供了一个中心化的外部配置。
怎么玩
服务端
也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
客户端
则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用git来存储配置信息,这样有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
能做什么
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分布式部署,比如 dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取自己的信息
- 当配置发生变动时,服务不需要重启即可感知配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露:post,curl命令刷新
与Github整合部署
由于SpringCloud Config默认使用Git来存储配置文件(也有其他方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是Http/https访问的形式
Config服务端配置与测试
现在github创建一个名为sprincloud-config的仓库,后拉取到本地
构建 cloud-config-center-3344
1.引入依赖
<dependencies>
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>
2.配置文件
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://github.com/wang-jian-yu/sprincloud-config.git #GitHub上面的git仓库名字
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
3.主启动类
package com.atguigu.springcloud;//服务端
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344
{
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
4.测试读取
http://localhost:3344/master/config-dev.yml
配置读取规则
- /{label}/{application}-{profile}.yml (分支名/服务名-xxx.yml)
- http://config-3344.com:3344/master/config-de v.yml(如上图)
- /{application}-{profile}.yml
- http://config-3344.com:3344/config-dev.yml:如果不写的话,默认就是从master分支上找
- /{application}-{profile}[/{label}] 读到json字符创
参数总结
label:分支,branch
name:服务名
profiles:环境(dev/test/prod)
Config客户端配置与测试
构建cloud-config-client-3355
1.引入依赖
cloud-config-client-3355
<dependencies>
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>
2.bootstrap.yml
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
application.yml:是用户级的资源配置项
bootstrap.yml:是系统级别的,优先级更加高
Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。
Bootstrap属性有高优先级,默认情况下,他们不会被本地配置覆盖,Bootstrap context 和 Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context 和 Application Context配置的分离。
要将客户端Client模块下的Application.yml文件改成bootstrap.yml这是很关键的,因为bootstrap.yml是比application.yml先加载的,bootstrap.yml优先级高于application.yml
3.主启动类
package com.atguigu.springcloud;
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355
{
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
4.controller
package com.atguigu.springcloud.controller;
@RestController
public class ConfigClientController
{
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo()
{
return configInfo;
}
}
5.测试
启动7001、3344、3355
- 3344自测通过http://localhost:3344/master/config-prod.yml
- http://localhost:3355/configInfo
存在的问题
分布式配置的动态刷新问题?
- Linux运维修改Github上的配置文件内容做调整
- 刷新3344,发现ConfigServer配置中心立刻响应
- 刷新3355,发现ConfigClient客户端没有任何响应
- 3355没有变化除非自己重启或者重启加载
- 难道每次运维修改后,都需要重启?
相当于直接修改Github上的配置文件,配置不会改变,这个时候就存在了分布式配置的动态刷新问题
Config客户端之动态刷新
为了避免每次更新配置都要重启客户端微服务3355
引入了动态刷新
- 引入actuator监控依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 在3355添加配置
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
3.在controller加上@RefreshScope
注解
4. 修改github的信息,刷新3344生效,刷新3355仍然无果。
用cmd发请求curl -X POST "http://localhost:3355/actuator/refresh"激活再试,然后就能够生效了,成功刷新了配置,避免了服务重启
5.这个方案存在问题:
- 假设有多个微服务客户端 3355/ 3366 / 3377
- 每个微服务都要执行一次post请求,手动刷新?
- 可否广播,一次通知,处处生效?
- …
目前来说,暂时做不到这个,所以才用了下面的内容,即Spring Cloud Bus 消息总线
消息总线SpringCloudBUS
消息总线一般是配合SpringCloudConfig一起使用的
概述
分布式自动刷新配置功能,SpringCloudBus配合SpringCloudConfig使用可以实现配置的动态刷新
Bus支持两种消息代理:RabbitMQ和Kafka
SpringCloudBus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能
。
SpringCloudBus能管理和传播分布式系统的消息,就像一个分布式执行器,可用于广播状态更改,事件推送等,也可以当做微服务的通信通道。
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理
来构建一个共用的消息主题
,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以被称为消息总线
。在总线上的各个实例,都可以方便的广播一些需要让其它连接在该主题上的实例都知道的消息。
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus),但一个服务刷新数据的时候,它会被这个消息放到Topic中,这样其它监听同一个Topic的服务
就能够得到通知,然后去更新自身的配置
- 此处用docker安装的
- RabbitMQ环境配置Windows配置RabbitMQ
- 访问: http://192.168.1.101:15672
通过topic进行广播通知
创建与3355一样的cloud-config-client-3366
SpringCloudBus动态刷新全局广播
服务端添加消息总线
配置
首先需要搭建好rabbitmq环境
- 然后各个引入RabbitMQ依赖
<!--添加消息总线Rabbitmq支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 增加yml中的rabbitmq配置:3344
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://github.com/wang-jian-yu/sprincloud-config.git #GitHub上面的git仓库名字
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#rabbitmq相关配置
rabbitmq:
host: 192.168.43.209
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
##rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
客户端添加消息总线
- 增加yml中的rabbitmq配置**(3355/3366)**
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
rabbitmq:
host: 192.168.43.209
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
这里的暴露端点和上面的不太一样
设计思想
- 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端配置
- 利用消息总线出发一个服务端ConfigServer的/bus/refresh断点,而刷新所有客户端的配置
- 图二的架构更加适合,图一不适合的原因有
- 图一打破了微服务的职责单一性,因为微服务本身是业务模块,他不应该承担配置刷新的之职责
- 打破了微服务各节点的对等性
- 有一定的局限性,例如,微服务在迁移时,它的网络地址常常发生变化,此时如果想要做到自动刷新,那就会增加更多的修改。
测试
当我们的服务端配置中心 和 客户端都增加完上述配置后,修改github后,我们需要做的就是手动发送一个POST请求到服务端
curl -X POST "http://localhsot:33444/actuator/bus-refresh"
执行完成后,配置中心会通过BUS消息总线,发送到所有的客户端,并完成配置的刷新操作。
完成了一次修改,广播通知,处处生效的效果
SpringCloudBus动态刷新定点通知
就是我想通知的目标是有差异化,有些客户端需要通过,有些不通知,也就是10个客户端,我只通知1个
简单一句话,就是指定某一个实例生效而不是全部
公式:http://localhost:配置中心端口/actuator/bus-refresh/{destination}
/bus/refresh
请求不再发送到具体的服务实例上,而是发送给config server并通过destination参数类指定需要更新配置的服务或实例。
案例
以刷新运行在3355端口上的config-client为例,只通知3355,不通知3366,可以使用下面命令
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
SpringCloud Stream 消息驱动
为什么引入消息驱动?
首先看到消息驱动,我们会想到,消息中间件
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka
存在的问题就是,中台和后台 可能存在两种MQ,那么他们之间的实现都是不一样的,这样会导致多种问题出现,而且上述我们也看到了,目前主流的MQ有四种,我们不可能每个都去学习
这个时候的痛点就是:有没有一种新的技术诞生,让我们不在关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。
这个时候,SpringCloudStream就运营而生,解决的痛点就是屏蔽了消息中间件的底层的细节差异,我们操作Stream就可以操作各种消息中间件了,从而降低开发人员的开发成本。
消息驱动概述
是什么—>官网、官网手册、Spring Cloud Stream中文指导手册
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
这就有点像Hibernate,它同时支持多种数据库,同时还提供了Hibernate Session的语法,也就是HQL语句,这样屏蔽了SQL具体实现细节,我们只需要操作HQL语句,就能够操作不同的数据库。
什么是SpringCloudStream
官方定义 SpringCloudStream是一个构件消息驱动微服务的框架
应用程序通过inputs
或者outputs
来与SpringCloudStream中binder对象(绑定器)交互。
通过我们配置来binding(绑定),而SpringCloudStream的binder对象负责与消息中间件交互
所以,我们只需要搞清楚如何与SpringCloudStream交互,就可以方便的使用消息驱动的方式。
通过使用SpringIntegration来连接消息代理中间件以实现消息事件驱动。
SpringCloudStream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅,消费组,分区的三个核心概念
目前仅支持RabbitMQ 和 Kafka
SpringCloudStrem设计思想
标准MQ
- 生产者/消费者之间靠消息媒介传递消息内容:Message
- 消息必须走特定的通道:Channel
- 消息通道里的消息如何被消费呢,谁负责收发处理
- 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅
为什么用SpringCloudStream
RabbitMQ和Kafka,由于这两个消息中间件的架构上肯定不同
比如像RabbitMQ有exchange,kafka有Tpic和Partitions分区
这些中间件的差异导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是灾难性的,一大堆东西都要推到重新做
,因为它根我们的系统耦合了,这时候SpringCloudStream给我们提供了一种解耦的方式
这个时候,我们就需要一个绑定器,可以想成是翻译官,用于实现两种消息之间的转换
SpringCloudStream为什么能屏蔽底层差异
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行消息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。
通过定义绑定器作为中间件,完美的实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要在考虑各种不同消息中间件的实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离
。
Binder
- input:对应消费者
- output:对应生产者
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解耦,服务可以关注更多的自己的业务流程。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播
,在RabbitMQ中就是Exchange
,在Kafka中就是Topic
Stream标准流程套路
我们的消息生产者和消费者只和Stream交互
- Binder:很方便的连接中间件,屏蔽差异
- Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
- Source和Sink:简单的可以理解为参照对象是SpringCloudStream自身,从Stream发布消息就是输出,接受消息就是输入。
编码中的注解
前提是已经安装好了RabbitMQ
- cloud-stream-rabbitmq-procider8801,作为消息生产者进行发消息模块
- cloud-stream-rabbitmq-procider8802,消息接收模块
- cloud-stream-rabbitmq-procider8803,消息接收模块
消息驱动之生产者
1.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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-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>
2.yml配置
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.1.106
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地址
3.主启动类
package com.atguigu.springcloud;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class StreamMQMain8801
{
public static void main(String[] args)
{
SpringApplication.run(StreamMQMain8801.class,args);
}
}
4.service
接口
package com.atguigu.springcloud.service;
public interface IMessageProvider
{
public String send();//发送
}
实现类
package com.atguigu.springcloud.service.impl;
import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.messaging.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
import javax.annotation.Resource;
import org.springframework.cloud.stream.messaging.Source;
import javax.annotation.Resource;
@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 serial;
}
}
5.controller
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.IMessageProvider;
import javax.annotation.Resource;
@RestController
public class SendMessageController
{
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage()
{
return messageProvider.send();
}
}
6.测试
我们进入RabbitAdmin页面 http://192.168.1.106:15672
会发现它已经成功创建了一个studyExchange的交换机,这个就是我们上面配置的
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设为text/plain
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
以后就会通过这个交换机进行消息的消费
我们运行下列代码,进行测试消息发送 http://localhost:8801/sendMessage
能够发现消息已经成功被RabbitMQ捕获,这个时候就完成了消息的发送
消息驱动之消费者
1.引入依赖
<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>
2.修改yml配置
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.1.106
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: atguiguB
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地址
3.主启动与8801一样
4.controller
package com.atguigu.springcloud.controller;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import javax.annotation.Resource;
@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 port: "+serverPort);
}
}
5.测试
- 刷5次
http://localhost:8801/sendMessage
- 8801控制台
- 8802控制台
- RabbitMQ流量波峰
分组消费
我们再 创建一个8803的消费者服务,需要启动的服务
- RabbitMQ:消息中间件
- 7001:服务注册
- 8801:消息生产
- 8802:消息消费
- 8803:消息消费
运行后有两个问题
- 有重复消费问题
- 消息持久化问题
消费
目前8802 、8803同时都收到了,存在重复消费的问题
如何解决:使用分组和持久化属性 group来解决
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到
,那么就会造成数据错误,我们得避免这种情况,这时我们就可以使用Stream中的消息分组来解决
。
注意:在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只能被其中一个消费一次
不同组是可以全面消费的(重复消费)
同一组会发生竞争关系,只能其中一个可以消费
分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,这里部署了8802 8803
多数情况下,生产者发送消息给某个具体微服务时,只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况,为了解决这个情况,在SpringCloudStream中,就提供了 消费组 的概念
分组
原理
微服务应用放置于同一个group中
,就能够保证
消息只会被其中一个应用消费一次,不同的组是可以消费的,同一组内会发生竞争关系,只有其中一个可以被消费。
我们将8802和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: atguiguA
引入:group: atguiguA
然后我们执行消息发送的接口:http://localhost:8801/sendMessage
我们在8801服务,同时发送了6条消息
然后看8802服务,接收到了3条
8803服务,也接收到了3条
这个时候,就通过分组,避免了消息的重复消费问题
8802、8803通过实现轮询分组,每次只有一个消费者,最后发送的消息只能够被一个接受
如果将他们的group变成两个不同的组,那么消息就会被重复消费
消息持久化
通过上面的方式,我们解决了重复消费的问题,再看看持久化
案例
- 停止8802和8803,并移除8802的group,保留8803的group
- 8801先发送4条消息到RabbitMQ
- 先启动8802,无分组属性,后台没有打出来消息
- 在启动8803,有分组属性,后台打出来MQ上的消息
这就说明消息已经被持久化了,等消费者登录后,会自动从消息队列中获取消息进行消费
SpringCloudSleuth分布式请求链路跟踪
概述
详细可以参考:使用Zipkin搭建蘑菇博客链路追踪
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
当链路特别多的时候
就需要有一个用于调用链路的监控和服务跟踪的解决方案
SpringCloudSleuth提供了一套完整的服务跟踪解决方案,在分布式系统中,提供了追踪解决方案,并且兼容支持了zipkin。
搭建
zipkin下载
SpringCloud从F版起,已经不需要自己构建Zipkin Server了,只需要调用jar包即可
运行
java -jar zipkin.jar
打开
http://127.0.0.1:9411/zipkin/
名词解释
- Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
- Span:表示调用链路来源,通俗的理解span就是一次请求信息
完整的调用链路
表示一请求链路, 一条链路通过Trace ID唯一标识,Span标识发起请求信息,各span通过parent id关联起来。
一条链路通过Trace Id唯一标识,Span表示发起的请求信息,各span通过parent id关联起来
整个链路的依赖关系如下:
引入依赖
cloud-provider-payment8001、cloud-consumer-order80
<!--cloud-provider-payment8001、cloud-consumer-order80-->
<!--链路监控包含sleuth+zipkin cloud-consumer-order80-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
修改yml
#cloud-provider-payment8001、cloud-consumer-order80
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://127.0.0.1:9411
sleuth:
sampler:
# 采集率介于0到1之间,1表示全部采集
probability: 1
controller添加
//cloud-provider-payment8001
@GetMapping("/payment/zipkin")
public String paymentZipkin()
{
return "hi ,i'am paymentzipkin server fall back,welcome to atguigu,O(∩_∩)O哈哈~";
}
//cloud-consumer-order80
// ====================> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin()
{
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
SpringCloud alibaba学习资料获取中文、英文、官网
Nacos
SpringCloud Alibaba简介
SpringCloud Alibaba诞生的主要原因是:因为Spring Cloud Netflix项目进入了维护模式
维护模式
将模块置为维护模式,意味着SpringCloud团队将不再向模块添加新功能,我们将恢复block级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request
我们打算继续支持这些模块,知道Greenwich版本被普遍采用至少一年
意味着
Spring Cloud Netflix将不再开发新的组件,我们都知道Spring Cloud项目迭代算是比较快,因此出现了很多重大issue都还来不及Fix,就又推出了另一个Release。进入维护模式意思就是以后一段时间Spring Cloud Netflix提供的服务和功能就这么多了,不在开发新的组件和功能了,以后将以维护和Merge分支Pull Request为主,新组件将以其他替代
诞生
2018.10.31,Spring Cloud Alibaba正式入驻Spring Cloud官方孵化器,并在Maven仓库发布了第一个
能做啥
- 服务限流降级:默认支持servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控
- 服务注册与发现:适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新
- 消息驱动能力:基于Spring Cloud Stream (内部用RocketMQ)为微服务应用构建消息驱动能力
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务,支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级,精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务,同时提供分布式的任务执行模型,如网格任务,网格任务支持海量子任务均匀分配到所有Worker
引入依赖版本控制
<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>
怎么玩
- Sentinel:阿里巴巴开源产品,把流量作为切入点,从流量控制,熔断降级,系统负载 保护等多个维度保护系统服务的稳定性
- Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
- RocketMQ:基于Java的高性能,高吞吐量的分布式消息和流计算平台
- Dubbo:Apache Dubbo是一款高性能Java RPC框架
- Seata:一个易于使用的高性能微服务分布式事务解决方案
- Alibaba Cloud OOS:阿里云对象存储(Object Storage Service,简称OOS),是阿里云提供的海量,安全,低成本,高可靠的云存储服务,您可以在任何应用,任何时间,任何地点存储和访问任意类型的数据。
- Alibaba Cloud SchedulerX:阿里中间件团队开发的一款分布式任务调度产品,支持周期的任务与固定时间点触发
Nacos简介
Nacos服务注册和配置中心,兼顾两种
为什么叫Nacos
前四个字母分别为:Naming(服务注册) 和 Configuration(配置中心) 的前两个字母,后面的s 是 Service
是什么
一个更易于构建云原生应用的动态服务发现,配置管理和服务
Nacos:Dynamic Naming and Configuration Server
Nacos就是注册中心 + 配置中心的组合
等价于:Nacos = Eureka + Config
能干嘛
替代Eureka做服务注册中心
替代Config做服务配置中心
下载
官网:https://github.com/alibaba/nacos
nacos文档:https://nacos.io/zh-cn/docs/what-is-nacos.html
比较
Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验
安装并运行
本地需要 java8 + Maven环境
下载:地址
github经常抽风,可以使用:https://blog.csdn.net/buyaopa/article/details/104582141
解压后:运行bin目录下的:startup.cmd
打开:http://localhost:8848/nacos
结果页面
Nacos作为服务注册中心
服务提供者注册Nacos
- cloudalibaba-provider-payment9001
1.引入依赖
<dependencies>
<!--SpringCloud ailibaba nacos -->
<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>
2.修改yml
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
3.主启动类
添加 @EnableDiscoveryClient
注解
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9002.class);
}
}
业务类
package com.atguigu.springcloud.alibaba.controller;
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry ,serverPore:"+serverPort+"\t id:"+id;
}
}
启动
nacos-payment-provider已经成功注册了
这个时候 nacos服务注册中心 + 服务提供者 9001 都OK了
通过IDEA的拷贝映射
添加
-DServer.port=9003
最后能够看到两个实例
5.同9001一样建一份cloudalibaba-provider-payment9002
服务消费者注册到Nacos
Nacos天生集成了Ribbon,因此它就具备负载均衡的能力
- cloudalibaba-consumer-nacos-order83
1.引入依赖
<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.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</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>
2.修改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
3.增加配置类
因为nacos集成了Ribbon,因此需要配置RestTemplate,同时通过注解 @LoadBalanced
实现负载均衡,默认是轮询的方式
package com.atguigu.springcloud.alibaba.config;
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemple() {
return new RestTemplate();
}
}
4.业务类
package com.atguigu.springcloud.alibaba.controller;
@RestController
@Slf4j
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);
}
}
下图注册进Nacos 的
测试
http://localhost:83/consumer/payment/nacos/13
得到的结果
//负载均衡
nacos registry ,serverPore:9001 id:13
nacos registry ,serverPore:9002 id:13
我们发现只需要配置了nacos,就轻松实现负载均衡
服务中心对比
之前我们提到的注册中心对比图
但是其实Nacos不仅支持AP,而且还支持CP,它的支持模式是可以切换的,我们首先看看Spring Cloud Alibaba的全景图,
Nacos和CAP
CAP:分别是一致性,可用性,分容容忍
我们从下图能够看到,nacos不仅能够和Dubbo整合,还能和K8s,也就是偏运维的方向
Nacos支持AP和CP切换
C是指所有的节点同一时间看到的数据是一致的,而A的定义是所有的请求都会收到响应
合适选择何种模式?
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式
。当前主流的服务如Spring Cloud 和 Dubbo服务,都是适合AP模式,AP模式为了服务的可用性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式
。
CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
Nacos作为服务配置中心演示
我们将我们的配置写入Nacos,然后以Spring Cloud Config的方式,用于抓取配置
Nacos作为配置中心 - 基础配置
- cloudalibaba-config-nacos-client3377
1.引入依赖
<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>
2.修改YML
Nacos同SpringCloud Config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常运行
。
SpringBoot中配置文件的加载是存在优先级顺序的:bootstrap优先级 高于 application
application.yml配置
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
bootstrap.yml配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心
config:
server-addr: localhost:8848 # 配置中心
file-extension: yml # 这里指定的文件格式需要和nacos上新建的配置文件后缀相同,否则读不到
group: TEST_GROUP
namespace: 1bdf1418-3ed4-442c-97c1-f525b6a85b34
#匹配规则
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
# nacos-config-client-test.yaml ----> config.info
主启动类
package com.atguigu.springcloud.alibaba;
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
业务类
package com.atguigu.springcloud.alibaba.controller;
@RestController
@RefreshScope // 支持nacos的动态刷新
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return configInfo;
}
}
通过SpringCloud原生注解 @RefreshScope
实现配置自动刷新
在Nacos中添加配置信息
Nacos中匹配规则
Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
这样,就对应我们Nacos中的这样一个配置
nacos-config-client-dev.yml
配置说明
我们在Nacos中添加配置
这里需要注意的是,在config:
的后面必须加上一个空格
测试
http://localhost:3377/config/info
启动前需要在nacos客户端-配置管理下有对应的yml配置文件,然后运行cloud-config-nacos-client:3377的主启动类,调用接口查看配置信息。
启动的时候出现问题
这是因为无法读取配置所引起的,解决方案就是我们的文件名不能用 .yml 而应该是 .yaml
我们需要删除重新建立。
自带动态刷新
修改Nacos中的yaml配置文件,再次查看配置的接口,就会发现配置已经刷新了
Nacos作为配置中心 - 分类配置
从上面的配置中心 + 动态刷新 , 就相当于 有了 SpringCloud Config + Spring Cloud Bus的功能
作为后起之秀的Nacos,还具备分类配置的功能
问题
用于解决多环境多项目管理
在实际开发中,通常一个系统会准备
- dev开发环境
- test测试环境
- prod生产环境
如何保证指定环境启动时,服务能正确读取到Nacos上相应环境的配置文件呢?
同时,一个大型分布式微服务系统会有很多微服务子项目,每个微服务子项目又都会有相应的开发环境,测试环境,预发环境,正式环境,那怎么对这些微服务配置进行管理呢?
Nacos图形化界面
配置管理:
命名空间:
Namespace + Group + Data ID 三者关系
这种分类的设计思想,就类似于java里面的package名 和 类名,最外层的namespace是可以用于区分部署环境的,Group 和 DataID逻辑上区分两个目标对象
默认情况:
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
Nacos默认的命名空间是public,Namespace主要用来实现隔离
比如说我们现在有三个环境:开发,测试,生产环境,我们就可以建立三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group可以把不同微服务划分到同一个分组里面去
Service就是微服务,一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT
,Cluster是对指定微服务的一个虚拟划分。比如说为了容灾,将Service微服务分别部署在了杭州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称,还可以尽量让同一个机房的微服务相互调用,以提升性能,最后Instance,就是微服务的实例。
三种方案加载配置
DataID方案
- 指定spring.profile.active 和 配置文件的DataID来使不同环境下读取不同的配置
- 默认空间 + 默认分组 + 新建dev 和 test两个DataID
Group方案
在创建的时候,添加分组信息
然后就可以添加分组
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心
config:
server-addr: localhost:8848 # 配置中心
file-extension: yaml # 这里指定的文件格式需要和nacos上新建的配置文件后缀相同,否则读不到
group: TEST_GROUP
Namspace方案
新建一个命名空间
新建完成后,能够看到有命名空间id
创建完成后,我们会发现,多出了几个命名空间切换
同时,我们到服务列表,发现也多了命名空间的切换
下面我们就可以通过引入namespaceI,来创建到指定的命名空间下
server
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 注册中心
config:
server-addr: localhost:8848 # 配置中心
file-extension: yaml # 这里指定的文件格式需要和nacos上新建的配置文件后缀相同,否则读不到
group: DEV_GROUP
namespace: bbf379fb-f979-4eab-8947-2f38cfae6c0c
最后通过 namespace + group + DataID 形成三级分类
Nacos集群和持久化配置
官网说明
用于部署生产中的集群模式
默认Nacos使用嵌入数据库实现数据的存储,所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
- 单机模式:用于测试和单机使用
- 集群模式:用于生产环境,确保高可用
- 多集群模式:用于多数据中心场景
单机模式支持mysql
在0.7版本之前,在单机模式下nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作流程:
安装数据库,版本要求:5.6.5 +
初始化数据库,数据库初始化文件:nacos-mysql.sql
修改conf/application.properties文件,增加mysql数据源配置,目前仅支持mysql,添加mysql数据源的url,用户名和密码
再次以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql中。
Nacos持久化配置解释
Nacos默认自带的是嵌入式数据库derby
因此我们需要完成derby
到mysql
切换配置步骤
- 在nacos\conf目录下,找到SQL脚本
然后执行SQL脚本,同时修改application.properties目录
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root
修改完成后,启动nacos,可以看到是一个全新的空记录页面,以前是记录进derby
Linux版Nacos + Mysql生产环境配置
配置
预计需要:1个Nginx + 3个nacos注册中心 + 1个mysql
所有的请求过来,首先先打到nginx上
Nacos下载Linux版本
在nacos github下载:https://github.com/alibaba/nacos/releases
选择Linux版本下载
- 将下载好的
nacos-server-1.3.1.tar
上传到linux的/opt
目录 - 解压到当前目录
tar -zxvf nacos-server-1.3.1.tar.gz
- 拷贝一份
cp -r nacos /mynacos/
集群配置
如果是一个nacos:启动 8848即可
如果是多个nacos:3333,4444,5555
那么就需要修改startup.sh里面的,传入端口号
步骤:
- Linux服务器上mysql数据库配置
- application.properties配置
- Linux服务器上nacos的集群配置cluster.conf
- 梳理出3台nacos集群的不同服务端口号
- 复制出cluster.conf(备份)
hostname -i
ifconfig
- 修改
-
编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口
- /nacos/bin 目录下有startup.sh
- 平时单机版的启动,直接./startup.sh
- 但是集群启动时,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例,命令:./startup.sh -p 3333表示启动端口号为3333的nacos服务器实例,和上一步的cluster.conf配置一样。
修改启动脚本,添加P,这样能够明确nacos启动的什么脚本
while getopts ":m:f:s:c:p:" opt
p)
EMBEDDED_STORAGE=$OPTARG;;
nohup $JAVA ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 &
修改完成后,就能够使用下列命令启动集群了
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
- 查看是否3台启动
ps -ef|grep nacos|grep -v grep|wc -l
- Nginx的配置,由它作为负载均衡器
- 修改nginx的配置文件
-
作为负载均衡分流,同时upstream 支持weight
-
通过nginx访问nacos节点:
http://192.168.210.133:1111/nacos/#/login
:下图成功
-
附带配置文件
nginx.conf
worker_processes 1;
events{
worker_connections 1024;
}
http{
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream cluster{
server 127.0.0.1:3333;
server 127.0.0.1:4444;
server 127.0.0.1:5555;
}
server{
listen 1111;
server_name localhost;
location / {
proxy_pass http://cluster;
}
error_page 500 502 503 504/50x.html;
location = /50x.html{
root html;
}
}
}
cluster.conf
#2020-07-12T14:24:19.037
192.168.210.133:3333
192.168.210.133:4444
192.168.210.133:5555
startuo.sh
cygwin=false
darwin=false
os400=false
case "`uname`" in
CYGWIN*) cygwin=true;;
Darwin*) darwin=true;;
OS400*) os400=true;;
esac
error_exit ()
{
echo "ERROR: $1 !!"
exit 1
}
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/opt/taobao/java
[ ! -e "$JAVA_HOME/bin/java" ] && unset JAVA_HOME
if [ -z "$JAVA_HOME" ]; then
if $darwin; then
if [ -x '/usr/libexec/java_home' ] ; then
export JAVA_HOME=`/usr/libexec/java_home`
elif [ -d "/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home" ]; then
export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home"
fi
else
JAVA_PATH=`dirname $(readlink -f $(which javac))`
if [ "x$JAVA_PATH" != "x" ]; then
export JAVA_HOME=`dirname $JAVA_PATH 2>/dev/null`
fi
fi
if [ -z "$JAVA_HOME" ]; then
error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)! jdk8 or later is better!"
fi
fi
export SERVER="nacos-server"
export MODE="cluster"
export FUNCTION_MODE="all"
export MEMBER_LIST=""
export EMBEDDED_STORAGE=""
while getopts ":m:f:s:c:p:" opt
do
case $opt in
m)
MODE=$OPTARG;;
f)
FUNCTION_MODE=$OPTARG;;
s)
SERVER=$OPTARG;;
c)
MEMBER_LIST=$OPTARG;;
p)
SERVER_PORT=$OPTARG;;
?)
echo "Unknown parameter"
exit 1;;
esac
done
export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
export BASE_DIR=`cd $(dirname $0)/..; pwd`
export DEFAULT_SEARCH_LOCATIONS="classpath:/,classpath:/config/,file:./,file:./config/"
export CUSTOM_SEARCH_LOCATIONS=${DEFAULT_SEARCH_LOCATIONS},file:${BASE_DIR}/conf/
if [[ "${MODE}" == "standalone" ]]; then
JAVA_OPT="${JAVA_OPT} -Xms512m -Xmx512m -Xmn256m"
JAVA_OPT="${JAVA_OPT} -Dnacos.standalone=true"
else
if [[ "${EMBEDDED_STORAGE}" == "embedded" ]]; then
JAVA_OPT="${JAVA_OPT} -DembeddedStorage=true"
fi
JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/logs/java_heapdump.hprof"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"
fi
if [[ "${FUNCTION_MODE}" == "config" ]]; then
JAVA_OPT="${JAVA_OPT} -Dnacos.functionMode=config"
elif [[ "${FUNCTION_MODE}" == "naming" ]]; then
JAVA_OPT="${JAVA_OPT} -Dnacos.functionMode=naming"
fi
JAVA_OPT="${JAVA_OPT} -Dnacos.member.list=${MEMBER_LIST}"
JAVA_MAJOR_VERSION=$($JAVA -version 2>&1 | sed -E -n 's/.* version "([0-9]*).*$/\1/p')
if [[ "$JAVA_MAJOR_VERSION" -ge "9" ]] ; then
JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${BASE_DIR}/logs/nacos_gc.log:time,tags:filecount=10,filesize=102400"
else
JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${JAVA_HOME}/lib/ext"
JAVA_OPT="${JAVA_OPT} -Xloggc:${BASE_DIR}/logs/nacos_gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M"
fi
JAVA_OPT="${JAVA_OPT} -Dloader.path=${BASE_DIR}/plugins/health,${BASE_DIR}/plugins/cmdb"
JAVA_OPT="${JAVA_OPT} -Dnacos.home=${BASE_DIR}"
JAVA_OPT="${JAVA_OPT} -jar ${BASE_DIR}/target/${SERVER}.jar"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} --spring.config.location=${CUSTOM_SEARCH_LOCATIONS}"
JAVA_OPT="${JAVA_OPT} --logging.config=${BASE_DIR}/conf/nacos-logback.xml"
JAVA_OPT="${JAVA_OPT} --server.max-http-header-size=524288"
if [ ! -d "${BASE_DIR}/logs" ]; then
mkdir ${BASE_DIR}/logs
fi
echo "$JAVA ${JAVA_OPT}"
if [[ "${MODE}" == "standalone" ]]; then
echo "nacos is starting with standalone"
else
echo "nacos is starting with cluster"
fi
# check the start.out log output file
if [ ! -f "${BASE_DIR}/logs/start.out" ]; then
touch "${BASE_DIR}/logs/start.out"
fi
# start
echo "$JAVA ${JAVA_OPT}" > ${BASE_DIR}/logs/start.out 2>&1 &
nohup $JAVA -Dserver.port=${SERVER_PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 &
echo "nacos is starting,you can check the ${BASE_DIR}/logs/start.out"
微服务注册进集群中
cloudalibaba-provider-payment9002
server:
port: 9002
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: 192.168.210.133:1111 # 换成nginx的1111端口,做负债均衡
management:
endpoints:
web:
exposure:
include: '*'
总结
Nginx + 3个Nacos + mysql的集群化配置
SpringCloudAlibabaSentinel实现熔断和限流
Sentinel
官网
Github:添加链接描述
Sentinel:分布式系统的流量防卫兵,相当于Hystrix
Hystrix存在的问题
- 需要我们程序员自己手工搭建监控平台
- 没有一套web界面可以给我们进行更加细粒度化的配置,流量控制,速率控制,服务熔断,服务降级。。
这个时候Sentinel运营而生
- 单独一个组件,可以独立出来
- 直接界面化的细粒度统一配置
约定 > 配置 >编码,都可以写在代码里,但是尽量使用注解和配置代替编码
是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
主要特征
生态圈
下载
Github:https://github.com/alibaba/Sentinel/releases
安装Sentinel控制台
sentinel组件由两部分组成,后台和前台8080
Sentinel分为两部分
- 核心库(Java客户端)不依赖任何框架/库,能够运行在所有Java运行时环境,同时对Dubbo、SpringCloud等框架也有较好的支持。
- 控制台(Dashboard)基于SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器
使用 java -jar
启动,同时Sentinel默认的端口号是8080,因此不能被占用
注意,下载时候,由于Github经常抽风,因此可以使用Gitee进行下,首先先去Gitee下载源码
然后执行mvn package
进行构建
http://localhost:8080/#/login
初始化演示工程
- 启动Nacos8848成功
构建8401
引入依赖
<dependencies>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合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>
<!--日常通用jar包配置-->
<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>
修改YML
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
#默认8719,假如被占用,持续加1 ,直到找到为止
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401
{
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
增加业务类
- Controller
@RestController
@Slf4j
public class FlowLimitController
{
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
log.info(Thread.currentThread().getName()+"\t"+"...testB");
return "------testB";
}
}
启动8401微服务,查看Sentinel控制台
我们会发现Sentinel里面空空如也,什么也没有,这是因为Sentinel采用的懒加载
执行一下访问即可:
http://localhost:8401/testA
http://localhost:8401/testB
流控规则
基本介绍
字段说明
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型 / 单机阈值
- QPS:(每秒钟的请求数量):但调用该API的QPS达到阈值的时候,进行限流
- 线程数:当调用该API的线程数达到阈值的时候,进行限流
- 是否集群:不需要集群
- 流控模式
- 直接:api都达到限流条件时,直接限流
- 关联:当关联的资源达到阈值,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】
- 流控效果
- 快速失败:直接失败,抛异常
- Warm UP:根据codeFactory(冷加载因子,默认3),从阈值/CodeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置QPS,否则无效
流控模式
直接(默认)
我们给testA增加流控
然后我们请求 http://localhost:8401/testA
,就会出现失败,被限流,快速失败
思考:
直接调用的是默认报错信息,能否有我们的后续处理,比如更加友好的提示,类似有hystrix的fallback方法
线程数
这里的线程数表示一次只有一个线程进行业务请求,当前出现请求无法响应的时候,会直接报错,例如,在方法的内部增加一个睡眠,那么后面来的就会失败
@GetMapping("/testD")
public String testD()
{
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return "------testD";
}
关联
当关联的资源达到阈值时,就限流自己
当与A关联的资源B达到阈值后,就限流A自己,B惹事,A挂了
场景:支付接口达到阈值后,就限流下订单的接口
设置:
当关联资源 /testB的QPS达到阈值超过1时,就限流/testA的Rest访问地址,当关联资源达到阈值后,限制配置好的资源名
这个使用我们利用postman模拟并发密集访问testB
首先我们需要使用postman,创建一个请求
同时将请求保存在 Collection中
然后点击箭头,选中接口,选择run
点击运行,大批量线程高并发访问B,导致A失效了,同时我们点击访问 http://localhost:8401/testA
,结果发现,我们的A已经挂了
在测试A接口
这就是我们的关联限流
链路
多个请求调用了同一个微服务
流控效果
直接
快速失败,默认的流控处理
- 直接失败,抛出异常:Blocked by Sentinel(Flow limiting)
预热
系统最怕的就是出现,平时访问是0,然后突然一瞬间来了10W的QPS
公式:阈值 除以 clodFactor(默认值为3),经过预热时长后,才会达到阈值
Warm Up方式,即预热/冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能会瞬间把系统压垮。通过冷启动,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值,给冷系统一个预热的时间,避免冷系统被压垮。通常冷启动的过程系统允许的QPS曲线如下图所示
默认clodFactor为3,即请求QPS从threshold / 3开始,经预热时长逐渐提升至设定的QPS阈值
假设这个系统的QPS是10,那么最开始系统能够接受的 QPS = 10 / 3 = 3,然后从3逐渐在5秒内提升到10
应用场景:
秒杀系统在开启的瞬间,会有很多流量上来,很可能把系统打死,预热的方式就是为了保护系统,可能慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
源码:
排队等待
大家均速排队,让请求以均匀的速度通过,阈值类型必须设置成QPS,否则无效
均速排队方式必须严格控制请求通过的间隔时间,也即让请求以匀速的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列,想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒处于空闲状态,我们系统系统能够接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
设置含义:/testA 每秒1次请求,超过的话,就排队等待,等待时间超过20000毫秒
- 用postman测试(collection)下图可见只能1秒一个
降级规则
名词介绍
-
RT(平均响应时间,秒级)
- 平均响应时间,超过阈值 且 时间窗口内通过的请求 >= 5,两个条件同时满足后出发降级
- 窗口期过后,关闭断路器
- RT最大4900(更大的需要通过 -Dcsp.sentinel.staticstic.max.rt=XXXXX才能生效)
-
异常比例(秒级)
- QPA >= 5 且异常比例(秒级)超过阈值时,触发降级;时间窗口结束后,关闭降级
-
异常数(分钟级)
- 异常数(分钟统计)超过阈值时,触发降级,时间窗口结束后,关闭降级
概念
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都进行自动熔断(默认行为是抛出DegradeException)
Sentinel的断路器是没有半开状态
半开的状态,系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用,具体可以参考hystrix
降级策略实战
RT
平均响应时间 (DEGRADE_GRADE_RT
):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count
,以 ms 为单位),那么在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException
)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx
来配置。
代码测试
package com.atguigu.springcloud.alibaba.controller;
@GetMapping("/testD")
public String testD()
{
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
log.info("testD 异常比例");
return "------testD";
}
然后使用Jmeter压力测试工具进行测试
按照上述操作,永远1秒种打进来10个线程,大于5个了,调用tesetD,我们希望200毫秒内处理完本次任务,如果200毫秒没有处理完,在未来的1秒的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电
Blocked by Sentinel (flow limiting)
后续我们停止使用jmeter,没有那么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK
异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO
):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule
中的 count
)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
- 代码
package com.atguigu.springcloud.alibaba.controller;
@GetMapping("/testD")
public String testD()
{
// try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
// log.info("testD 测试RT");
log.info("testD 异常比例");
int age = 10/0;
return "------testD";
}
单独访问一次,必然来一次报错一次,开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了,断路器开启(保险丝跳闸),微服务不可用,不在报错,而是服务降级了
设置3秒内,如果请求百分50出错,那么就会熔断
我们用jmeter每秒发送10次请求,3秒后,再次调用 localhost:8401/testD
出现服务降级
异常数
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT
):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow
小于 60s,则结束熔断状态后仍可能再进入熔断状态
时间窗口一定要大于等于60秒
异常数是按分钟来统计的
- 代码
@GetMapping("/testE")
public String testE()
{
log.info("testE 测试异常数");
int age = 10/0;
return "------testE 测试异常数";
}
- 下面设置是,一分钟内出现5次,则熔断
首先我们再次访问 http://localhost:8401/testE
,第一次访问绝对报错,因为除数不能为0,我们看到error窗口,但是达到5次报错后,进入熔断后的降级
Sentinel热点规则
什么是热点数据
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
兜底的方法
分为系统默认的和客户自定义的,两种,之前的case中,限流出现问题了,都用sentinel系统默认的提示:Blocked By Sentinel,我们能不能自定义,类似于hystrix,某个方法出现问题了,就找到对应的兜底降级方法。
从 @HystrixCommand
到 @SentinelResource
配置
@SentinelResource的value,就是我们的资源名,也就是对哪个方法配置热点规则
@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";
}
// 和上面的参数一样,不错需要加入 BlockException
public String deal_testHotKey (String p1, String p2, BlockException exception)
{
return "------deal_testHotKey,o(╥﹏╥)o"; // 兜底的方法
}
我们对参数0,设置热点key进行限流
配置完成后
当我们不断的请求时候,也就是以第一个参数为目标,请求接口,我们会发现多次请求后
http://localhost:8401/testHotKey?p1=a
就会出现以下的兜底错误
------deal_testHotKey,o(╥﹏╥)o
这是因为我们针对第一个参数进行了限制,当我们QPS超过1的时候,就会触发兜底的错误
假设我们请求的接口是:http://localhost:8401/testHotKey?p2=a
,我们会发现他就没有进行限流
参数例外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后,马上被限流
- 普通:超过一秒1个后,达到阈值1后马上被限流
- 我们期望p1参数当它达到某个特殊值时,它的限流值和平时不一样
- 特例:假设当p1的值等于5时,它的阈值可以达到200
- 一句话说:当key为特殊值的时候,不被限制
平时的时候,参数1的QPS是1,超过的时候被限流,但是有特殊值,比如5,那么它的阈值就是200
我们通过 http://localhost:8401/testHotKey?p1=5
一直刷新,发现不会触发兜底的方法,这就是参数例外项
热点参数的注意点,参数必须是基本类型或者String
结语
@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理
RuntimeException,如 int a = 10/0 ; 这个是java运行时抛出的异常,RuntimeException,@SentinelResource不管
也就是说:@SentinelResource
主管配置出错,运行出错不管。
如果想要有配置出错,和运行出错的话,那么可以设置 fallback
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey", fallback = "fallBack")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2)
{
//int age = 10/0;
return "------testHotKey";
}
Sentinel系统配置
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
这样相当于设置了全局的QPS过滤
@SentinelResource注解
- 按资源名称限流 + 后续处理
- 按URL地址限流 + 后续处理
问题
- 系统默认的,没有体现我们自己的业务要求
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观
- 每个业务方法都添加一个兜底方法,那代码膨胀加剧
- 全局统一的处理方法没有体现
- 关闭8401,发现流控规则已经消失,说明这个是没有持久化
客户自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流处理逻辑
- Handler
package com.atguigu.springcloud.alibaba.myhandler;
public class CustomerBlockHandler
{
public static CommonResult handlerException(BlockException exception)
{
return new CommonResult(4444,"按客戶自定义,global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception)
{
return new CommonResult(4444,"按客戶自定义,global handlerException----2");
}
}
那么我们在使用的时候,就可以首先指定是哪个类,哪个方法
package com.atguigu.springcloud.alibaba.controller;
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler()
{
return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));
}
更多注解属性说明
所有的代码都要用try - catch - finally 进行处理
sentinel主要有三个核心API
- Sphu定义资源
- Tracer定义统计
- ContextUtil定义了上下文
服务熔断
sentinel整合Ribbon + openFeign + fallback
- 搭建 9003 和 9004 服务提供者
- 引入依赖
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</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: 9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
- 主启动
package com.atguigu.springcloud.alibaba;
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9004
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain9004.class, args);
}
}
4.Controller
package com.atguigu.springcloud.alibaba.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;
}
}
不设置任何参数
- 搭建84工程
- 引入依赖
<dependencies>
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springboot</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</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: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
- 主启动
package com.atguigu.springcloud.alibaba;
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
- config
package com.atguigu.springcloud.alibaba.config;
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
然后在使用 84作为服务消费者,当我们值使用 @SentinelResource
注解时,不添加任何参数,那么如果出错的话,是直接返回一个error页面,对前端用户非常不友好,因此我们需要配置一个兜底
的方法
package com.atguigu.springcloud.alibaba.controller;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
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
package com.atguigu.springcloud.alibaba.controller;
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
测试http://localhost:84/consumer/fallback/2
可见轮询
加入fallback后,当我们程序运行出错时,我们会有一个兜底的异常执行,但是服务限流和熔断的异常还是出现默认的
设置blockHandler
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",blockHandler = "blockHandler" ,fallback = "handlerFallback") //blockHandler只负责sentinel控制台配置违规
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;
}
//本例是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);
}
添加降级规则
连续 几次访问后:http://localhost:84/consumer/fallback/4
blockHandler和fallback一起配置
@RequestMapping("/consumer/fallback/{id}")
@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;
}
若blockHandler 和 fallback都进行了配置,则被限流降级而抛出 BlockException时,只会进入blockHandler处理逻辑
异常忽略
Feign系列
引入依赖
<!--84 -->
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改YML
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
启动类激活Feign
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
引入Feign接口
package com.atguigu.springcloud.alibaba.service;
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService
{
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
加入fallback兜底方法实现
package com.atguigu.springcloud.alibaba.service;
@Component
public class PaymentFallbackService implements PaymentService
{
@Override
public CommonResult<Payment> paymentSQL(Long id)
{
return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
Controller
package com.atguigu.springcloud.alibaba.controller;
//==================OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
return paymentService.paymentSQL(id);
}
}
测试
请求接口:http://localhost:84/consumer/paymentSQL/1
测试84调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级
我们发现过了一段时间后,会触发服务降级,返回失败的方法
熔断框架对比
Sentinel规则持久化
是什么
一旦我们重启应用,sentinel规则将会消失,生产环境需要将规则进行持久化
怎么玩
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效
解决方法
使用nacos持久化保存
引入依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
添加nacos配置
内容解析
- resource:资源名称
- limitApp:来源应用
- grade:阈值类型,0表示线程数,1表示QPS
- count:单机阈值
- strategy:流控模式,0表示直接,1表示关联,2表示链路
- controlBehavior:流控效果,0表示快速失败,1表示Warm,2表示排队等待
- clusterMode:是否集群
[
{
"resource":"/rateLimit/byUrl",
"limitApp":"default",
"grade":1,
"count":1,
"strategy":0,
"controlBehavior":0,
"clusterMode":false
}
]
这样启动的时候,调用一下接口,我们的限流规则就会重新出现~
SpringCloudAlibabaSeata处理分布式事务
基于分布式的事务管理
分布式事务
分布式之前,单机单库没有这个问题,从 1:1 -> 1:N -> N:N
跨数据库,多数据源的统一调度,就会遇到分布式事务问题
如下图,单体应用被拆分成微服务应用,原来的三个模板
被拆分成三个独立
的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
Seata简介
官方文档:点我传送
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
分布式事务处理过程的一致性ID + 三组件模型
- Transaction ID XID:全局唯一的事务ID
- 三组件的概念
- Transaction Coordinator(TC):事务协调器,维护全局事务,驱动全局事务提交或者回滚
- Transaction Manager(TM):事务管理器,控制全局事务的范围,开始全局事务提交或回滚全局事务
- Resource Manager(RM):资源管理器,控制分支事务,负责分支注册分支事务和报告
处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TM调度XID下管辖的全部分支事务完成提交或回滚请求
下载
地址:https://github.com/seata/seata/releases
下载 0.9版本完成后,修改conf目录下的file.conf配置文件
修改file.conf
首先我们需要备份原始的file.conf文件
主要修改,自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息,也就是修改存储的数据库
修改service模块
修改服务模块中的分组
vgroup_mapping.my_test_tx_group = "fsp_tx_group"
修改store模块
修改存储模块
创建一个seata数据库
在seata数据库中建表,建表语句在 seata/conf目录下的 db_store.sql
修改seata-server的registry.conf配置文件
目的是:指明注册中心为nacos,及修改nacos连接信息
然后启动nacos 和 seata-server
- 点击seata-server.bat启动,下图成功启动
怎么玩
- 本地:@Transaction
- 全局:@GlobalTransaction
Spring自带的是 @Transaction 控制本地事务
而 @GlobalTransaction控制的是全局事务
我们只需要在需要支持分布式事务的业务类上,使用该注解即可
订单/库存/账户业务微服务准备
在这之前首先需要先启动Nacos,然后启动Seata,保证两个都OK
分布式事务的业务说明
这里我们会创建三个微服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过远程调用账户服务来扣减用户账户里面的金额,最后在订单服务修改订单状态为已完成
该操作跨越了三个数据库,有两次远程调用,很明显会有分布式事务的问题。
一句话:下订单 -> 扣库存 -> 减余额
创建数据库
- seata_order:存储订单的数据库
- seata_storage:存储库存的数据库
- seata_account:存储账户信息的数据库
建库SQL
create database seata_order;
create database seata_storage;
create database seata_account;
建立业务表
- seata_order库下建立t_order表
- seata_storage库下建t_storage表
- seata_account库下建t_account表
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) 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:已完结',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`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 '剩余库存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL 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 NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
创建回滚日志表
订单 - 库存 - 账户 3个库都需要建各自的回滚日志表,目录在 db_undo_log.sql
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE `undo_log`;
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;
订单/库存/账户业务微服务准备
业务需求
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
新建Order-Module表
约定
entity,domain:相当于实体类层
vo:view object,value object
dto:前台传到后台的数据传输类
新建module2001
引入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: 123
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
增加file.conf
在resources目录下,创建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.my_test_tx_group = "fsp_tx_group"
#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"
}
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 = "123"
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"
}
}
domain
package com.atguigu.springcloud.alibaba.domain;
@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);
}
}
package com.atguigu.springcloud.alibaba.domain;
@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接口及实现
package com.atguigu.springcloud.alibaba.dao;
@Mapper
public interface OrderDao
{
//1 新建订单
void create(Order order);
//2 修改订单状态,从零改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
<!-- src\main\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接口
package com.atguigu.springcloud.alibaba.service;
public interface OrderService
{
void create(Order order);
}
StorageService的Feign接口,
package com.atguigu.springcloud.alibaba.service;
@FeignClient(value = "seata-storage-service")
public interface StorageService
{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
AccountService的Feign接口,账户接口
package com.atguigu.springcloud.alibaba.service;
@FeignClient(value = "seata-account-service")
public interface AccountService
{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
OrderServiceImpl实现类
package com.atguigu.springcloud.alibaba.service.impl;
@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("----->开始新建订单");
//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哈哈~");
}
}
业务类
package com.atguigu.springcloud.alibaba.controller;
@RestController
public class OrderController
{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
Config配置
Mybatis DataSourceProxyConfig配置,这里是使用Seata对数据源进行代理
package com.atguigu.springcloud.alibaba.config;
@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();
}
}
Mybatis配置
package com.atguigu.springcloud.alibaba.config;
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
启动类
package com.atguigu.springcloud.alibaba;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class SeataOrderMainApp2001
{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
新建Storage-Module
参考项目:seata-storage-service2002
新建账户Account-Module
参考项目:seata-account-service2003
测试
数据库初始情况
正常下单
访问
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
超时异常,没加@GlobalTransaction
我们在account-module模块,添加睡眠时间20秒,因为openFeign默认时间是1秒
出现了数据不一致的问题
故障情况
- 当库存和账户金额扣减后,订单状态并没有设置成已经完成,没有从零改成1
- 而且由于Feign的重试机制,账户余额还有可能被多次扣除
超时异常,添加@GlobalTransaction
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
rollbackFor表示,什么什么错误就会回滚
添加这个后,发现下单后的数据库并没有改变,记录都添加不进来
一部分补充
Seata
2019年1月份,蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Seata:Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
2020起始,参加工作以后用1.0以后的版本。
再看TC/TM/RM三大组件
什么是TC,TM,RM
TC:seata服务器
TM:带有@GlobalTransaction注解的方法
RM:数据库,也就是事务参与方
分布式事务的执行流程
- TM开启分布式事务(TM向TC注册全局事务记录),相当于注解
@GlobelTransaction
注解 - 按业务场景,编排数据库,服务等事务内部资源(RM向TC汇报资源准备状态)
- TM结束分布式事务,事务一阶段结束(TM通知TC提交、回滚分布式事务)
- TC汇总事务信息,决定分布式事务是提交还是回滚
- TC通知所有RM提交、回滚资源,事务二阶段结束
AT模式如何做到对业务的无侵入
默认AT模式,阿里云GTS
AT模式
前提
- 基于支持本地ACID事务的关系型数据库
- Java应用,通过JDBC访问数据库
整体机制
两阶段提交协议的演变
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段
- 提交异步化,非常快速的完成
- 回滚通过一阶段的回滚日志进行反向补偿
一阶段加载
在一阶段,Seata会拦截 业务SQL
- 解析SQL语义,找到业务SQL,要更新的业务数据,在业务数据被更新前,将其保存成
before image(前置镜像)
- 执行业务SQL更新业务数据,在业务数据更新之后
- 将其保存成 after image,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
二阶段提交
二阶段如果顺利提交的话,因为业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照和行锁删除掉,完成数据清理即可
二阶段回滚
二阶段如果回滚的话,Seata就需要回滚到一阶段已经执行的 业务SQL,还原业务数据
回滚方式便是用 before image 还原业务数据,但是在还原前要首先校验脏写,对比数据库当前业务数据 和after image,如果两份数据完全一致,没有脏写,可以还原业务数据,如果不一致说明有脏读,出现脏读就需要转人工处理