1. 背景 Netflix版本由于停更,引发了Cloud各种组件的升级和替换:
创建父工程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 <packaging > pom</packaging > <properties > <java.version > 1.8</java.version > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <spring-boot.version > 2.6.13</spring-boot.version > <spring-cloud.version > 2021.0.8</spring-cloud.version > <cloud-Alibaba.version > 2021.0.4.0</cloud-Alibaba.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.8.18</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <scope > provided</scope > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > </dependencies > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > ${spring-cloud.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-dependencies</artifactId > <version > ${cloud-Alibaba.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <encoding > UTF-8</encoding > </configuration > </plugin > </plugins > </build >
2. 注册中心 Eureka Eureka在2.x版本后有些许变化,这里再简单回顾一下Eureka的用法。
服务端 创建子工程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-server</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
注意依赖发生了变换
1 2 3 4 5 6 7 @EnableEurekaServer @SpringBootApplication public class Eureka7001App { public static void main (String[] args) { SpringApplication.run(Eureka7001App.class, args); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 7001 eureka: instance: hostname: localhost client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
集群版的配置参照1.x版本即可
客户端 创建订单工程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @EnableEurekaClient @SpringBootApplication public class Order8001App { public static void main (String[] args) { SpringApplication.run(Order8001App.class, args); } } @Slf4j @RestController public class OrderController { @Value("${server.port}") private String port; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/order") public String oder () { return "下订单:" + port; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 server: port: 8001 spring: application: name: order-service eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: order-service:8001 prefer-ip-address: true
服务发现 客户端添加注解:**@EnableDiscoveryClient**;那么就可以获取到注册到Eureka中的服务信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Autowired private DiscoveryClient discoveryClient; @GetMapping(value = "/payment/discovery") public Object discovery () { List<String> services = discoveryClient.getServices(); for (String element : services) { System.out.println(element); } List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE" ); for (ServiceInstance element : instances) { System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t" + element.getUri()); } return this .discoveryClient; }
注意使用的类是:import org.springframework.cloud.client.discovery.DiscoveryClient;
Zookeeper 修改Eureka7001App模块:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-zookeeper-discovery</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 @EnableDiscoveryClient @SpringBootApplication public class Eureka7001App { public static void main (String[] args) { SpringApplication.run(Eureka7001App.class, args); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server: port: 7001 spring: application: name: zookeeper-cloud cloud: zookeeper: connect-string: 192.168 .124 .105 :2181
如果项目启动报错,可能是因为自带的zookeeper版本依赖和服务版本有冲突:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-zookeeper-discovery</artifactId > <exclusions > <exclusion > <groupId > org.apache.zookeeper</groupId > <artifactId > zookeeper</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.apache.zookeeper</groupId > <artifactId > zookeeper</artifactId > <version > 3.5.5</version > </dependency >
默认的版本为3.6.0,这里启动的服务版本为3.5.5;可以兼容
从下图可以看到,我们服务注册使用的是临时节点
使用Eureka做注册中心时,需要启动一个专门的SpringBoot服务作为注册中心,而使用consul或者zookeeper时,只需要将需要管理的服务直接注册到对应的服务中即可,即SpringBoot服务都是客户端。
Consul 官网:Consul | HashiCorp Developer
consul的安装启动在这里就不进行介绍了,以下是相关的依赖和配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-consul-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 spring: application: name: consul-cloud cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
还是仅需要@EnableDiscoveryClient一个注解即可
三种注册中心的比较:
AP:Eureka
CP:Zookeeper/Consul
3. 服务调用 Ribbon Ribbon的使用没有大的改动,这里简单介绍一下。
如果只是使用简单的轮询,我们在注册中心部分的依赖就可以实现,创建一个消费者:
1 2 3 4 5 6 7 8 @EnableEurekaClient @EnableDiscoveryClient @SpringBootApplication public class Consumer80App { public static void main (String[] args) { SpringApplication.run(Consumer80App.class, args); } }
1 2 3 4 5 6 7 8 @Configuration public class BeanConfig { @Bean @LoadBalanced public RestTemplate restTemplate (RestTemplateBuilder builder) { return builder.build(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j @RestController public class ConsumerController { @Autowired private RestTemplate restTemplate; private String order = "http://ORDER-SERVICE" ; @GetMapping("/consumer/order") private String pay () { log.info("=======进行远程调用=======" ); return restTemplate.getForObject(order + "/order" , String.class); } }
注意这里的服务名称必须大写,和Eureka界面展示的名称保持一致。
再复制一份oder子工程,我们就能发现已经在以轮询的形式进行负载均衡。
如果我们需要其它或自定义负载均衡算法,就需要引入Ribbon的依赖了:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-ribbon</artifactId > </dependency >
自定义负载均衡算法以及**@RibbonClient**注解的使用,在此就不再赘述
OpenFeign
Feign
OpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端 Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。 OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
修改消费者代码:
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
主启动类添加注解:@EnableFeignClients;创建service:
1 2 3 4 5 6 @Component @FeignClient(value = "PAY-SERVICE") public interface PayService { @GetMapping("pay") String pay () ; }
1 2 3 4 5 6 7 @Autowired private PayService payService;@GetMapping("/consumer/pay") private String pay () { log.info("=======Feign进行远程调用=======" ); return payService.pay(); }
超时控制 1 2 3 4 5 6 ribbon: ReadTimeout: 5000 ConnectTimeout: 5000
日志打印 Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,也就是对Feign接口的调用情况进行监控和输出。
日志级别:
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
1 2 3 4 5 6 7 8 9 @Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel () { return Logger.Level.FULL; } }
1 2 3 4 logging: level: com.test.service.PayService: debug
效果:
4. 服务降级 Hystrix Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
触发降级的情况:
程序运行异常
响应超时
服务熔断触发服务降级
线程池/信号量打满也会导致服务降级
1 2 3 4 5 6 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-hystrix</artifactId > <version > 2.2.10.RELEASE</version > </dependency >
在消费者服务中使用@HystrixCommand和@EnableHystrix两个注解:
@EnableCircuitBreaker已经过时了
Hystrix的使用也没有太大的变化,我们可以使用@DefaultProperties注解来指定当前控制器默认的降级方法:
1 2 3 4 5 6 7 8 9 @DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod") public class ConsumerController { public String paymentGlobalFallbackMethod () { return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~" ; } }
可以使用fallback来制定service的降级方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Component @FeignClient(value = "PAY-SERVICE",fallback = PayServiceImpl.class) public interface PayService { @GetMapping("pay") String pay () ; @GetMapping("pay/timeout") String payTimeOut () ; } @Component public class PayServiceImpl implements PayService { @Override public String pay () { return "FallbackFactory.pay()" ; } @Override public String payTimeOut () { return "FallbackFactory.payTimeOut()" ; } }
和 fallbackFactory 属性的使用方法差不多
配置文件:
1 2 3 feign: circuitbreaker: enabled: true
注意像现在调用pay/timeout接口会因为超时触发降级
设置全局超时时间:
1 2 3 4 hystrix: command: default: execution.isolation.thread.timeoutInMilliseconds: 5000
服务监控 1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-hystrix-dashboard</artifactId > <version > 2.2.10.RELEASE</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean public ServletRegistrationBean getServlet () { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet (); ServletRegistrationBean registrationBean = new ServletRegistrationBean (streamServlet); registrationBean.setLoadOnStartup(1 ); registrationBean.addUrlMappings("/hystrix.stream" ); registrationBean.setName("HystrixMetricsStreamServlet" ); return registrationBean; }
被监控的服务需要添加依赖:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
监控地址:http://localhost:80/hystrix.stream
5. 网关 Zuul 作用:
路由
过滤
负载均衡
灰度发布
路由功能是负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础。
创建新的工程:router
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-zuul</artifactId > <version > 2.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
1 2 3 4 5 6 7 8 @EnableZuulProxy @EnableEurekaClient @SpringBootApplication public class Router9005App { public static void main (String[] args) { SpringApplication.run(Router9005App.class, args); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server: port: 9005 spring: application: name: zuul eureka: client: service-url: defaultZone: http://localhost:7001/eureka instance: instance-id: zuul:9005 prefer-ip-address: true
按照上面的配置运行,最终会出现:
1 2 java.lang.NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath()Ljava/lang/String; at ...
原因:
2.2.10.RELEASE版本的zuul需要配置2.4.x的springboot版本;但是当前使用的SpringCloud版本需要2.6.x和2.7.x版本的SpringBoot,所以需要整体下调版本,由于用法没有太多变化(并且已经被淘汰了),这里就不再进行演示。
Gateway 官网介绍:Spring Cloud Gateway
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和 Project Reactor等技术,,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标是提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
核心概念:
Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
Predicate(断言):参考的是Java8的java.util.function.Predicate;开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
工作流程的核心逻辑:路由转发+执行过滤器链
我们在router模块的基础上进行修改:
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: port: 9005 spring: application: name: gateway-service eureka: instance: hostname: gateway-service instance-id: gateway-service:9005 prefer-ip-address: true client: service-url: register-with-eureka: true fetch-registry: true defaultZone: http://localhost:7001/eureka
1 2 3 4 5 6 7 @EnableEurekaClient @SpringBootApplication public class Router9005App { public static void main (String[] args) { SpringApplication.run(Router9005App.class, args); } }
配置网关路由的两种方式:
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 spring: application: name: gateway-service cloud: gateway: routes: - id: pay_service uri: lb://pay-service predicates: - Path=/payService/** filters: - StripPrefix=1
代码中注入RouteLocator的Bean
1 2 3 4 5 6 7 8 @Bean public RouteLocator routes (RouteLocatorBuilder builder) { return builder.routes() .route("pay_service" , r -> r.path("/payService/**" ) .filters(f -> f.stripPrefix(1 )) .uri("lb://pay-service" ) ).build(); }
两种配置二选一即可,当前的请求路径为:http://localhost:9005/payService/pay
Route Predicate Factories Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。
Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 spring: application: name: gateway-service cloud: gateway: routes: - id: pay_service uri: lb://pay-service predicates: - Path=/payService/** - After=2024-01-05T15:10:03.685+08:00[Asia/Shanghai] - Before=2024-02-05T15:10:03.685+08:00[Asia/Shanghai] - Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai] - Cookie=username,asdf - Header=X-Request-Id, \d+ - Host=**.test.com - Method=GET - Query=username, \d+ filters: - StripPrefix=1
其中predicates配置项就指定了九种不同的Route Predicate Factories:
Path Route Predicate
After Route Predicate
Before Route Predicate
Between Route Predicate
Cookie Route Predicate
Header Route Predicate
Host Route Predicate
Method Route Predicate
Query Route Predicate
说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
6. 配置中心 SpringCloud Config 在GitHub中创建仓库并,上传文件(我这里是直接在仓库中新建文件,使用中文会导致读取显示乱码)
远程文件config-dev.yml:
1 2 3 today: weather: 大风阴天 mood: nice
依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 server: port: 6001 spring: application: name: cloud-config cloud: config: server: git: uri: git@github.com:LemonPuer/cloud_config.git label: main eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: cloud-config:6001 prefer-ip-address: true
主启动类:
1 2 3 4 5 6 7 8 @EnableEurekaClient @EnableConfigServer @SpringBootApplication public class Config6001 { public static void main (String[] args) { SpringApplication.run(Config6001.class, args); } }
读取配置文件:http://localhost:6001/main/config-dev.yml
配置读取规则:
1 2 3 4 #推荐 /{label}/{application}-{profile}.yml /{application}-{profile}.yml /{application}/{profile}[/{label}]
客户端 修改消费者consumer80。
依赖:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency >
添加配置文件bootstrap.yml:
1 2 3 4 5 6 7 spring: cloud: config: label: main name: config profile: dev uri: http://127.0.0.1:6001
加载顺序(优先级): bootstrap > application > 配置中心
即:bootstrap.yml > bootstrap.properties > application.yml > application.properties
后者的设置会覆盖前者
1 2 3 4 5 6 7 @Value("${today.mood}") private String mood;@GetMapping("mood") public String mood () { return "今天的心情是:" + mood; }
此时如果配置文件发生改变,那么需要将客户端重新启动才能读取到最新的配置
配置动态刷新 需要引入actuator监控:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
修改配置文件:
1 2 3 4 5 6 management: endpoints: web: exposure: include: "*"
控制器添加注解@RefreshScope:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RefreshScope @RestController public class configController { @Value("${today.mood}") private String mood; @GetMapping("mood") public String mood () { return "今天的心情是:" + mood; } }
我们修改配置文件后发现客户端读取的还是旧文件,我们需要手动发送重置请求:
1 curl -X POST "http://localhost:80/actuator/refresh"
成功后可以读取到最新的数据,避免了服务重启。
但此时还存在一个问题,当同一个服务的机器过多,对每一台机器进行请求是一件非常繁琐的事情。
7. 消息总线 SpringCloud Bus 分布式自动刷新配置功能;Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。
注意:Bus支持两种消息代理:RabbitMQ 和 Kafka
原理:ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
两种设计思路:
利用消息总线触发一个客户端/bus/refresh,从而刷新所有客户端的配置
利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置
我们使用第二种,第一种不合适的理由如下:
打破了微服务的职责单一性,对服务本身进行了侵入
破坏了微服务各节点的对等性
有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化
需要准备好RabbitMQ的环境,然后调整工程Order8001,使其加入配置中心。
给服务端config6001添加消息总线支持:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bus-amqp</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 management: endpoints: web: exposure: include: 'bus-refresh'
页面查看地址:http://192.168.124.105:15672/
修改客户端(80,8001):
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bus-amqp</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 management: endpoints: web: exposure: include: "*"
1 curl -X POST "http://localhost:6001/actuator/busrefresh"
旧版本的刷新地址:curl -X POST "http://localhost:6001/actuator/bus-refresh"
也可以通过请求参数指定集群进行配置更新:
1 curl -X POST "http://localhost:6001/actuator/busrefresh/order-service"
http://配置中心ip:配置中心的端口号/actuator/bus-refresh/{destination}
8. 消息驱动 SpringCloud Stream 作用:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
设计理念:通过向应用程序暴露统一的Channel通道,将应用程序与消息中间件隔离。
其中INPUT对应消费者,OUTPUT对应生产者。
stream中的消息通信方式遵循了发布-订阅模式,使用Topic主题进行广播,在RabbitMQ就是Exchange,在Kafka中就是Topic。
消息发送/消费标准流程:
channel:队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
Source和Sink:可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
生产者 修改consumer80工程,添加依赖:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-stream-rabbit</artifactId > </dependency >
修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 server: port: 80 spring: application: name: consumer-service cloud: config: label: main name: config profile: dev uri: http://127.0.0.1:6001 stream: binders: myRabbit: type: rabbit environment: spring: rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 bindings: test-output: content-type: application/json destination: myExchange2 binder: myRabbit rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: consumer-service:80 prefer-ip-address: true logging: level: com.test.service.PayService: debug feign: circuitbreaker: enabled: true client: config: default: connect-timeout: 5000 read-timeout: 5000 hystrix: command: default: execution.isolation.thread.timeoutInMilliseconds: 5000 management: endpoints: web: exposure: include: "*"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RestController public class MqController { @Autowired private MqService mqService; @GetMapping("/send/{message}") public String sendMessage (@PathVariable String message) { return mqService.sendMessage(message); } } @Service public class MqServiceImpl implements MqService { @Autowired private StreamBridge streamBridge; @Override public String sendMessage (String message) { boolean send = streamBridge.send("test-output" , message); return send ? "发送成功" : "发送失败" ; } }
消费者 修改order8001,引入依赖:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-stream-rabbit</artifactId > </dependency >
修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 server: port: 8001 spring: application: name: order-service cloud: stream: function: definition: getMessage binders: myRabbit: type: rabbit environment: spring: rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 bindings: test-output: content-type: application/json destination: myExchange1 binder: myRabbit getMessage-in-0: content-type: application/json destination: myExchange2 binder: myRabbit group: order8001 rabbitmq: host: 192.168 .124 .105 port: 5672 username: john password: 123456 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: order-service:8001 prefer-ip-address: true management: endpoints: web: exposure: include: "*"
1 2 3 4 5 6 7 @Bean public Consumer<String> getMessage () { return message -> { System.out.println("Received message: " + message); }; }
注意bean的名称要与配置文件对应,例如getMessage
9. 请求链路跟踪 简称APM(Application Performance Managment)。
SpringCloud Sleuth Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案,用来监控服务节点之间的调用链路。
兼容支持zipkin(开源的分布式跟踪系统),官网:OpenZipkin · A distributed tracing system
下载链接:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.25.2/zipkin-server-2.25.2-exec.jar
这已经是支持java8的最后一个版本
运行:java -jar zipkin-server-2.25.2-exec.jar
上/下游服务(consumer80 / order8001)添加依赖:
1 2 3 4 5 6 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-zipkin</artifactId > <version > 2.2.8.RELEASE</version > </dependency >
修改配置文件:
1 2 3 4 5 6 7 spring: zipkin: base-url: http://192.168.124.105:9411 sleuth: sampler: probability: 1
界面访问:http://192.168.124.105:9411/
10. Alibaba Nacos 简介:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
具有注册中心+配置中心的功能,相当于:Eureka+Config +Bus。
官网:Nacos
注册中心比较:
服务注册与发现框架
CAP模型
控制台管理
社区活跃度
Eureka
AP
支持
低
Zookeeper
CP
不支持
中
Consul
CP
支持
高
Nacos
AP
支持
高
安装启动 需要Java8环境。
1 2 3 4 tar -zxvf nacos-server-2.3.0.tar.gz -C app/ cd /opt/app/nacos/bin # 单机模式启动 ./startup.sh -m standalone
访问地址:http://192.168.124.105:8848/nacos
注册中心 创建工程NacosConsumer80,添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-loadbalancer</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > </dependencies >
1 2 3 4 5 6 7 8 @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class NacosConsumer80 { public static void main (String[] args) { SpringApplication.run(NacosConsumer80.class, args); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 server: port: 80 spring: application: name: consumer-service cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 logging: level: com.test.service.PayService: debug feign: circuitbreaker: enabled: true client: config: default: connect-timeout: 5000 read-timeout: 5000 hystrix: command: default: execution.isolation.thread.timeoutInMilliseconds: 5000
远程调用业务方法:
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @RestController public class ConsumerController { @Autowired private PayService payService; @GetMapping("/consumer/pay") private String pay () { log.info("=======Feign进行远程调用=======" ); return payService.pay(); } }
修改服务提供方:Pay9001
1 2 3 4 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency >
主启动类添加注解:@EnableDiscoveryClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 server: port: 9001 spring: application: name: pay-service cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 management: endpoints: web: exposure: include: '*'
界面:
Nacos 支持AP和CP模式的切换
如何选择:
一般来说, 如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如 Spring cloud 和 Dubbo 服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或者存储配置信息,那么 CP 是必须,K8S服务和DNS服务则适用于CP模式。 CP模式下则支持注册持久化实例,此时则是以 Raft 协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
1 curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
配置中心 1 2 3 4 5 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency >
修改NacosConsumer80项目,引入依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency >
修改bootstrap.yml
1 2 3 4 5 6 7 8 spring: application: name: consumer-service cloud: nacos: config: server-addr: 192.168 .124 .105 :8848 file-extension: yaml
注意,spring.application.name=consumer-service必须放在bootstrap.yml中,否则不能自动更新配置
修改application.yml:
1 2 3 4 5 6 7 spring: profiles: active: dev cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848
业务类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RefreshScope @RestController public class ConfigController { @Value("${testName}") private String testName; @Value("${today.mood}") private String mood; @GetMapping("name") public String name () { return testName; } @GetMapping("mood") public String mood () { return "今天的心情是:" + mood; } }
Nacos中配置文件的匹配公式:
1 ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新
除了DataID,Nacos还支持Group和Namespcae,可以通过配置文件指定:
1 2 3 4 5 6 7 8 9 10 spring: application: name: consumer-service cloud: nacos: config: server-addr: 192.168 .124 .105 :8848 file-extension: yaml group: cloud namespace: prod
集群搭建 重启Nacos可以发现之前添加的配置文件都还存在,这是因为Nacos中存在内置数据源derby,当需要以集群形式启动时,内置数据源就会存在数据不一致的问题,Nacos支持Mysql数据存储。
cd /opt/app/nacos/conf可以看到mysql的初始化脚本。同样的,我们需要将mysql的配置信息添加到当前目录的application.properties中:
1 2 3 4 5 6 spring.datasource.platform =mysql db.num =1 db.url.0 =jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user =root db.password =123456
创建cluster.conf,在其中添加Nacos的集群信息:
1 2 3 192.168.124.106:8848 192.168.124.106:8849 192.168.124.106:8850
Nginx+Nacos+MySQL
Sentinel 实现熔断与限流,相当于Hystrix。
安装启动 官网:home | Sentinel (sentinelguard.io)
GitHub - alibaba/Sentinel)
运行条件:java8;8080端口未被占用。java -jar sentinel-dashboard-1.8.7.jar
管理界面:ip:8080
登录账号密码都是sentinel
修改工程:NacosConsumer80,添加依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency >
添加sentinel配置
1 2 3 4 5 6 7 8 9 10 11 spring: profiles: active: dev cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 sentinel: transport: dashboard: 192.168 .124 .105 :8080 port: 8719
Sentinel采用的懒加载,需要请求之后才能在页面展示
效果:
流控规则
资源名:唯一名称,默认请求路径
针对来源:Sentinel可以针对调用者进行限流(填写微服务名),默认default(不区分来源)
阈值类型/单机阈值:
QPS(每秒钟的请求数量):当调用接口的QPS达到阈值时,进行限流
线程数:当调用接口的线程数达到阈值时,进行限流(不准确,不常用)
是否集群:限流规则是针对服务的单个机器还是整个服务集群
流控模式:
直接:接口达到限流条件时直接限流
关联:当关联的资源达到阈值时,限流当前接口
链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值就进行限流)
流控效果:
快速失败:直接失败,抛出异常
Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor。经过预热时长,才达到设置的QPS阈值(适用于需要时间创建缓存的接口)
排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
降级规则
RT(平均响应时间,秒级)
统计时长内的平均响应时间 超出比例阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级
熔断时长过后关闭断路器
RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
异常比列(秒级):QPS >= 5 且异常比例(秒级统计)超过比例阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级):异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
热点Key限流
热点即经常访问的数据。
参数索引:可以根据请求参数的值来进行自定义限流策略
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("test") @SentinelResource("testHotKey") public String test (String p1, String p2) { if (p1.equals("name" ) && p2.equals("mood" )) { return testName + "," + mood; } else if (p1.equals("name" )) { return testName + "," + p2; } else if (p2.equals("mood" )) { return p1 + "," + mood; } return "p1:" + p1 + " p2:" + p2; }
当进入限流时,页面会直接报错,给用户不好的使用体验,我们可以通过@SentinelResource注解来自定义降级方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @GetMapping("test") @SentinelResource(value = "testHotKey",blockHandler = "myBlockHandler") public String test (String p1, String p2) { if (p1.equals("name" ) && p2.equals("mood" )) { return testName + "," + mood; } else if (p1.equals("name" )) { return testName + "," + p2; } else if (p2.equals("mood" )) { return p1 + "," + mood; } return "p1:" + p1 + " p2:" + p2; } public String myBlockHandler (String p1, String p2, BlockException exception) { return "服务降级:myBlockHandler:" + p1 + "," + p2; }
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
系统规则
Load (仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
CPU usage (1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
RT :当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
线程数 :当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS :当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
@SentinelResource 在上面的部分我们已经简单使用了@SentinelResource注解,但又出现了以下问题:
自定义处理方法和业务代码耦合
每个业务代码都需要添加兜底方法,代码膨胀
解决方案:
1 2 3 4 5 @GetMapping("testHandler") @SentinelResource(value = "testHandler", blockHandlerClass = {TestBlockHandler.class}, blockHandler = "handlerException") public String testHandler () { return "用户:" + testName + "<br/>" + "今天的心情是:" + mood; }
1 2 3 4 5 public class TestBlockHandler { public static String handlerException (BlockException exception) { return "自定义的限流处理信息......TestBlockHandler.handlerException" ; } }
其它属性:
value:资源名称,必需项(不能为空)
entryType:entry 类型,可选项(默认为 EntryType.OUT)
blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
返回值类型必须与原函数返回值类型一致;
方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
返回值类型必须与原函数返回值类型一致;
方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
规则持久化 在前面的实践中可以发现:一旦重启应用,sentinel规则将消失。
修改NacosConsumer80配置文件设置规则持久化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 spring: profiles: active: dev cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 sentinel: transport: dashboard: 192.168 .124 .105 :8080 port: 8719 datasource: myDS: nacos: server-addr: 192.168 .124 .105 :8848 data-id: ${spring.application.name} group-id: DEFAULT_GROUP data-type: json rule-type: flow
Nacos添加限流规则:
1 2 3 4 5 6 7 8 9 10 11 [ { "resource" : "testHandler" , "limitApp" : "default" , "grade" : 1 , "count" : 1 , "strategy" : 0 , "controlBehavior" : 0 , "clusterMode" : false } ]
resource:资源名称; limitApp:来源应用; grade:阈值类型,0表示线程数,1表示QPS; count:单机阈值; strategy:流控模式,0表示直接,1表示关联,2表示链路; controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待; clusterMode:是否集群。
重新启动服务后进行请求可以发现配置依然生效。
Seata 在进入分布式之后,每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
官网:Apache Seata
从上面官网的图片可以了解到,seata主要使用四部分实现分布式事务:
全局唯一的事务ID(Transaction ID XID)
Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
处理过程:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
XID 在微服务调用链路的上下文中传播;
RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
TM 向 TC 发起针对 XID 的全局提交或回滚决议;
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
安装启动 这里下载的是2.0.0版本,查看conf目录下的application.yaml可知默认使用的是文件存储:
在script/server/db存在mysql数据库的脚本,复制执行后,修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 config: type: nacos nacos: server-addr: 192.168 .124 .105 :8848 namespace: group: SEATA_GROUP data-id: seataServer.properties registry: type: nacos nacos: server-addr: 192.168 .124 .105 :8848 group: SEATA_GROUP namespace: cluster: default store: mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.124.106:3306/seata?rewriteBatchedStatements=true user: root password: 123456 min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 1000 max-wait: 5000
启动:./seata-server.sh
使用实践 修改工程NacosConsumer80,添加依赖:
1 2 3 4 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-seata</artifactId > </dependency >
这里默认的seata客户端版本为1.5.2,如果排除掉引入2.0.0存在配置冲突问题
修改配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 server: port: 80 spring: profiles: active: dev cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 seata: registry: type: nacos nacos: server-addr: 192.168 .124 .105 :8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: seata-test service: vgroup-mapping: seata-test: default logging: level: com.test.service.PayService: debug feign: circuitbreaker: enabled: true client: config: default: connect-timeout: 5000 read-timeout: 5000 hystrix: command: default: execution.isolation.thread.timeoutInMilliseconds: 5000
可以对照seata-server的配置进行理解
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/consumer/dbPay/{userId}") @GlobalTransactional(name = "dbPay", rollbackFor = Exception.class) public String payDb (@PathVariable("userId") Long userId) { Long l = orderService.dbOrder(userId); String s = payService.payByDb(userId); if ("fail" .equals(s)) { throw new RuntimeException ("payService调用失败!" ); } orderService.dbOrderFinish(l); return "success" ; }
修改工程order8002,添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-seata</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 server: port: 8002 spring: application: name: order-service cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.124.106:3306/seata_order username: root password: 123456 seata: registry: type: nacos nacos: server-addr: 192.168 .124 .105 :8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: seata-test service: vgroup-mapping: seata-test: default
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("dbOrder/{userId}") public Long dbOrder (@PathVariable("userId") Long userId) { Order order = Order.builder().userId(userId).money(new BigDecimal (100 )).count(1 ).productId(1L ).status(0 ).build(); orderMapper.insert(order); return order.getId(); } @GetMapping("dbOrderFinish/{id}") public String dbOrderFinish (@PathVariable("id") Long id) { orderMapper.updateById(Order.builder().id(id).status(1 ).build()); return "success" ; }
修改项目Pay9001,添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-seata</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 server: port: 9001 spring: application: name: pay-service cloud: nacos: discovery: server-addr: 192.168 .124 .105 :8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.124.106:3306/seata_pay username: root password: 123456 management: endpoints: web: exposure: include: '*' seata: registry: type: nacos nacos: server-addr: 192.168 .124 .105 :8848 namespace: "" group: SEATA_GROUP application: seata-server tx-service-group: seata-test service: vgroup-mapping: seata-test: default
1 2 3 4 5 6 @GetMapping("dbPay/{userId}") public String payByDb (@PathVariable("userId") Long userId) { accountMapper.updateAccount(userId, 100 ); return "进行支付: " + port; }
执行理论 分布式的执行流程:
TM 开启分布式事务(TM 向 TC 注册全局事务记录)
按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 )
TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务)
TC 汇总事务信息,决定分布式事务是提交还是回滚
TC 通知所有 RM 提交/回滚 资源,事务二阶段结束
AT模式 在上面的示例中,我们使用的就是对业务代码侵入最少也是最简单的AT模式。
原理介绍,Seata会使用数据库连接代理,对sql语句进行拦截。
一阶段加载
二阶段提交:SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚:使用before image还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
除了AT模式,还存在TCC模式,Saga模式和XA模式。