微服务架构下的一整套解决方案
分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶
阿里的架构图:
京东物流的架构图:
但是随着Eureka等组件的闭源,后续的一些解决方案也有了新的替换产品
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的版本选择也不是任意的,而是应该参考官网的约束配置
地址:https://spring.io/projects/spring-cloud
版本对应:https://start.spring.io/actuator/info
建一个空工程---------------------------约定 > 配置 > 编码
Mavan选安装的3.5及以上的
<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.0modelVersion>
<groupId>com.atguigu.springbootgroupId>
<artifactId>clound2020artifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<junit.version>4.12junit.version>
<log4j.version>1.2.17log4j.version>
<lombok.version>1.16.18lombok.version>
<mysql.version>5.1.47mysql.version>
<druid.version>1.1.16druid.version>
<mybatis.spring.boot.version>1.3.0mybatis.spring.boot.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.2.2.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR1version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.spring.boot.version}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>${junit.version}version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>${log4j.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<optional>trueoptional>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
project>
* 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 namespace="com.atguigu.springcloud.dao.PaymentDao">
<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>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id=#{id};
select>
mapper>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
创建与cloud-provider-payment8001相同的entities
RestTemplate官网
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 );
}
}
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
前面我们没有服务注册中心,也可以服务间调用,为什么还要服务注册?
当服务很多时,单靠代码手动管理是很麻烦的,需要一个公共组件,统一管理多服务,包括服务是否正常运行,等
Eureka用于服务注册,目前官网已经停止更新
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
dependency>
dependencies>
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/
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);
}
}
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
1,就是pay模块启动时,注册自己,并且自身信息也放入eureka
2.order模块,首先也注册自己,放入信息,当要调用pay时,先从eureka拿到pay的调用地址
3.通过HttpClient调用
并且还会缓存一份到本地,每30秒更新一次
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/
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
@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);
}
}
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 #新加的
@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);
}
}
#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是一个分布式协调工具,可以实现注册中心功能
关闭Linux服务器防火墙后,启动Zookeeper服务器,Zookeeper服务器取代Eureka服务器,zk作为服务注册中心。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
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();
}
}
服务已经成功注册到Zookeeper客户端,那么注册上去的节点被称为临时节点,还是持久节点?
首先Eureka有自我保护机制,也就是某个服务下线后,不会立刻清除该服务,而是将服务保留一段时间
Zookeeper也一样在服务下线后,会等待一段时间后,也会把该节点删除,这就说明Zookeeper上的节点是临时节点。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
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是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司用Go语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能中的每一个都可以根据需要单独使用,也可以一起使用构建全方位的服务网路,总之Consul提供了一种完整的服务网络解决方案。
它具有很多优点,包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议,支持跨数据中心的WAN集群,提供图形化界面,跨平台,支持Linux,MAC,Windows
<dependencies>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>RELEASEversion>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>RELEASEversion>
<scope>testscope>
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}
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();
}
}
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
###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);
}
}
package com.atguigu.springcloud.config;
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
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;
}
}
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 | 已集成 |
Availability:高可用
Consistency:强一致性
Partition Tolerance:分区容错性
CAP理论关注粒度是数据,而不是整体系统设计的策略
因此现在的微服务架构要么是 CP 要么是 AP,也就是P一定需要保证,最多只能较好的同时满足两个
CAP理论的核心:一个分布式系统不可能同时很好的满足:一致性,可用性和分区容错性这个三个需求
因此,根据CAP原理将NoSQL数据库分成了满足CA原则,满足CP原则,满足AP的三大类
部分情况下,我们对数据一致性的要求没有这么高,比如蘑菇博客的点赞和浏览记录,都是每隔一段时间才写入数据库的。
Eureka是AP架构
因为同步原因出现问题,而造成数据没有一致性
当出现网络分区后,为了保证高可用,系统B可以返回旧值,保证系统的可用性
结论:违背了一致性C的要求,只满足可用性和分区容错性,即AP
Zookeeper和Consul是CP架构
当出现网络分区后,为了保证一致性,就必须拒绝请求,否者无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错性,即CP
Ribbon目前已经进入了维护模式,但是目前主流还是使用Ribbon
Spring Cloud想通过LoadBalancer用于替换Ribbon
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端,负载均衡的工具
简单的说,Ribbon是NetFlix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供了一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
Load Balance,简单来说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。
Nginx是服务器负载均衡,客户端所有的请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后,缓存到JVM本地,从而在本地实现RPC远程调用的技术。
一句话就是:RIbbon = 负载均衡 + RestTemplate调用
Ribbon其实就是一个软负载均衡的客户端组件,它可以和其它所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。
Ribbon在工作时分成两步
新版的Eureka已经默认引入Ribbon了,不需要额外引入
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
主要方法为:
@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根据特定算法从服务列表中选取一个要访问的服务,IRule是一个接口
public interface IRule {
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
然后对该接口,进行特定的实现
IRule的实现主要有以下七种
官网警告:自定义的配置类不能放在@ComponentScanner所扫描的当前包下以及子包下,否者我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。eg:
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);
}
}
负载均衡算法: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+自旋锁)
@GetMapping(value = "/payment/lb")
public String getPaymentLB()
{
return serverPort;
}
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);
}
}
@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);
}
关于Feign的停更,目前已经使用OpenFeign进行替换
Feign是一个声明式WebService客户端。使用Feign能让编写WebService客户端更加简单。OpenFeign->Github
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring Cloud对feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon + RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端来包装这些依赖,所以Feign在这个基础上做了进一步封装,由他来帮助我们定义和实现依赖服务的接口定义。在Feign的实现下,我们只需要创建一个接口,并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是微服务接口上标注一个Feign注解),即可完成服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且声明式的方法,优雅而简单的实现了服务调用。
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 |
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
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)
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);
}
}
服务提供者需要超过3秒才能返回数据,但是服务调用者默认只等待1秒,这就会出现超时问题。
//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;
}
//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();
}
http://localhost/consumer/payment/feign/timeout
这是因为默认Feign客户端只等待一秒钟,但是服务端处理需要超过3秒钟,导致Feign客户端不想等待了,直接返回报错,这个时候,消费方的OpenFeign就需要增大超时时间
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
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节,说白了就是对Feign接口的调用情况进行监控和输出。
package com.atguigu.springcloud.config;
//cloud-consumer-feign-order80
import feign.Logger;
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
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官宣停更,官方推荐使用:resilence4j替换,同时国内Spring Cloud Alibaba 提出了Sentinel实现熔断和限流
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败(网络卡顿,网络超时)
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 雪崩效应
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝
),向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常
,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中蔓延,乃至雪崩。
fallback,假设对方服务不可用了,那么至少需要返回一个兜底的解决方法,即向服务调用方返回一个符合预期的,可处理的备选响应
。
例如:服务繁忙,请稍后再试,不让客户端等待并立刻返回一个友好的提示,fallback
哪些情况会触发降级
break,类比保险丝达到了最大服务访问后,直接拒绝访问,拉闸断电,然后调用服务降级的方法并返回友好提示
一般过程:服务降级 -> 服务熔断 -> 恢复调用链路
flowlimit,秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队
,一秒钟N个,有序进行
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
package com.atguigu.springcloud;
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
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哈哈~"+" 耗时(秒): ";
}
}
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;
}
}
Jmete
我们会发现当线程多的时候,会直接卡死,甚至把其它正常的接口都已经拖累
这是因为我们使用20000个线程去访问那个延时的接口,这样会把该微服务的资源全部集中处理 延时接口,而导致正常的接口资源不够,出现卡顿的现象。
同时tomcat的默认工作线程数被打满,没有多余的线程来分解压力和处理。
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
package com.atguigu.springcloud;
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80
{
public static void main(String[] args)
{
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
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);
}
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;
}
}
超时导致服务器变慢,超时不再等待
出错,宕机或者程序运行出错,出错要有兜底
解决
使用新的注解 @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";
}
虽然服务方的业务处理逻辑需要三秒,但是服务方的峰值为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ㄒ)/~~";
}
}
方法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ㄒ)/~~";
}
}
这个时候,如果我们将服务提供方进行关闭,但是我们在客户端做了服务降级处理,让客户端在服务端不可用时,也会获得提示信息,而不会挂起耗死服务器
服务熔断也是服务降级的一个 特例
熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应状态
当检测到该节点微服务调用响应正常后,恢复调用链路
在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;
}
当然狂发错误请求的时候,错误率到达60%以上,闸门打开,即使是正确的请求,也会调用回调,不会走主逻辑
当断路器被打开的时候,即使是正确的请求,该方法也会被断路
涉及到断路器的三个重要参数:快照时间窗,请求总阈值,错误百分比阈值
开启和关闭的条件
断路器开启后
后面讲解Sentinel的时候进行说明
红色:返回路径
完整的请求路线:
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形化的形式展示给用户,包括每秒执行多少请求,成功多少请求,失败多少,Netflix通过Hystrix-metrics-event-stream项目实现了对以上指标的监控,SpringCloud也提供了HystrixDashboard整合,对监控内容转化成可视化页面
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
server:
port: 9001
package com.atguigu.springcloud;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001
{
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
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;
}
}
我们需要使用当前hystrix需要监控的端口号,也就是使用 9001 去监控 8001
,即使用hystrix dashboard去监控服务提供者的端口号
然后我们运行
http://localhost:8001/payment/circuit/31
就能够发现Hystrix Dashboard能够检测到我们的请求
http://localhost:8001/payment/circuit/-31
如何看懂图
首先是七种颜色
每个颜色都对应的一种结果
然后是里面的圆
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康程度从
绿色 < 黄色 < 橙色 <红色,递减
该实心圆除了颜色变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大,所以通过该实心圆的展示,就可以快速在大量的实例中快速发现故障实例和高压力实例
曲线:用于记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势
zuul目前已经出现了分歧,zuul 升级到 Zuul2的时候出现了内部分歧,并且导致Zuul的核心人员的离职,导致Zuul2一直跳票,等了两年,目前造成的局面是Zuul已经没人维护,Zuul2一直在开发中
目前主流的服务网关采用的是Spring Cloud 社区推出了 Gateway
Zuul是所有来自设备和web站点到Netflix流媒体应用程序后端的请求的前门。作为一个边缘服务应用程序,Zuul的构建是为了支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。
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链的方式提供了网关基本的功能,例如:安全,监控、指标 和 限流。
网关可以想象成是所有服务的入口
目前已经有了Zuul了,为什么还要开发出Gateway呢?
一方面是因为Zuul 1.0已经进入了维护阶段,而且Gateway是Spring Cloud团队研发的,属于亲儿子
,值得信赖,并且很多功能Zuul都没有用起来,同时Gateway也非常简单便捷
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了Zuul 2.X,但是Spring Cloud没有整合计划,因为NetFlix相关组件都进入维护期,随意综合考虑Gateway是很理想的网关选择。
基于Spring Framework 5,Project Reactor 和Spring boot 2.0 进行构建
在Spring Cloud Gateway Finchley正式版发布之前,Spring Cloud推荐网关是NetFlix提供的Zuul
基于Servlet 2.5使用阻塞架构
,它不支持任何场连接,Zuul的设计模式和Nginx比较像,每次IO操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能较差。非阻塞API
还支持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模型的弊端
传统的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来实现响应式流规范。
路由就是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为True则匹配该路由
参考的Java8的 java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容,例如请求头和请求参数,如果请求与断言想匹配则进行路由
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
Web请求通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行了一些精细化的控制。
Predicate就是我们的匹配条件,而Filter就可以理解为一个无所不能的拦截器,有了这两个元素,在加上目标URL,就可以实现一个具体的路由了。
客户端向Spring Cloud Gateway发出请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。
Handler在通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求前(pre)或之后(post)执行业务逻辑
。
Filter在 Pre 类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。
在 Post类型的过滤器中可以做响应内容,响应头的修改,日志的输出,流量监控等有着非常重要的作用。
Gateway的核心逻辑:路由转发 + 执行过滤链
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
在添加网关之前,我们的访问是
http://localhost:8001/payment/get/31
添加网关之后,我们的访问路径是
http://localhost:9527/payment/get/31
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();
}
}
默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
首先需要开启从注册中心动态创建路由的功能,利用微服务名进行路由
yml中的配置
URL换成服务名
uri: lb://CLOUD-PAYMENT-SERVICE
断言,路径相匹配的进行路由
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
Before Route Predicate:在什么时间之前执行
Between Route Predicate:在什么时间之间执行
Cookie Route Predicate:Cookie级别
常用的测试工具:
// 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
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用
Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生的
生命周期: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参数,否则访问不了
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中式,动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,原来四个微服务,需要配置四个application.yml,但需要四十个微服务,那么就需要配置40份配置文件,我们需要做的就是一处配置,到处生效。
所以这个时候就需要一个统一的配置管理
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用提供了一个中心化的外部配置。
服务端
也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
客户端
则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用git来存储配置信息,这样有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
由于SpringCloud Config默认使用Git来存储配置文件(也有其他方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是Http/https访问的形式
现在github创建一个名为sprincloud-config的仓库,后拉取到本地
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
package com.atguigu.springcloud;//服务端
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344
{
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
http://localhost:3344/master/config-dev.yml
label:分支,branch
name:服务名
profiles:环境(dev/test/prod)
cloud-config-client-3355
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
package com.atguigu.springcloud;
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355
{
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
package com.atguigu.springcloud.controller;
@RestController
public class ConfigClientController
{
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo()
{
return configInfo;
}
}
启动7001、3344、3355
分布式配置的动态刷新问题?
相当于直接修改Github上的配置文件,配置不会改变,这个时候就存在了分布式配置的动态刷新问题
为了避免每次更新配置都要重启客户端微服务3355
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
3.在controller加上@RefreshScope
注解
4. 修改github的信息,刷新3344生效,刷新3355仍然无果。
用cmd发请求curl -X POST "http://localhost:3355/actuator/refresh"激活再试,然后就能够生效了,成功刷新了配置,避免了服务重启
5.这个方案存在问题:
目前来说,暂时做不到这个,所以才用了下面的内容,即Spring Cloud Bus 消息总线
消息总线一般是配合SpringCloudConfig一起使用的
分布式自动刷新配置功能,SpringCloudBus配合SpringCloudConfig使用可以实现配置的动态刷新
Bus支持两种消息代理:RabbitMQ和Kafka
SpringCloudBus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能
。
SpringCloudBus能管理和传播分布式系统的消息,就像一个分布式执行器,可用于广播状态更改,事件推送等,也可以当做微服务的通信通道。
在微服务架构的系统中,通常会使用轻量级的消息代理
来构建一个共用的消息主题
,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以被称为消息总线
。在总线上的各个实例,都可以方便的广播一些需要让其它连接在该主题上的实例都知道的消息。
ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus),但一个服务刷新数据的时候,它会被这个消息放到Topic中,这样其它监听同一个Topic的服务
就能够得到通知,然后去更新自身的配置
通过topic进行广播通知
首先需要搭建好rabbitmq环境
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
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'
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: "*"
这里的暴露端点和上面的不太一样
当我们的服务端配置中心 和 客户端都增加完上述配置后,修改github后,我们需要做的就是手动发送一个POST请求到服务端
curl -X POST "http://localhsot:33444/actuator/bus-refresh"
执行完成后,配置中心会通过BUS消息总线,发送到所有的客户端,并完成配置的刷新操作。
完成了一次修改,广播通知,处处生效的效果
就是我想通知的目标是有差异化,有些客户端需要通过,有些不通知,也就是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"
首先看到消息驱动,我们会想到,消息中间件
存在的问题就是,中台和后台 可能存在两种MQ,那么他们之间的实现都是不一样的,这样会导致多种问题出现,而且上述我们也看到了,目前主流的MQ有四种,我们不可能每个都去学习
这个时候的痛点就是:有没有一种新的技术诞生,让我们不在关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。
这个时候,SpringCloudStream就运营而生,解决的痛点就是屏蔽了消息中间件的底层的细节差异,我们操作Stream就可以操作各种消息中间件了,从而降低开发人员的开发成本。
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
这就有点像Hibernate,它同时支持多种数据库,同时还提供了Hibernate Session的语法,也就是HQL语句,这样屏蔽了SQL具体实现细节,我们只需要操作HQL语句,就能够操作不同的数据库。
官方定义 SpringCloudStream是一个构件消息驱动微服务的框架
应用程序通过inputs
或者outputs
来与SpringCloudStream中binder对象(绑定器)交互。
通过我们配置来binding(绑定),而SpringCloudStream的binder对象负责与消息中间件交互
所以,我们只需要搞清楚如何与SpringCloudStream交互,就可以方便的使用消息驱动的方式。
通过使用SpringIntegration来连接消息代理中间件以实现消息事件驱动。
SpringCloudStream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅,消费组,分区的三个核心概念
目前仅支持RabbitMQ 和 Kafka
RabbitMQ和Kafka,由于这两个消息中间件的架构上肯定不同
比如像RabbitMQ有exchange,kafka有Tpic和Partitions分区
这些中间件的差异导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是灾难性的,一大堆东西都要推到重新做
,因为它根我们的系统耦合了,这时候SpringCloudStream给我们提供了一种解耦的方式
这个时候,我们就需要一个绑定器,可以想成是翻译官,用于实现两种消息之间的转换
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行消息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。
通过定义绑定器作为中间件,完美的实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要在考虑各种不同消息中间件的实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离
。
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解耦,服务可以关注更多的自己的业务流程。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播
,在RabbitMQ中就是Exchange
,在Kafka中就是Topic
我们的消息生产者和消费者只和Stream交互
前提是已经安装好了RabbitMQ
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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地址
package com.atguigu.springcloud;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class StreamMQMain8801
{
public static void main(String[] args)
{
SpringApplication.run(StreamMQMain8801.class,args);
}
}
接口
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;
}
}
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();
}
}
我们进入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捕获,这个时候就完成了消息的发送
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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地址
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);
}
}
我们再 创建一个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变成两个不同的组,那么消息就会被重复消费
通过上面的方式,我们解决了重复消费的问题,再看看持久化
这就说明消息已经被持久化了,等消费者登录后,会自动从消息队列中获取消息进行消费
详细可以参考:使用Zipkin搭建蘑菇博客链路追踪
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
当链路特别多的时候
就需要有一个用于调用链路的监控和服务跟踪的解决方案
SpringCloudSleuth提供了一套完整的服务跟踪解决方案,在分布式系统中,提供了追踪解决方案,并且兼容支持了zipkin。
SpringCloud从F版起,已经不需要自己构建Zipkin Server了,只需要调用jar包即可
java -jar zipkin.jar
http://127.0.0.1:9411/zipkin/
表示一请求链路, 一条链路通过Trace ID唯一标识,Span标识发起请求信息,各span通过parent id关联起来。
一条链路通过Trace Id唯一标识,Span表示发起的请求信息,各span通过parent id关联起来
整个链路的依赖关系如下:
cloud-provider-payment8001、cloud-consumer-order80
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
#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
//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诞生的主要原因是:因为Spring Cloud Netflix项目进入了维护模式
将模块置为维护模式,意味着SpringCloud团队将不再向模块添加新功能,我们将恢复block级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request
我们打算继续支持这些模块,知道Greenwich版本被普遍采用至少一年
Spring Cloud Netflix将不再开发新的组件,我们都知道Spring Cloud项目迭代算是比较快,因此出现了很多重大issue都还来不及Fix,就又推出了另一个Release。进入维护模式意思就是以后一段时间Spring Cloud Netflix提供的服务和功能就这么多了,不在开发新的组件和功能了,以后将以维护和Merge分支Pull Request为主,新组件将以其他替代
官网:SpringCloud Alibaba
2018.10.31,Spring Cloud Alibaba正式入驻Spring Cloud官方孵化器,并在Maven仓库发布了第一个
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
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
结果页面
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
添加 @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
最后能够看到两个实例
Nacos天生集成了Ribbon,因此它就具备负载均衡的能力
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
因为nacos集成了Ribbon,因此需要配置RestTemplate,同时通过注解 @LoadBalanced
实现负载均衡,默认是轮询的方式
package com.atguigu.springcloud.alibaba.config;
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemple() {
return new RestTemplate();
}
}
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);
}
}
测试
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的全景图,
CAP:分别是一致性,可用性,分容容忍
我们从下图能够看到,nacos不仅能够和Dubbo整合,还能和K8s,也就是偏运维的方向
C是指所有的节点同一时间看到的数据是一致的,而A的定义是所有的请求都会收到响应
合适选择何种模式?
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式
。当前主流的服务如Spring Cloud 和 Dubbo服务,都是适合AP模式,AP模式为了服务的可用性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式
。
CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
我们将我们的配置写入Nacos,然后以Spring Cloud Config的方式,用于抓取配置
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
这样,就对应我们Nacos中的这样一个配置
nacos-config-client-dev.yml
配置说明
这里需要注意的是,在config:
的后面必须加上一个空格
http://localhost:3377/config/info
启动前需要在nacos客户端-配置管理下有对应的yml配置文件,然后运行cloud-config-nacos-client:3377的主启动类,调用接口查看配置信息。
启动的时候出现问题
这是因为无法读取配置所引起的,解决方案就是我们的文件名不能用 .yml 而应该是 .yaml
我们需要删除重新建立。
修改Nacos中的yaml配置文件,再次查看配置的接口,就会发现配置已经刷新了
从上面的配置中心 + 动态刷新 , 就相当于 有了 SpringCloud Config + Spring Cloud Bus的功能
作为后起之秀的Nacos,还具备分类配置的功能
用于解决多环境多项目管理
在实际开发中,通常一个系统会准备
如何保证指定环境启动时,服务能正确读取到Nacos上相应环境的配置文件呢?
同时,一个大型分布式微服务系统会有很多微服务子项目,每个微服务子项目又都会有相应的开发环境,测试环境,预发环境,正式环境,那怎么对这些微服务配置进行管理呢?
命名空间:
这种分类的设计思想,就类似于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,就是微服务的实例。
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
新建一个命名空间
新建完成后,能够看到有命名空间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采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
在0.7版本之前,在单机模式下nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作流程:
安装数据库,版本要求:5.6.5 +
初始化数据库,数据库初始化文件:nacos-mysql.sql
修改conf/application.properties文件,增加mysql数据源配置,目前仅支持mysql,添加mysql数据源的url,用户名和密码
再次以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql中。
Nacos默认自带的是嵌入式数据库derby
因此我们需要完成derby
到mysql
切换配置步骤
然后执行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
预计需要:1个Nginx + 3个nacos注册中心 + 1个mysql
所有的请求过来,首先先打到nginx上
在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里面的,传入端口号
步骤:
编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口
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
ps -ef|grep nacos|grep -v grep|wc -l
作为负载均衡分流,同时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的集群化配置
Github:添加链接描述
Sentinel:分布式系统的流量防卫兵,相当于Hystrix
Hystrix存在的问题
这个时候Sentinel运营而生
约定 > 配置 >编码,都可以写在代码里,但是尽量使用注解和配置代替编码
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
Github:https://github.com/alibaba/Sentinel/releases
sentinel组件由两部分组成,后台和前台8080
Sentinel分为两部分
使用 java -jar
启动,同时Sentinel默认的端口号是8080,因此不能被占用
注意,下载时候,由于Github经常抽风,因此可以使用Gitee进行下,首先先去Gitee下载源码
然后执行mvn package
进行构建
http://localhost:8080/#/login
<dependencies>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>4.6.3version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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);
}
}
@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
字段说明
我们给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,创建一个请求
同时将请求保存在 Collection中
然后点击箭头,选中接口,选择run
点击运行,大批量线程高并发访问B,导致A失效了,同时我们点击访问 http://localhost:8401/testA
,结果发现,我们的A已经挂了
在测试A接口
这就是我们的关联限流
多个请求调用了同一个微服务
快速失败,默认的流控处理
系统最怕的就是出现,平时访问是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毫秒
RT(平均响应时间,秒级)
异常比例(秒级)
异常数(分钟级)
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都进行自动熔断(默认行为是抛出DegradeException)
Sentinel的断路器是没有半开状态
半开的状态,系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用,具体可以参考hystrix
平均响应时间 (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后,直接高并发发送请求,多次调用达到我们的配置条件了,断路器开启(保险丝跳闸),微服务不可用,不在报错,而是服务降级了
我们用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 测试异常数";
}
首先我们再次访问 http://localhost:8401/testE
,第一次访问绝对报错,因为除数不能为0,我们看到error窗口,但是达到5次报错后,进入熔断后的降级
Github文档传送门
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
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的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 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
。这样相当于设置了全局的QPS过滤
创建CustomerBlockHandler类用于自定义限流处理逻辑
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
sentinel整合Ribbon + openFeign + fallback
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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;
}
}
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springbootgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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);
}
}
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;
}
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后,当我们程序运行出错时,我们会有一个兜底的异常执行,但是服务限流和熔断的异常还是出现默认的
@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
@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处理逻辑
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
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
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
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);
}
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"));
}
}
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规则将会消失,生产环境需要将规则进行持久化
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效
使用nacos持久化保存
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
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的支持
[
{
"resource":"/rateLimit/byUrl",
"limitApp":"default",
"grade":1,
"count":1,
"strategy":0,
"controlBehavior":0,
"clusterMode":false
}
]
基于分布式的事务管理
分布式之前,单机单库没有这个问题,从 1:1 -> 1:N -> N:N
跨数据库,多数据源的统一调度,就会遇到分布式事务问题
如下图,单体应用被拆分成微服务应用,原来的三个模板
被拆分成三个独立
的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
官方文档:点我传送
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
分布式事务处理过程的一致性ID + 三组件模型
地址:https://github.com/seata/seata/releases
下载 0.9版本完成后,修改conf目录下的file.conf配置文件
首先我们需要备份原始的file.conf文件
主要修改,自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息,也就是修改存储的数据库
修改服务模块中的分组
vgroup_mapping.my_test_tx_group = "fsp_tx_group"
修改存储模块
在seata数据库中建表,建表语句在 seata/conf目录下的 db_store.sql
目的是:指明注册中心为nacos,及修改nacos连接信息
然后启动nacos 和 seata-server
Spring自带的是 @Transaction 控制本地事务
而 @GlobalTransaction控制的是全局事务
我们只需要在需要支持分布式事务的业务类上,使用该注解即可
在这之前首先需要先启动Nacos,然后启动Seata,保证两个都OK
这里我们会创建三个微服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过远程调用账户服务来扣减用户账户里面的金额,最后在订单服务修改订单状态为已完成
该操作跨越了三个数据库,有两次远程调用,很明显会有分布式事务的问题。
一句话:下订单 -> 扣库存 -> 减余额
建库SQL
create database seata_order;
create database seata_storage;
create database seata_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;
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
entity,domain:相当于实体类层
vo:view object,value object
dto:前台传到后台的数据传输类
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.37version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
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
在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 {
# 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"
}
}
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:已完结
}
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);
}
<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>
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,"订单创建成功");
}
}
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);
}
}
参考项目:seata-storage-service2002
参考项目:seata-account-service2003
访问
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
我们在account-module模块,添加睡眠时间20秒,因为openFeign默认时间是1秒
出现了数据不一致的问题
故障情况
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
rollbackFor表示,什么什么错误就会回滚
添加这个后,发现下单后的数据库并没有改变,记录都添加不进来
2019年1月份,蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案
Seata:Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架
2020起始,参加工作以后用1.0以后的版本。
什么是TC,TM,RM
TC:seata服务器
TM:带有@GlobalTransaction注解的方法
RM:数据库,也就是事务参与方
@GlobelTransaction
注解默认AT模式,阿里云GTS
两阶段提交协议的演变
在一阶段,Seata会拦截 业务SQL
before image(前置镜像)
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
二阶段如果顺利提交的话,因为业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照和行锁删除掉,完成数据清理即可
二阶段如果回滚的话,Seata就需要回滚到一阶段已经执行的 业务SQL,还原业务数据
回滚方式便是用 before image 还原业务数据,但是在还原前要首先校验脏写,对比数据库当前业务数据 和after image,如果两份数据完全一致,没有脏写,可以还原业务数据,如果不一致说明有脏读,出现脏读就需要转人工处理