长情的啤酒 · ACTION_POINTER_UP,getX ...· 6 月前 · |
失恋的回锅肉 · 如何将ArrayBuffer转换为Base6 ...· 9 月前 · |
乐观的火锅 · 日志管理之 Docker logs - ...· 1 年前 · |
老实的伏特加 · 使用Python 文件读取的多种方式(四种方 ...· 1 年前 · |
坚强的吐司 · Python中self的用法详解,或者总是提 ...· 1 年前 · |
一、前言在前面的文章:SpringCloud之Feign实现声明式客户端负载均衡详细案例我们聊了OpenFeign的概述、为什么会使用Feign代替Ribbon、Feign和OpenFeign的区别、以及详细的OpenFeign实现声明式客户端负载均衡案例。在一些业务场景中,微服务间相互调用需要做鉴权,以保证我们服务的安全性。即:服务A调用服务B的时候需要将服务B的一些鉴权信息传递给服务B,从而保证服务B的调用也可以通过鉴权,进而保证整个服务调用链的安全。本文我们就讨论如果通过openfeign的拦截器RequestInterceptor实现服务调用链中上下游服务请求头数据的传递。二、实现RequestInterceptor通过RequestInterceptor 拦截器拦截我们的openfeign服务请求,将上游服务的请求头或者请求体中的数据封装到我们的openfeign调用的请求模板中,从而实现上游数据的传递。1、RequestInterceptor实现类1)RequestInterceptor实现类package com.saint.feign.config; import feign.RequestInterceptor; import feign.RequestTemplate; import lombok.extern.slf4j.Slf4j; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.Objects; * 自定义的Feign拦截器 * @author Saint @Slf4j public class MyFeignRequestInterceptor implements RequestInterceptor { * 这里可以实现对请求的拦截,对请求添加一些额外信息之类的 * @param requestTemplate @Override public void apply(RequestTemplate requestTemplate) { // 1. obtain request final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效) if (Objects.isNull(attributes)) { log.error("MyFeignRequestInterceptor is invalid!"); return; HttpServletRequest request = attributes.getRequest(); // 2. obtain request headers,and put it into openFeign RequestTemplate Enumeration<String> headerNames = request.getHeaderNames(); if (Objects.nonNull(headerNames)) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); requestTemplate.header(name, value); // todo 需要传递请求参数时放开 // 3. obtain request body, and put it into openFeign RequestTemplate // Enumeration<String> bodyNames = request.getParameterNames(); // StringBuffer body = new StringBuffer(); // if (bodyNames != null) { // while (bodyNames.hasMoreElements()) { // String name = bodyNames.nextElement(); // String value = request.getParameter(name); // body.append(name).append("=").append(value).append("&"); // } // } // if (body.length() != 0) { // body.deleteCharAt(body.length() - 1); // requestTemplate.body(body.toString()); // log.info("openfeign interceptor body:{}", body.toString()); // } }2)使RequestInterceptor生效(均已验证)使RequestInterceptor生效的方式有四种;1> 代码方式全局生效直接在Spring可以扫描到的路径使用@Bean方法将RequestInterceptor实现类注入到Spring容器;package com.saint.feign.config; import feign.RequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; * @author Saint @Configuration public class MyConfiguration { @Bean public RequestInterceptor requestInterceptor() { return new MyFeignRequestInterceptor(); }2> 配置方式全局生效feign: client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: full # 拦截器配置(和@Bean的方式二选一) requestInterceptors: - com.saint.feign.config.MyFeignRequestInterceptor3> 代码方式针对某个服务生效直接在@FeignClient注解中指定configuration属性为RequestInterceptor实现类;4、配置方式针对某个服务生效feign: client: config: SERVICE-A: connectTimeout: 5000 readTimeout: 5000 loggerLevel: full # 拦截器配置(和@Bean的方式二选一) requestInterceptors: - com.saint.feign.config.MyFeignRequestInterceptor2、效果验证1)feign-server服务改造在文章 SpringCloud之Feign实现声明式客户端负载均衡详细案例的基础下,我们修改feign-server项目,添加一个MVC拦截器(用于获取请求头中的数据)1> MvcInterceptorpackage com.saint.feign.config; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; * 自定义MVC拦截器 * @author Saint @Slf4j public class MvcInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token-saint"); log.info("obtain token is : {}", token); return true; }2> MvcInterceptorConfig设置MVC拦截器会拦截哪些路径的请求,这里是所有的请求全部拦截。package com.saint.feign.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; * MVC拦截器配置 * @author Saint @Configuration public class MvcInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MvcInterceptor()) .addPathPatterns("/**"); }2)结果验证1> 执行请求:2> feign-consumer中的日志:3> feign-server中的日志:==结果显示,RequestInterceptor生效了==三、结合Hystrix限流使用时的坑(仅做记录)==此处OpenFeign依赖的SpringCloud版本是2020.X之前。==在application.yaml文件中做如下配置开启了Hystrix限流:feign: hystrix: enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 30000做完上述配置后,Feign接口的熔断机制为:线程模式;如果我们自定义了一个RequestInterceptor实现类,就会导致hystrix熔断机制失效,接口调用异常(404、null);1、原因分析在feign调用之前,会走RequestInterceptor拦截器,拦截器中使用了ServletRequestAttributes获取请求数据;默认feign使用的是线程池模式,当开启熔断的时候,负责熔断的线程和执行Feign接口的线程不是同一个线程,ServletRequestAttributes取到的将会是空值。2、解决方案将hystrix熔断方式从线程模式改为信号量模式;feign: hystrix: enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 30000 strategy: SEMAPHORE3、Hystrix线程和信号量隔离区别4、线程和信号量隔离的使用场景?1> 线程池隔离请求并发量大,并且耗时长(一般是计算量大或者读数据库);采用线程池隔离,可以保证大量的容器线程可用,不会由于其他服务原因,一直处于阻塞或者等待状态,快速失败返回。2> 信号量隔离请求并发量大,并且耗时短(一般是计算量小,或读缓存);采用信号量隔离时的服务的返回往往非常快,不会占用容器线程太长时间;其减少了线程切换的一些开销,提高了缓存服务的效率 。
@[toc]一、Feign概述Feign是一个声明式的客户端负载均衡器;采用的是基于接口的注解;整合了ribbon,具有负载均衡的能力;整合了Hystrix,具有熔断的能力;1、为什么会使用Feign代替Ribbon使用RestTemplate + ribbon的方式来进行服务间的调用,会导致我们每次去调用其他服务的一个接口,都要单独写一些代码。而Feign是声明式调用,可以让我们不用写代码,直接用一些接口和注解就可以完成对其他服务的调用。2、Feign和OpenFeign的区别?依赖不同:一个是spring-cloud-starter-feign,一个是spring-cloud-starter-openfeign支持的注解:OpenFeign是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。即:OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。二、Feign实现负载均衡整体项目目录包括四个Module,分别为:eureka-server、feign-server-api、feign-server、feign-consumer。其中eureka-server作为服务注册中心、feign-server-api作为服务提供者给consumer引入、feign-server作为具体的服务提供者实现、feign-consumer作为服务消费者。0、最上层父项目spring-cloud-center的pom.xml文件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <packaging>pom</packaging> <!--子模块--> <modules> <module>feign-server-api</module> <module>feign-server</module> <module>feign-consumer</module> <module>eureka-server</module> </modules> <artifactId>spring-cloud-center</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> <name>spring-cloud-center</name> <properties> <java.version>1.8</java.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <!-- java编译插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。1、搭建服务注册中心eureka-servereureka-server整体代码结构目录如下:其整体很简单、仅仅包含一个pom.xml文件、一个配置文件、一个启动类。1、pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud-center</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka-server</description> <dependencies> <!--集成Eureka-server--> <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</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> </dependencies> </project>2、修改配置文件application.ymlserver: port: 10010 spring: application: name: eureka-server eureka: client: # 把自身注册到Eureka-server中 register-with-eureka: true # 服务注册中心不需要去检索其他服务 fetch-registry: false # 指定服务注册中心的位置 service-url: defaultZone: http://localhost:10010/eureka instance: hostname: localhost3、修改启动类package com.saint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; * @author Saint @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }这里和普通的启动有一个区别:需要加上 @EnableEurekaServer 注解开启Eureka-Server。4、启动eureka-server启动成功后,控制台输出如下:进入到eureka-server 的dashboard,可以看到eureka-server已经上线:2、搭建服务提供者API(feign-server-api)feign-server-api整体代码结构目录如下:其中包含一个pom.xml文件、一个用户类User、一个标注@RequestMapping注解的接口ServiceA。1、pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud-center</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>feign-server-api</artifactId> <version>0.0.1-SNAPSHOT</version> <description>feign test service provider api</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>2、Userpackage com.saint.feign.model; * @author Saint public class User { private Long id; private String name; private Integer age; public User() { public User(Long id, String name, Integer age) { this.id = id; this.name = name; this.age = age; public Long getId() { return id; public void setId(Long id) { this.id = id; public String getName() { return name; public void setName(String name) { this.name = name; public Integer getAge() { return age; public void setAge(Integer age) { this.age = age; @Override public String toString() { return "User [id=" + id + ", name=" + name + ", age=" + age + "]"; }3、ServiceApackage com.saint.feign.service; import com.saint.feign.model.User; import org.springframework.web.bind.annotation.*; * @author Saint @RequestMapping("/user") public interface ServiceA { @RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET) String sayHello(@PathVariable("id") Long id, @RequestParam("name") String name, @RequestParam("age") Integer age); @RequestMapping(value = "/", method = RequestMethod.POST) String createUser(@RequestBody User user); @RequestMapping(value = "/{id}", method = RequestMethod.PUT) String updateUser(@PathVariable("id") Long id, @RequestBody User user); @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) String deleteUser(@PathVariable("id") Long id); @RequestMapping(value = "/{id}", method = RequestMethod.GET) User getById(@PathVariable("id") Long id); }3、搭建服务提供者implement(feign-server)feign-server-api整体代码结构目录如下:其中包含一个pom.xml文件、一个application配置文件、一个启动类、一个Controller。1、pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud-center</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>feign-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>feign test service provider</description> <dependencies> <dependency> <groupId>com.saint</groupId> <artifactId>feign-server-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--集成eureka-client--> <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> </dependencies> </project>2、application.ymlserver: port: 8081 spring: application: name: service-a eureka: client: # 将当前服务注册到服务注册中心 service-url: defaultZone: http://localhost:10010/eureka3、启动类FeignServerApplicationpackage com.saint.feign; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; * @author Saint @EnableEurekaClient @SpringBootApplication public class FeignServerApplication { public static void main(String[] args) { SpringApplication.run(FeignServerApplication.class, args); }4、编写ServiceAControllerServiceAController实现feign-server-api模块下的ServiceA,提供具体的业务实现。package com.saint.feign.controller; import com.saint.feign.model.User; import com.saint.feign.service.ServiceA; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController public class ServiceAController implements ServiceA { @Override public String sayHello(@PathVariable("id") Long id, @RequestParam("name") String name, @RequestParam("age") Integer age) { System.out.println("打招呼,id=" + id + ", name=" + name + ", age=" + age); return "{'msg': 'hello, " + name + "'}"; @Override public String createUser(@RequestBody User user) { System.out.println("创建用户," + user); return "{'msg': 'success'}"; @Override public String updateUser(@PathVariable("id") Long id, @RequestBody User user) { System.out.println("更新用户," + user); return "{'msg': 'success'}"; @Override public String deleteUser(@PathVariable("id") Long id) { System.out.println("删除用户,id=" + id); return "{'msg': 'success'}"; @Override public User getById(@PathVariable("id") Long id) { System.out.println("查询用户,id=" + id); return new User(1L, "张三", 20); 5、启动service-a服务实例1(8081端口)服务启动成功后,控制台输出如下:再看eureka-server dashboard中多了一个 SERVICE-A 服务,并且其有一个实例 192.168.1.6:service-a:8081。6、启动service-a服务实例2(8082端口)1> 修改FeignServerApplication的配置:2> 复制出一个FeignServerApplication配置:3> 修改第二启动类配置名为:FeignServerApplication-8082,启动端口为8082:4> 运行FeignServerApplication-8082:5> 启动之后,看eureka-server dashboard中SERVICE-A 服务多了一个实例 192.168.1.6:service-a:8082:4、搭建服务消费者feign-consumerfeign-consumer整体代码结构目录如下:其包含一个pom.xml文件、一个application配置文件、一个启动类、一个FeignClient接口、一个Controller。1、pom.xml这里使用的是open-feign,想使用老版本的feign把代码中的注释放开;并把spring-cloud-starter-openfeign依赖注掉即可。<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-cloud-center</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>feign-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <description>feign test consumer</description> <dependencies> <dependency> <groupId>com.saint</groupId> <artifactId>feign-server-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--集成feign PS: 博主使用的SpringCloud版本内部没有管理feign的版本,而是管理的open-feign版本--> <!-- <dependency>--> <!-- <groupId>org.springframework.cloud</groupId>--> <!-- <artifactId>spring-cloud-starter-feign</artifactId>--> <!-- <version>1.4.7.RELEASE</version>--> <!-- </dependency>--> <!--集成openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--集成eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>2、修改配置文件application.ymlserver: port: 9090 spring: application: name: service-b eureka: client: # 将当前服务注册到服务注册中心 service-url: defaultZone: http://localhost:10010/eureka注:服务端口为9090,后面我们进行接口调用的时候会用到。3、修改启动类package com.saint.feign; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; * @author Saint @SpringBootApplication @EnableEurekaClient @EnableFeignClients public class FeignConsumerApplication { public static void main(String[] args) { SpringApplication.run(FeignConsumerApplication.class, args); }其中的@EnableFeignClients注解负责使@FeignClient注解生效,可以被扫描到。4、FeignClient接口(ServiceAClient)package com.saint.feign.client; import com.saint.feign.service.ServiceA; import org.springframework.cloud.openfeign.FeignClient; * @author Saint @FeignClient("SERVICE-A") public interface ServiceAClient extends ServiceA { }FeignClient接口要实现feign-server-api中的ServiceA接口,以表明当前FeignClient针对的对象5、编写ServiceBControllerpackage com.saint.feign.controller; import com.saint.feign.model.User; import com.saint.feign.client.ServiceAClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/ServiceB/user") public class ServiceBController { @Autowired private ServiceAClient serviceA; @RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET) public String greeting(@PathVariable("id") Long id, @RequestParam("name") String name, @RequestParam("age") Integer age) { return serviceA.sayHello(id, name, age); @RequestMapping(value = "/", method = RequestMethod.POST) public String createUser(@RequestBody User user) { return serviceA.createUser(user); @RequestMapping(value = "/{id}", method = RequestMethod.PUT) public String updateUser(@PathVariable("id") Long id, @RequestBody User user) { return serviceA.updateUser(id, user); @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public String deleteUser(@PathVariable("id") Long id) { return serviceA.deleteUser(id); @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User getById(@PathVariable("id") Long id) { return serviceA.getById(id); }ServiceBController中通过FeignClient做负载均衡调用SERVICE-A服务中提供的接口。6、启动feign-consumer启动成功后,看eureka-server dashboard中多了一个 SERVICE-B 服务,并且其有一个实例 192.168.1.6:service-b:9090。5、使用浏览器进行调用服务消费者上述步骤中,我们已经依次启动了eureka-server、feign-server-8081、feign-server-8082、feign-consumer;三个服务、四个实例。此处我们针对服务消费者ribbon-feign-sample-consumer做四次接口调用,均为:http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18然后我们去看feign-server-8081、feign-server-8082的控制台输出:1> feign-server-8081控制台输出:2> feign-server-8082控制台输出:3> 结果说明:我们可以发现,四个请求,ribbon-feign-sample-8081和ribbon-feign-sample-8082各分担了两个请求。从现象上来看,已经Feign实现了负载均衡,并且默认是按照轮询的方式。下文我们接着讨论 Feign是如何实现负载均衡(源码分析)?
@[TOC]一、前言前置Ribbon相关文章:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)【云原生&微服务四】SpringCloud之Ribbon和Erueka集成的细节全在这了(源码剖析)【微服务五】Ribbon随机负载均衡算法如何实现的【微服务六】Ribbon负载均衡策略之轮询(RoundRobinRule)、重试(RetryRule)我们聊了以下问题:为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?SpringCloud是如何集成Ribbon的?Ribbon如何作用到RestTemplate上的?如何获取到Ribbon的ILoadBalancer?ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?Ribbon如何发送网络HTTP请求?Ribbon如何用IPing机制动态检查服务实例是否存活?Ribbon负载均衡策略之随机(RandomRule)、轮询(RoundRobinRule)、重试(RetryRule)实现方式;本文继续讨论 最佳可用规则(BestAvailableRule)是如何实现的?PS:Ribbon依赖Spring Cloud版本信息如下:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>二、BestAvailableRuleBestAvailableRule会逐个考察Server,如果Server被tripped了,则跳过;最终选择一个并发请求量最小的Server。1、负载规则我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,所以看BestAvailableRule的choose(Object key)方法:详细代码注释如下:public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule { // 维护了服务实例的一些状态信息 private LoadBalancerStats loadBalancerStats; @Override public Server choose(Object key) { // 如果服务实例状态信息为空,则直接使用父类的choose()方法,采用RoundRobin算法 if (loadBalancerStats == null) { return super.choose(key); // 获取所有的服务实例 List<Server> serverList = getLoadBalancer().getAllServers(); // 最小并发连接数 int minimalConcurrentConnections = Integer.MAX_VALUE; // 当前时间 long currentTime = System.currentTimeMillis(); Server chosen = null; // 遍历每个实例 for (Server server: serverList) { ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 如果服务实例被tripped了,则直接跳过当前服务实例 if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 获取实例的并发数(当且仅当 当前时间与上次有效更改连接数的时间间隔在指定范围之内(默认10分钟)) int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 找到并发连接最小的那个服务实例 if (concurrentConnections < minimalConcurrentConnections) { minimalConcurrentConnections = concurrentConnections; chosen = server; // 如果遍历完所有的服务实例之后,还没有找到server,则调用父类的choose()方法,用RoundRobin算法进行选择。 if (chosen == null) { return super.choose(key); } else { return chosen; @Override public void setLoadBalancer(ILoadBalancer lb) { super.setLoadBalancer(lb); if (lb instanceof AbstractLoadBalancer) { loadBalancerStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats(); }方法的核心逻辑:首先判断如果服务实例状态信息为空,则直接使用父类的choose()方法,采用RoundRobin算法。否则:从BestAvailableRule所属的ILoadBalancer中获取服务的所有实例,记录当前时间;遍历服务的每个实例,获取实例的ServerStats,如果实例被tripped了,则直接跳过当前服务实例;否则,获取实例的并发数(这里当且仅当 当前时间与上次有效更改连接数的时间间隔在指定范围之内(默认10分钟)),如果超过了时间范围则返回0。循环结束后返回并发数最小的第一个实例。最后,如果遍历完所有的服务实例之后,还没有得到Server,则调用其父类的choose()方法,使用RoundRobin算法选择一个实例。下面我们接着看几个细节点:如何判断服务实例被tripped?如何获取服务实例的并发数?2、如何判断服务实例被tripped?逻辑体现在ServerStats的isCircuitBreakerTripped(long currentTime)方法中:public boolean isCircuitBreakerTripped(long currentTime) { // 获取断路器超时时间点 long circuitBreakerTimeout = getCircuitBreakerTimeout(); // 如果断路器超时时间点 <= 0,则直接返回false。 if (circuitBreakerTimeout <= 0) { return false; // 如果断路器超时时间点 > 当前时间,则返回true,表示服务实例被tripped了;否则返回false return circuitBreakerTimeout > currentTime; }方法核心逻辑:判断断路器超时时间点是否大于当前时间,如果大于,则表示当前服务实例被tripped了,也就不会再被选择;否者,正常选择。3、如何获取服务实例的并发数?逻辑体现在ServerStats的getActiveRequestsCount(long currentTime)方法中:public int getActiveRequestsCount(long currentTime) { // 获取实例当前的并发连接数 int count = activeRequestsCount.get(); // 连接数为0,则直接返回0 if (count == 0) { return 0; // 如果当前时间与上次有效更改连接数的时间间隔不在指定范围之内(默认10分钟),则并发连接数设置为0,并返回0 } else if (currentTime - lastActiveRequestsCountChangeTimestamp > activeRequestsCountTimeout.get() * 1000 || count < 0) { activeRequestsCount.set(0); return 0; } else { // 正常场景下返回并发连接数 return count; AtomicInteger activeRequestsCount = new AtomicInteger(0); private static final DynamicIntProperty activeRequestsCountTimeout = DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.serverStats.activeRequestsCount.effectiveWindowSeconds", 60 * 10);关键点在于实例的并发数是如何维护的?下面我就接着看。4、实例并发数的维护:1)增加实例的并发数在开始执行一个Rest请求时会通过ServerStats#incrementActiveRequestsCount()方法新增一个连接数(activeRequestsCount);虽然是在new一个RibbonStatsRecorder时新增的实例并发数,但是RibbonStatsRecorder内部组合的ServerStats来源于Ribbo的上下文RibbonLoadBalancerContext,所以每次new RibbonStatsRecorder时,ServerStats数据是共享的;2)减少实例的并发数当Rest请求调用外部服务执行完毕之后,会通过ServerStats#decrementActiveRequestsCount()方法减少一个连接数(activeRequestsCount):RibbonStatsRecorder#recordStats(Object entity) 方法如下:三、后续文章下一篇文章我们接着分析Ribbon负载均衡策略之WeightedResponseTimeRule。
@[TOC]一、前言前置Ribbon相关文章:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)【云原生&微服务四】SpringCloud之Ribbon和Erueka集成的细节全在这了(源码剖析)【微服务五】Ribbon随机负载均衡算法如何实现的我们聊了以下问题:为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?SpringCloud是如何集成Ribbon的?Ribbon如何作用到RestTemplate上的?如何获取到Ribbon的ILoadBalancer?ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?Ribbon如何发送网络HTTP请求?Ribbon如何用IPing机制动态检查服务实例是否存活?Ribbon负载均衡策略之随机(RandomRule)实现方式;本文继续讨论 轮询(RoundRobinRule)、重试(RetryRule)是如何实现的?PS:Ribbon依赖Spring Cloud版本信息如下:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>二、轮询算法 --> RoundRobinRule我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,而choose(Object key)方法中又会调用choose(ILoadBalancer lb, Object key)方法,所以我们只需要看各个IRule实现类的choose(ILoadBalancer lb, Object key)方法;随机算法体现在RoundRobinRule#incrementAndGetModulo()方法:private AtomicInteger nextServerCyclicCounter; private int incrementAndGetModulo(int modulo) { // 死循环直到获取到一个索引下标 for (;;) { // 获取当前AtomicInteger类型变量的原子值 int current = nextServerCyclicCounter.get(); // 当前原子值 + 1 然后对 服务实例个数取余 int next = (current + 1) % modulo; // CAS修改AtomicInteger类型变量,CAS成功返回next,否则无限重试 if (nextServerCyclicCounter.compareAndSet(current, next)) return next; }轮询算法很简单,重点在于通过AtomicInteger原子类型变量 + 死循环 CAS操作实现,每次返回原子类型变量的当前值 + 1,因为原子类型变量可能超过服务实例数,所以每次对原子类型变量赋值时,都会对其和服务实例总数做取余运算。三、重试算法 --> RetryRule进入RetryRule的choose(ILoadBalancer lb, Object key)方法;方法的核心逻辑:首先记录开始要选择一个服务实例时的时间(即:开始请求时间为当前时间),和允许获取到服务实例的deadline,deadline为当前时间 + 500ms;接着使用RetryRule组合的RoundRobinRule轮询选择一个服务实例;如果选择的服务实例为空并且当前时间还没到deadline 或 选择的服务实例不是活着的并且当前时间还没到deadline,则进行重试、重新获取一个服务实例;重试之前会先启动一个延时(deadline-当前时间)执行的定时任务,其中负责到deadline时中断当前线程;死循环(当前线程不是中断状态时),调用RoundRobin算法选择一个服务实例,如果这个服务实例是有效的 或 当前时间过了截止时间,则跳出循坏;并取消上面新建的延时执行的定时任务,返回当前实例;如果服务实例不是活着的并且当前时间在截止时间之内,则调用Thread.yield(),让出线程资源,使当前线程 或 相同优先级的其他线程可以获取运行机会,也就是说 yield的线程有可能被线程调度程序再次选中执行。所以:RetryRule在subRule.choose(String)获得无效的服务实例后,会一直重试,但重试次数取决于重试的deadline、当前线程相同优先级的其他线程个数。
@[toc]一、前言在前面的Ribbon系列文章:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)【云原生&微服务四】SpringCloud之Ribbon和Erueka集成的细节全在这了(源码剖析)我们聊了以下问题:为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?SpringCloud是如何集成Ribbon的?Ribbon如何作用到RestTemplate上的?如何获取到Ribbon的ILoadBalancer?ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?Ribbon如何发送网络HTTP请求?Ribbon如何用IPing机制动态检查服务实例是否存活?本篇文章我们继续看Ribbon内置了哪些负载均衡策略?RandomRule负载均衡策略的算法是如何实现的?PS:Ribbon依赖Spring Cloud版本信息如下:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>二、Ribbon内置了哪些负载均衡算法?RandomRule --> 随机选择一个ServerRoundRobinRule --> 轮询选择,轮询Index,选择index对应位置的Server,请求基本平摊到每个Server上。WeightedResponseTimeRule --> 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低。ZoneAvoidanceRule --> 综合判断Server所在Zone的性能和Server的可用性选择server,在没Zone的环境下,类似于轮询(RoundRobinRule)。默认策略BestAvailableRule --> 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。RetryRule --> 对选定的负载均衡策略上 重试机制,在一个配置时间段内选择Server不成功,就一直尝试使用subRule(默认是RoundRobinRule)的方式选择一个可用的Server。AvailabilityFilteringRule --> 过滤掉一直连接失败的(被标记为circuit tripped的)的Server,并过滤掉那些高并发的后端Server 或者 使用一个AvailabilityPredicate来定义过滤Server的逻辑,本质上就是检查status里记录的各个Server的运行状态;其具体逻辑如下:先用round robin算法,轮询依次选择一台server,如果判断这个server是否是存活的、可用的,如果这台server是不可以访问的,那么就用round robin算法再次选择下一台server,依次循环往复10次,还不行,就走RoundRobin选择。三、随机算法 --> RandomRule我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,而choose(Object key)方法中又会调用choose(ILoadBalancer lb, Object key)方法,所以我们只需要看各个IRule实现类的choose(ILoadBalancer lb, Object key)方法;PS:allList和upList的一些疑问和解惑!最近和一个大V聊了一下RandomRule中Server的选择,随机的下标是以allList的size为基数,而Server的选择则是拿到随机数以upList为准;当时我们考虑极端情况可能存在越界问题!当天晚上博主又追了一下Ribbon的整个执行流程,结论如下:upList和allList是Ribbon维护在自己内存的,在服务启动时会从服务注册中心把服务实例信息拉到upList和allList;后续无论是通过ping机制还是每30s从注册中心拉取全量服务实例列表,但凡all list发生变更,都会触发一个事件,然后修改本地内存的up list。另外默认ping机制并不会定时每10s执行,因为默认的IPing实现是DummyPing,而BaseLoadBalancer#canSkipPing()里会判断IPing实现是DummyPing则不启动Timer定时做Ping机制。Eureka和Ribbon整合之后,EurekaRibbonClientConfiguration(spring-cloud-netflix-eureka-client包下)类中新定义了一个IPing(NIWSDiscoveryPing),此时会启动Timer每10s做一次ping操作。随机算法体现在RandomRule#chooseRandomInt()方法:然而,chooseRandomInt()方法中居然使用的不是Random,而是ThreadLocalRandom,并直接使用ThreadLocalRandom#nextInt(int)方法获取某个范围内的随机值,ThreadLocalRandom是个什么东东?1、ThreadLocalRandom详解ThreadLocalRandom位于JUC(java.util.concurrent)包下,继承自Random。1)为什么不用Random?从Java1.0开始,java.util.Random就已经存在,其是一个线程安全类,多线程环境下,科通通过它获取到线程之间互不相同的随机数,其线程安全性是通过原子类型AtomicLong的变量seed + CAS实现的。尽管Random使用 CAS 操作来更新它原子类型AtomicLong的变量seed,并且在很多非阻塞式算法中使用了非阻塞式原语,但是CAS在资源高度竞争时的表现依然糟糕。2)ThreadLocalRandom的诞生?JAVA7在JUC包下增加了该类,意在将它和Random结合以克服Random中的CAS性能问题;虽然可以使用ThreadLocal<Random>来避免线程竞争,但是无法避免CAS 带来的开销;考虑到性能诞生了ThreadLocalRandom;ThreadLocalRandom不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。ThreadLocalRandom的核心实现细节:使用一个普通long类型的变量SEED替换Random中的AtomicLong类型的seed;不能同构构造函数创建ThreadLocalRandom实例,因为它的构造函数是私有的,要使用静态工厂ThreadLocalRandom.current();它是CPU缓存感知式的,使用8个long虚拟域来填充64位L1高速缓存行3)ThreadLocalRandom的错误使用场景1> 代码示例:package com.saint.random; import java.util.concurrent.ThreadLocalRandom; * @author Saint public class ThreadLocalRandomTest { private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); public static void main(String[] args) { for (int i = 0; i < 10; i++) { new SonThread().start(); private static class SonThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " obtain random value is : " + RANDOM.nextInt(100)); }2> 运行结果:居然每个线程获取到的随机值都是一样的!!!3> 运行结果分析:上述代码中之所以每个线程获取到的随机值都是一样,因为:ThreadLocalRandom 类维护了一个类单例字段,线程通过调用 ThreadLocalRandom#current() 方法来获取 ThreadLocalRandom单例对象;然后以线程维护的实例字段 threadLocalRandomSeed 为种子生成下一个随机数和下一个种子值;线程在调用 current() 方法的时候,会根据用每个线程 thread 的一个实例字段 threadLocalRandomProbe 是否为 0 来判断当前线程实例是是第一次调用随机数生成方法,进而决定是否要给当前线程初始化一个随机的 threadLocalRandomSeed 种子值。所以,如果其他线程绕过 current() 方法直接调用随机数方法(比如nextInt()),那么它的种子值就是可预测的,即一样的。4)ThreadLocalRandom的正确使用方式每次要获取随机数时,调用ThreadLocalRandom的正确使用方式是ThreadLocalRandom.current().nextX(int):public class ThreadLocalRandomTest { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new SonThread().start(); private static class SonThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " obtain random value is : " + ThreadLocalRandom.current().nextInt(100)); }运行结果如下:5)ThreadLocalRandom源码解析1> nextInt(int bound)方法获取随机值public int nextInt(int bound) { if (bound <= 0) throw new IllegalArgumentException(BadBound); // 1. 使用当前种子值SEED获取新种子值,mix32()可以看到是一个扰动函数 int r = mix32(nextSeed()); int m = bound - 1; // 2. 使用新种子值获取随机数 if ((bound & m) == 0) // power of two r &= m; else { // reject over-represented candidates for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1) return r; }当bound=100时,代码执行如下:2> nextSeed()方法获取下一个种子值final long nextSeed() { Thread t; long r; // read and update per-thread seed //r = UNSAFE.getLong(t, SEED) 获取当前线程中对应的SEED值 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); return r; }nextSeed()方法中首先使用基于主内存地址的Volatile读的方式获取老的SEED种子值,然后再使用基于主内存地址的Volatile写的方式设置新的SEED种子值;种子值相关常量:// Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; // 种子值 private static final long SEED; private static final long PROBE; private static final long SECONDARY; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; SEED = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSeed")); PROBE = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomProbe")); SECONDARY = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSecondarySeed")); } catch (Exception e) { throw new Error(e); }3> 总述ThreadLocalRandom中直接基于主内存地址的Volatile读方式读取老SEED值。ThreadLocalRandom中直接基于主内存地址的Volatile写方式将老SEED值替换为新SEED值;因为这里的种子值都是线程级别的,所以不需要原子级别的变量,也不会出现多线程竞争修改种子值的情况。谈到基于主内存地址的Volatile读写,ConCurrentHashMap中也有大量使用,参考博文:https://blog.csdn.net/Saintmm/article/details/122911586。
@[toc]一、前言在前面的文章,博主聊了Ribbon如何与SpringCloud、Eureka集成,Ribbon如何自定义负载均衡策略、Ribbon如何和SpringCloud集成:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)在【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)一文中博主分析到了SpringCloud集成Ribbon如何获取到负载均衡器ILoadBalancer;本文我们接着分析如下问题:ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?Ribbon如何发送网络HTTP请求?Ribbon如何用IPing机制动态检查服务实例是否存活?PS: 文章中涉及到的SpringBoot相关知识点,比如自动装配,移步博主的SpringBoot专栏:Spring Boot系列。PS--2:Ribbon依赖Spring Cloud版本信息如下:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>==下面以请求http://localhost:9090/say/saint为入口进行debug。==二、Ribbon和Eureka我们知道SpringCloud诞生之初,我们通常采用Eureka作为服务注册/发现中心、Ribbon作为负载均衡器,而Ribbon的诞生也正是因为要结合Eureka做负载均衡;而现在很多项目都是Nacos + OpenFeign的组合,不过OpenFeign的底层还是Ribbon,其很多便捷性体现在代理对象的封装,后续我们接着讨论。1、Ribbon如何与eureka整合,通过eureka client获取到对应的注册表?在博文:SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)中我们知道了Ribbon默认的ILoadBalancer是ZoneAwareLoadBalancer;所以我们这里就看一下ZoneAwareLoadBalancer如何与eureka整合,通过eureka client获取到对应的注册表?先看ZoneAwareLoadBalancer的类图:ZoneAwareLoadBalancer的父类是DynamicServerListLoadBalancer,DynamicServerListLoadBalancer构造函数中会调用restOfInit()方法(其中会获取到所有的服务实例);void restOfInit(IClientConfig clientConfig) { boolean primeConnection = this.isEnablePrimingConnections(); // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList() this.setEnablePrimingConnections(false); // 感知新的服务实例的添加/移除,即动态维护服务实例列表 enableAndInitLearnNewServersFeature(); // 更新Eureka client 中所有服务的实例列表,即初始化服务实例列表 updateListOfServers(); if (primeConnection && this.getPrimeConnections() != null) { this.getPrimeConnections() .primeConnections(getReachableServers()); this.setEnablePrimingConnections(primeConnection); LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString()); }1)为什么是DynamicServerListLoadBalancer的restOfInit()方法?第一次获取Eureka中服务实例列表的执行流程如下:第一次从Ribbon的SpringClientFactory中获取GREETING-SERVICE服务对应的Spring子上下文时,获取不到,所以需要创建针对GREETING-SERVICE服务创建Spring子上下文;最终进入到NamedContextFactory#createContext(String name)方法中,方法的最后会调用AnnotationConfigApplicationContext#refresh()方法,refresh()方法中会对ZoneAwareLoadBalancer进行初始化;又由于DynamicServerListLoadBalancer是ZoneAwareLoadBalancer的父类,所以初始化ZoneAwareLoadBalancer时也会执行DynamicServerListLoadBalancer的构造函数,进而会执行DynamicServerListLoadBalancer的restOfInit()方法;2)DynamicServerListLoadBalancer#restOfInit()restOfInit()中主要做两件事:启动一个定时任务,定时更新服务实例列表。初始化服务实例列表List<Server>,并感知服务实例信息的变更;下面我们分开来看:0> 初始化服务实例列表流程图1> 初始化服务实例列表updateListOfServers()方法负责初始化服务实例列表,代码执行流程如下:最终进入到DiscoveryEnabledNIWSServerList的obtainServersViaDiscovery()方法从Eureka Client本地缓存的服务注册表中获取到服务的全部实例信息;我们debug过程中,知道了updateListOfServers()方法中涉及到的ServerList<T> serverListImpl是,那么serverListImpl是从哪来的?==PS:如果使用Nacos作为服务注册中心,这里的ServerList是NacosServerList。==ServerList从哪来的?serverListImpl是和Eureka相关的,我们去找eureka相关的jar包,最终找到spring-cloud-netflix-eureka-client jar包,其中有个org.springframework.cloud.netflix.ribbon.eureka目录,目录中有个EurekaRibbonClientConfiguration类:其中负责实例化ServerList<T>为DomainExtractingServerList,然而我们调用DomainExtractingServerList的getUpdatedListOfServers()方法获取服务的所有实例时,实际是交给其组合的DiscoveryEnabledNIWSServerList类成员的getUpdatedListOfServers()方法去执行;至此,我们知道了ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?其实就是从eureka client里去获取一下注册表,然后更新到LoadBalancer中去。2、 动态更新服务实例列表1)流程图2)流程解析enableAndInitLearnNewServersFeature()方法负责定时更新服务实例列表,代码执行逻辑如下:PollingServerListUpdater#start(UpdateAction)方法中会启动一个延迟1S并每间隔3S执行一次的定时任务去执行DynamicServerListLoadBalancer#doUpdate()方法去动态更新服务实例列表。再看DynamicServerListLoadBalancer#doUpdate()方法:方法内部其实就是调用初始化服务实例类别的那个方法:updateListOfServers(),也就是我们上面聊的。至此,我们也就知道了ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?3、 如何根据负载均衡规则从List<Server>中选出一个Server?回到RibbonLoadBalancerClient#execute()方法中:进入到getServer()方法,看如何选择一个服务的?核心逻辑如下:会进入到BaseLoadBalancer的chooseServer()方法中,用IRule来选择了一台服务器;IRule是RibbonClientConfiguraiton中实例化的ZoneAvoidanceRule,调用它的choose()方法来选择一个server,最终是用的ZoneAvoidanceRule的父类PredicateBasedRule#choose()方法:先执行过滤规则,过滤掉一批server,再根据我们自己指定的filter规则,然后用round robin轮询算法选择一个Server。下面看看负载均衡的轮询算法是怎么做的?1)轮询算法轮询算法很简单,重点在于通过AtomicInteger原子类型变量 + 死循环 CAS操作实现,每次返回原子类型变量nextIndex的当前值,因为原子类型变量nextIndex可能超过服务实例数,所以每次对原子类型变量nextIndex赋值时,都会对其做取余运算。4、如何发送网络HTTP请求?1)流程图2)流程解析getServer()方法选择出一个服务实例之后,进入到execute()重载方法去执行HTTP请求:代码整体执行流程如下:流程说明:在LoadBalancerInterceptor#intercept()方法中通过LoadBalancerRequestFactory工厂封装了HTTP请求为LoadBalancerRequest;在RibbonLoadBalancerClient的execute()方法中,调用了T returnVal = request.apply(serviceInstance);,进入到LoadBalancerRequest的apply()方法中,传入了选择出来的server,对这台server发起一个指定的一个请求。将LoadBalancerRequest和server再次封装为了一个ServiceRequestWrapper;然后通过ServiceRequestWrapper#getURI()方法,基于选择出来的server的地址,重构请求URI;即:将服务名替换为具体的IP:Port地址。最后将将真正的请求URL交给spring-web下的负责底层的http请求的组件ClientHttpRequestExecution去执行,发起了一次真正的HTTP请求。5、ping机制如何检查服务实例是否还存活?在RibbonClientConfiguration类中会注入IPing类型的实例DummyPing,其中isAlive()方法直接返回TRUE;public class DummyPing extends AbstractLoadBalancerPing { public DummyPing() { public boolean isAlive(Server server) { return true; }这里是Ribbon默认的IPing,但是Eureka和Ribbon整合之后,EurekaRibbonClientConfiguration(spring-cloud-netflix-eureka-client包下)类中新定义了一个IPing。@Bean @ConditionalOnMissingBean public IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, serviceId)) { return this.propertiesFactory.get(IPing.class, config, serviceId); NIWSDiscoveryPing ping = new NIWSDiscoveryPing(); ping.initWithNiwsConfig(config); return ping; }IPing的实例为NIWSDiscoveryPing;NIWSDiscoveryPing的isAlive()方法会检查某个server对应的eureka client中的InstanceInfo的状态,看看服务实例的status是否还正常;public boolean isAlive(Server server) { boolean isAlive = true; if (server!=null && server instanceof DiscoveryEnabledServer){ DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server; // 获取服务实例信息 InstanceInfo instanceInfo = dServer.getInstanceInfo(); if (instanceInfo!=null){ // 获取实例状态 InstanceStatus status = instanceInfo.getStatus(); if (status!=null){ // 服务实例状态是否为UP isAlive = status.equals(InstanceStatus.UP); return isAlive; }1)哪里使用到了IPing的isAlive()方法?在ZoneAwareLoadBalancer实例构造时(进入到父类BaseLoadBalancer中),会启动一个定时调度的任务,每隔10s,就用IPing组件对server list中的每个server都执行一下isAlive()方法,判断服务实例是否还存活。public BaseLoadBalancer() { this.name = DEFAULT_NAME; this.ping = null; setRule(DEFAULT_RULE); // 启动一个每10s执行一次的定时任务,最IPing#isAlive()操作 setupPingTask(); lbStats = new LoadBalancerStats(DEFAULT_NAME); void setupPingTask() { if (canSkipPing()) { return; if (lbTimer != null) { lbTimer.cancel(); lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true); // 执行BaseLoadBalancer的内部类PingTask#run(),默认每10s执行一次 lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000); // 快速进行一次IPing forceQuickPing(); class PingTask extends TimerTask { public void run() { try { new Pinger(pingStrategy).runPinger(); } catch (Exception e) { logger.error("LoadBalancer [{}]: Error pinging", name, e); }进入到PingTask#run()方法之后,下面看一下Pinger(pingStrategy).runPinger()的执行流程:注意看canSkipPing()方法:private boolean canSkipPing() { if (ping == null || ping.getClass().getName().equals(DummyPing.class.getName())) { // default ping, no need to set up timer return true; } else { return false; }默认IPing的实现是DummyPing,在启动做Ping的Timer会通过BaseLoadBalancer#canSkipPing()方法判断是否要跳过Timer的启动,当ping为空或者为DummyPing是跳过,所以默认不会启动Timer去每10s做一次IPing操作;然而Eureka和Ribbon整合之后,EurekaRibbonClientConfiguration(spring-cloud-netflix-eureka-client包下)类中新定义了一个IPing(NIWSDiscoveryPing),此时会启动Timer每10s做一次ping操作。三、总结 以及 后续文章到这里针对Ribbon的整个执行流程我们也就讨论完了,大体执行流程图下:后文章,我们将讨论Ribbon内置的那些负载均衡算法是如何实现的?
@[toc]一、前言在前面的文章,博主聊了Ribbon如何与SpringCloud、Eureka集成,Ribbon如何自定义负载均衡策略:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)前面我们学会了怎么使用Ribbon,那么为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?SpringCloud是如何集成Ribbon的?Ribbon如何作用到RestTemplate上的?如何获取到的ILoadBalancer?本文就这几个问题展开讨论。PS: 文章中涉及到的SpringBoot相关知识点,比如自动装配,移步博主的SpringBoot专栏:Spring Boot系列。PS2:Ribbon依赖Spring Cloud版本信息如下:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.7.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>==下面以请求http://localhost:9090/say/saint为入口进行debug。==二、@LoadBalanced注解原理这里我们就来看看为什么采用@Bean方法将RestTemplate注入到Spring容器时,加上@LoadBalanced注解就可以实现负载均衡?1、找找@LoadBalanced注解在哪@LoadBalanced在org.springframework.cloud.client.loadbalancer包下,属于spring-cloud-commons项目;嗯,然后呢?既然是SpringCloud项目,二话不说开找自动装配类XXXAutoConfiguration 或 配置类XXXConfiguration。上面说了@LoadBalanced属于spring-cloud-commons项目,找到其jar包下/META-INF/spring.factories文件(为啥找这个文件呢,见SpringBoot自动装配机制原理):既然是负载均衡,我们从类的命名来推测,锁定AsyncLoadBalancerAutoConfiguration类和LoadBalancerAutoConfiguration类;这两个类选哪个呢,“小公鸡点到谁就是谁”?还是看命名,AsyncLoadBalancerAutoConfiguration中有Async,是异步负载均衡请求的;我们看同步,同步好debug,进一步锁定到LoadBalancerAutoConfiguration类。1)@LoadBalanced流程图总述2)LoadBalancerAutoConfiguration自动装配类进到类里,我们发现它组合了一个RestTemplate集合,即:我们创建的那个RestTemplate实例(被@LoadBalanced注解标注)会放到这里来!此处细节涉及到SpringBoot的源码,为了避免偏题,本文仅提供一种思路,不做详细解释;在LoadBalancerAutoConfiguration类中会注入一个SmartInitializingSingleton实例;SmartInitializingSingleton接口中只有一个afterSingletonsInstantiated()方法;在SpringBoot启动过程中,当RestTemplate实例化完之后,会执行这个方法,做如下操作:遍历每个RestTemplate实例,然后再遍历所有的RestTemplateCustomizer对每个RestTemplate实例做定制化操作,即添加拦截器LoadBalancerInterceptor操作。1> RestTemplateCustomizer从哪里来的?还是在LoadBalancerAutoConfiguration类中会通过@Bean方法注入RestTemplateCustomizer;2> LoadBalancerInterceptor拦截器RestTemplateCustomizer所做的定制化就是给RestTemplate添加一个LoadBalancerInterceptor 拦截器;LoadBalancerInterceptor和RestTemplateCustomizer都在LoadBalancerAutoConfiguration的静态内部类LoadBalancerInterceptorConfig中,在SpringBoot启动流程中我们知道,同一个类中@Bean方法的加载是从上至下的,所以肯定是LoadBalancerInterceptor先加载到Spring容器中。此外,LoadBalancerAutoConfiguration类中还会自动装配一些Retry....相关的类,用于请求重试。看到这,我们知道了Ribbon如何作用到RestTemplate上!但是好像还差一点东西,我要通过RestTemplate做一个操作时,入口在哪?2、RestTemplate执行请求时的入口上面我们说到,针对每一个RestTemplate,都会给其添加一个LoadBalancerInterceptor拦截器,所以我们对RestTemplate执行某个操作时,会被LoadBalancerInterceptor所拦截。LoadBalancerInterceptor中组合了LoadBalancerClient,通过LoadBalancerClient做负载均衡;然而想要将LoadBalancerInterceptor注入到Spring容器,需要先将LoadBalancerClient注入到Spring容器。那么LoadBalancerClient是何时注入的?1)LoadBalancerClient何时注入到Spring容器去哪找?我去哪找?既然我们集成了netflix-ribbon,找一下以netflix-ribbon命名的jar包;找到spring-cloud-netflix-ribbonjar包;老规矩,SpringClout项目直接就开找自动装配类XxxAutoConfiguration,自动装配类找不到注入Bean,再找配置类XxxConfiguration。找到RibbonAutoConfiguration类,其中会注入LoadBalancerClient:注意@AutoConfigureAfter注解和@AutoConfigureBefore注解,其表示:RibbonAutoConfiguration类加载要发生在LoadBalancerAutoConfiguration类加载之前、发生在EurekaClientAutoConfiguration类加载之后。到这里,LoadBalancerClient实例比定在LoadBalancerAutoConfiguration加载之前已经注入到了Spring容器,我们回到LoadBalancerAutoConfiguration类,看其中注入LoadBalancerInterceptor类到Spring容器的地方;2)LoadBalancerInterceptor#intercept()方法拦截请求LoadBalancerInterceptor类实现ClientHttpRequestInterceptor接口,其中只有一个核心方法:intercept()用于拦截通过RestTemplate执行的请求。在intercept()方法中,会基于LoadBalancerRequestFactory创建出来一个对RestTemplate请求包装后的请求,并将请求转发给组合的LoadBalancerClient接口的实现类RibbonLoadBalancerClient#execute()方法去执行;这里我们也就知道了真正执行RestTemplate请求方法的入口是RibbonLoadBalancerClient#execute()。下面我们就继续来看RibbonLoadBalancerClient#execute()里面都做了什么?3、RibbonLoadBalancerClient执行请求从LoadBalancerInterceptor#intercept()方法进入到RibbonLoadBalancerClient#execute()方法代码执行流程如下:最终进入到RibbonLoadBalancerClient#execute()方法中会做三件事:根据服务名从Ribbon自己的Spring子上下文中获取服务名对应的ApplicationContext,进而获取到ILoadBalancer;根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server。拼装真正的请求URI,做HTTP请求调用。本文我们重点看一下如何获取到ILoadBalancer?1)获取ILoadBalancer流程图总述2)如何获取到ILoadBalancer?进入到#RibbonLoadBalancerClient#getLoadBalancer(String serviceId)方法;protected ILoadBalancer getLoadBalancer(String serviceId) { // 通过SpringClientFactory来获取对应的LoadBalancer return this.clientFactory.getLoadBalancer(serviceId); }其将请求交给SpringClientFactory的getLoadBalancer(String)方法处理:看SpringClientFactory的类图:SpringClientFactory继承自NamedContextFactory,所以super.getInstance(name, type)方法为NamedContextFactory#getInstance()方法:SpringClientFactory不是spring包下的,而是spring cloud与ribbon整合代码的包(org.springframework.cloud.netflix.ribbon)下的;其对spring进行了一定程度上的封装,从spring里面获取bean的入口,都变成了这个spring cloud ribbon自己的SpringClientFactory;也就是说:对于每个服务名称,都会有一个独立的spring的ApplicationContext容器(体现在NamedContextFactory类的contexts属性中);ApplicationContext中包含了自己这个服务的独立的一堆的组件,比如说LoadBalancer;如果要获取一个服务对应的LoadBalancer,其实就是在自己的那个ApplicationContext里获取LoadBalancer接口类型的实例化Bean;默认可以通过父类NamedContextFactory的getLoadBalacner()方法获取到ILoadBalancer接口对应的实例ZoneAwareLoadBalancer。即获取到的ILoadBalancer为ZoneAwareLoadBalancer。1> 为什么默认实例化的ILoadBalancer是ZoneAwareLoadBalancer?在spring-cloud-netflix-ribbonjar包下找到RibbonClientConfiguration类,RibbonClientConfiguration类中加载了的ILoadBalancer的实例bean --> ZoneAwareLoadBalancer:默认的LoadBalancer是ZoneAwareLoadBalancer,ZoneAwareLoadBalancer类图如下:ZoneAwareLoadBalancer继承自DynamicServerListLoadBalancer,DynamicServerListLoadBalancer继承自BaseLoadBalancer。三、后续文章下一篇文章,我们继续分析:ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?
@[TOC]一、前置知识在前一篇文章【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)我们讨论了SpringCloud如何集成Eureka和Ribbon,本文就在其基础上讨论一下如何自定义Ribbon的负载均衡策略、以及Ribbon的核心API。二、Ribbon核心API博主习惯性的在深入研究一门技术的时候去GitHub上看文档,然而Ribbon在GitHub上的文档(https://github.com/Netflix/ribbon)真的是没啥可看的;就给了一个demo和Release notes。Ribbon有三个核心接口:ILoadBalancer、IRule、IPing,其中:ILoadBalancer是负载均衡器;IRule 复杂负载均衡的规则,ILoadBalancer根据其选择一个可用的Server服务器;IPing负责定时ping每个服务器,判断其是否存活。三、自定义负载均衡策略IRule1、编写IRule实现类MyRule重写IRule的choose(Object o)方法,每次都访问List<Server>中第一个服务实例;import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.Server; import java.util.List; * 自定义负载均衡规则,只用第一个实例; * @author Saint public class MyRule implements IRule { private ILoadBalancer loadBalancer; @Override public Server choose(Object o) { final List<Server> allServers = this.loadBalancer.getAllServers(); return allServers.get(0); @Override public void setLoadBalancer(ILoadBalancer iLoadBalancer) { this.loadBalancer = iLoadBalancer; @Override public ILoadBalancer getLoadBalancer() { return loadBalancer; }==注意:一般很少需要自己定制负载均衡算法的,除非是类似hash分发的那种场景,可以自己写个自定义的Rule,比如说,每次都根据某个请求参数,分发到某台机器上去。不过在分布式系统中,尽量减少这种需要hash分发的情况。==下面我接着看如何把自定义的MyRule应用到指定的服务上 或 全部服务上。2、编写Ribbon配置类在Ribbon配置类中通过@Bean注解将自定义的IRule实现类MyRule注入到Spring容器中。import com.netflix.loadbalancer.IRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; * 自定义Ribbon配置 * @author Saint @Configuration public class MyRibbonConfiguration { @Bean public IRule getRule() { return new MyRule(); }3、应用到全部服务上(Ribbon全局配置)Ribbon全局配置有两种方式:一种是依赖Spring的自动扫描、一种是依赖@RibbonClients注解。1)Spring的自动扫描所谓Spring的自动扫描,就是将自定义的Ribbon配置类放在Spring容器可以扫描到的包目录下即可。如上图所示,程序的启动类RibbonFeignSampleConsumerApplication所在的目录为com.saint,Ribbon配置类MyRibbonConfiguration 所在的目录为com.saint.config;又因没有指定包扫描的路径,所以目录会扫描启动类所在的包com.saint,因此Spring可以自动扫描到MyRibbonConfiguration、进而扫描到MyRule。注意:Ribbon的配置类一定不能Spring扫描到。因为Ribbon有自己的子上下文,Spring的父上下文如果和Ribbon的子上下文重叠,会有各种各样的问题。比如:Spring和SpringMVC父子上下文重叠会导致事务不生效。==所以不推荐使用这种方式。==2)@RibbonClients注解在启动类所在目录的父目录(com.saint)中新建config文件夹(com.config),并将MyRibbonConfiguration类移动到其中,代码目录结构如下:这样操作之后,Ribbon配置类讲不会被Spring扫描到。所以需要利用@RibbonClients注解做一些配置;在com.saint.config目录下新增GreetingServiceRibbonConf类:package com.saint.config; import com.config.MyRibbonConfiguration; import org.springframework.cloud.netflix.ribbon.RibbonClients; import org.springframework.context.annotation.Configuration; * @author Saint @Configuration @RibbonClients(defaultConfiguration = MyRibbonConfiguration.class) public class GreetingServiceRibbonConf { }3、应用到指定服务上(Ribbon局部配置)针对Ribbon局部配置,有两种方式:代码配置 和 属性配置,上面提到的@RibbonClients就属于代码配置的方式,区别在于Ribbon局部配置使用的是@RibbonClient注解;1)代码配置 -- @RibbonClient将GreetingServiceRibbonConf类的内容修改如下:package com.saint.config; import com.config.MyRibbonConfiguration; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.context.annotation.Configuration; * 自定义 调用greeting-service服务时 使用的配置 * @author Saint @Configuration @RibbonClient(name = "GREETING-SERVICE", configuration = MyRibbonConfiguration.class) public class GreetingServiceRibbonConf { }当然我们也可以不使用GreetingServiceRibbonConf作为一个配置类,直接将@RibbonClient(name = "GREETING-SERVICE", configuration = MyRibbonConfiguration.class)加在启动类中也是一样的。2)属性配置 -- application.yml首先将GreetingServiceRibbonConf类中的内容全部注释掉:然后在application.yml文件中添加如下内容:# GREETING-SERVICE为要调用的微服务名 GREETING-SERVICE: ribbon: NFLoadBalancerRuleClassName: com.saint.config.MyRule3)两种方式对比:代码配置:基于代码、更加灵活;但是线上修改得重新打包、发布,并且还有小坑(父子上下文问题)属性配置: 配置更加直观、优先级更高(相对代码配置)、线上修改无需重新打包、发布;但是极端场景下没有代码配置方式灵活。注意:如果代码配置和属性配置两种方式混用,属性配置优先级更高。4)细粒度配置-最佳实践:尽量使用属性配置,属性方式实现不了的情况下再考虑代码配置。同一个微服务内尽量保持单一性,使用同样的配置方式,避免两种方式混用,增加定位代码的复杂性。4、使用浏览器进行调用服务消费者结合博文:【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon),我们已经依次启动了eureka-server、ribbon-feign-sample-8081、ribbon-feign-sample-8082、ribbon-feign-sample-consumer;三个服务、四个实例。此处我们针对服务消费者ribbon-feign-sample-consumer做四次接口调用,分别为:http://localhost:9090/say/sainthttp://localhost:9090/say/saint2http://localhost:9090/say/saint3http://localhost:9090/say/saint4然后我们去看ribbon-feign-sample-8081、ribbon-feign-sample-8082的控制台输出:1> ribbon-feign-sample-8081控制台输出:2> ribbon-feign-sample-8082控制台输出:3> 结果说明:我们可以发现,四个请求,ribbon-feign-sample-8082实例处理了所有的请求,我们自定义的IRule已经生效。四、自定义服务实例是否存活判定策略IPing和IRule的自定义方式一样,这里只提供自定义的IPing,具体配置方式和IRule一样。1、自定义IPingMyPing表示实例永不失活,因为其isAlive(Server server)永远返回TRUE。package com.saint.config; import com.netflix.loadbalancer.IPing; import com.netflix.loadbalancer.Server; * 自定义IPing,判断每个服务是否还存活 * @author Saint public class MyPing implements IPing { @Override public boolean isAlive(Server server) { return true; }2、修改Ribbon配置类package com.config; import com.netflix.loadbalancer.IPing; import com.netflix.loadbalancer.IRule; import com.saint.config.MyPing; import com.saint.config.MyRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; * 自定义Ribbon配置 * @author Saint @Configuration public class MyRibbonConfiguration { @Bean public IRule getRule() { return new MyRule(); @Bean public IPing getPing() { return new MyPing(); }五、性能优化-饥饿加载Ribbon默认是懒加载微服务,所以第一次调用特别慢,我们可以修改饥饿加载。ribbon: eager-load: # 开启饥饿加载 enabled: true # 开启饥饿加载的微服务列表,多个以,分隔 clients: user-center,xxx
@[TOC]1、SpringBoot 和 Spring Cloud版本依赖关系以下内容均体现在Spring Cloud官网(https://spring.io/projects/spring-cloud)。0)Spring Cloud版本名变更从2020.0.X版本开始,Spring Cloud版本的命名方式修改为时间线的方式。而SpringCloud之前的版本名称是伦敦地铁站的站名命名,且首字母顺序与版本时间顺序一致,如:AngelBrixtonCamdenDalstonEdgwareFinchleyGreenwichHoxton还是伦敦地铁站的站名命名版本时,当SpringCloud的发布内容积累到临界点或者一个重大Bug被解决后,会发布一个"Service Releases"版本,简称"SR"版本(参考官网:https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-Hoxton-Release-Notes)。其中也包括相关组件的版本,比如:Spring Cloud Netflix 2.2.9 RELEASE。而从2020.0.X版本开始,则是数字递增的方式:==SpringCloud与SpringBoot的版本对应关系,可以通过以下三种方式来确定:==1)SpringCloud发布版本与SpringBoot版本兼容性的表格表中描述的是一个版本范围;比如与SpringCloud Hoxton版本适配的SpringBoot版本应该是2.2.x版本 或 2.3.x(SR5开始以上)的版本。2)访问https://start.spring.io/actuator/infoJSON格式化后的Spring Cloud版本内容如下:3)Spring Cloud参考文章中会推荐使用Spring Boot版本这种方式最精准。2、SpringCloud 和 SpringCloudAlibaba版本对应关系spring Cloud Alibaba官方版本声明:https://github.com/alibaba/spring-cloud-alibaba/wiki。注意:2021.x分支 Spring Cloud Alibaba 版本命名方式进行了调整, 未来将对应 Spring Cloud 版本, 前三位为 Spring Cloud 版本,最后一位为扩展版本,比如适配 Spring Cloud 2021.0.1 版本对应的 Spring Cloud Alibaba 第一个版本为:2021.0.1.0,第个二版本为:2021.0.1.1,依此类推)2)Spring Cloud alibaba 组件版本关系3、依赖管理Spring Cloud Alibaba BOM 中包含了它所使用的所有依赖的版本。我们只需要在<dependencyManagement>标签中 添加如下内容:<project> ..... <properties> <java.version>1.8</java.version> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version> </properties> <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> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>此处是博主研究Spring Cloud Alibaba的版本(亲测很稳):补充和朋友聊在企业中的版本使用情况,目前(2022-6月)中小型公司大多使用的版本:<spring-boot.version>2.3.7.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>有企业踩坑使用过的最新且稳定版本:<spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>SpringCloudAlibaba官方给的最新版本:<spring-boot.version>2.6.3</spring-boot.version> <spring-cloud.version>2021.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
一、前言前一段时间在公司写了一个链路追踪的服务,其中SpringMVC做为门面对外提供服务,微服务之间采用Dubbo接口调用。对于Dubbo接口之间传递链路信息,采用RpcContext将需要的参数透传过去。然而在使用RpcContext时遇到了几个问题导致RpcContext未按我设想的方式传递。二、RpcContext介绍RpcContext 本质上是一个使用 ThreadLocal 实现的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A调B,B再调C,则B机器上,在B调C之前,RpcContext记录的是A调B的信息,在B调C之后,RpcContext记录的是B调C的信息。RpcContext使用ThreadLocal的部分源码如下:public class RpcContext { * 存放主体内容 private static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() { protected RpcContext initialValue() { return new RpcContext(); * 获取RpcContext信息 public static RpcContext getContext() { return (RpcContext)LOCAL.get(); * 清空RpcContext信息 public static void removeContext() { LOCAL.remove(); }注意:不同Dubbo版本的RpcContext略有区别,本质上都是使用的ThreadLocal。二、RpcContext的使用//服务提供方使用,获取参数 RpcContext.getContext().getAttachments() //服务器消费方使用,设置参数 RpcContext.getContext().setAttachment() 1> 消费端(DubboConsumer):// 远程调用之前,通过attachment传KV给提供方 RpcContext.getContext().setAttachment("userKey", "userValue"); // 远程调用 xxxService.xxx(); // 此时 RpcContext 的状态已变化 RpcContext.getContext(); 2> 服务端(DubboProvider):public class XxxServiceImpl implements XxxService { public void xxx() { // 通过RpcContext获取用户传参,这里会返回userValue String value = RpcContext.getContext().getAttachment("userKey"); // 本端是否为提供端,这里会返回true boolean isProviderSide = RpcContext.getContext().isProviderSide(); // 获取调用方IP地址 String clientIP = RpcContext.getContext().getRemoteHost(); // 获取当前服务配置信息,所有配置信息都将转换为URL的参数 String application = RpcContext.getContext().getUrl().getParameter("application"); } 三、结合Filter使用RpcContext一般修改RpcContext信息都是在Dubbo的拦截器中,这样有两个好处:统一入口设置参数,方便维护。解决一次完整请求调用涉及多次嵌套RPC调用时获取不到上下文中设置的参数值问题。例如:1> 在DubboConsumerFilter中设置RpcContext信息:@Slf4j @Activate(group = {CommonConstants.CONSUMER}) public class DubboConsumerFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { try { // 服务端从dubbo上下文中取出rpcContext信息 String jsonStr = null; if (!CollectionUtils.isEmpty(RpcContext.getContext().getObjectAttachments())) { jsonStr = RpcContext.getContext().getAttachment(HttpHeaderKeys.APP_NAME.getKey()); // 获取不到rpcContext信息,则手动塞入 if (StringUtils.isEmpty(jsonStr)) { RpcContext.getContext().setAttachment("userKey", "saint"); } catch (Exception e){ log.error("Exception in process DubboConsumerFilter" ,e); // do nothing return invoker.invoke(invocation); }2> 在DubboProviderFilter中获取RpcContext信息:@Slf4j @Activate(group = {CommonConstants.PROVIDER}) public class DubboProviderFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 服务端从dubbo上下文中取出traceContext信息 String jsonStr = null; if (!CollectionUtils.isEmpty(RpcContext.getContext().getObjectAttachments())) { jsonStr = RpcContext.getContext().getAttachment(HttpHeaderKeys.APP_NAME.getKey()); log.info("DubboProviderFilter get dubbo RpcContext is : {}", jsonStr); return invoker.invoke(invocation); }四、使用RpcContext的坑1、一个dubbo接口调用多个dubbo接口,RpcContext会改变一个dubbo接口同步调用多个dubbo接口(比如Dubbo接口B和Dubbo接口C)时,在调用Dubbo接口C时,RPCContext已经发生改变了,需要重新获取调用链路信息。原因分析见后面的RpcContext原理。参考解决方案可以采用ThreadLocal保存,然后在ProviderFilter中清除ThreadLocal,防止数据错乱,但是会造成一定的内存泄漏(数据量较小是可以接收的)。0> 用于存储调用链路信息的ThreadLocal:import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; * 链路信息上下文 * @author Saint public class RpcTraceContext implements Serializable { private final static ThreadLocal<TraceContext> traceContextHolder = new ThreadLocal<>(); public static ThreadLocal<TraceContext> get() { return traceContextHolder; * 设置traceContext * @param traceContext traceContext public static void setTraceContext(TraceContext traceContext) { traceContextHolder.set(traceContext); * 获取traceContext * @return traceContext public static TraceContext getTraceContext() { return traceContextHolder.get(); * 清空trace上下文 public static void clear() { traceContextHolder.remove(); @Data @Accessors(chain = true) public static class TraceContext implements Serializable { private Long userId; private String traceId; private String controllerAction; private String visitIp; private String appName; }1> DubboProviderFilter:@Slf4j @Activate(group = {CommonConstants.CONSUMER}) public class DubboConsumerFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { try { //服务端从dubbo上下文中取出traceContext信息 String jsonStr = null; if (!CollectionUtils.isEmpty(RpcContext.getContext().getAttachments())) { jsonStr = RpcContext.getContext().getAttachment("TRACE_CONTEXT"); if (StringUtils.isNotEmpty(jsonStr)) { // 这里是为了解决duboo接口调用多个dubbo接口,第一个dubbo接口之后的dubbo接口获取到的RpcContext为空的问题。 RpcTraceContext.TraceContext traceContext = JSON.parseObject(jsonStr, RpcTraceContext.TraceContext.class); RpcTraceContext.setTraceContext(traceContext); } catch (Exception e){ log.error("Exception in process DubboConsumerFilter" ,e); return invoker.invoke(invocation); }2> DubboProviderFilter:@Slf4j @Activate(group = {CommonConstants.PROVIDER}) public class DubboProviderFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { RpcTraceContext.clear(); // 服务端从dubbo上下文中取出traceContext信息 String jsonStr = null; if (!CollectionUtils.isEmpty(RpcContext.getContext().getAttachments())) { jsonStr = RpcContext.getContext().getAttachment("TRACE_CONTEXT"); return invoker.invoke(invocation); }2、异步调用的两个坑1)异步调用依赖传递性问题表现:如果consumer-A异步调用provider-B,而provider-B本身又调用了provider-C。当provider-B调用provider-C时,会变成异步。问题原因:是否异步调用取决于RpcContext中async的值,其次才是服务本身的配置。当A调用B时,会把async=true传给B的RpcContext;B调用C时,虽然服务本身async=false,但RpcContext中async=true,自然也就成了异步调用。2)异步回调返回null问题表现:consumer-A调用provider-B,而provider-B本身又调用了provider-C。consumer-A调用provider-B返回null。问题原因:异步调用直接返回空的RpcResult,需要后序通过RpcContext.getContext().getFuture() .get()获取返回值。async透传到provider-B端之后,也是异步调用provider-C,但是直接返回空的RpcResult给consumer-A。3)解决方案不让async参数应用到provider端。需要修改ContextFilter源码,重写RpcContext时删除async参数;五、RpcContext原理首先RpcContext内部有一个ThreadLocal变量(高版本用的InternalThreadLocal本质上也是ThreadLocal),它是作为ThreadLocalMap的key,表明每个线程有一个RpcContext。其次Dubbo内嵌了两Filter,分别为:ContextFilter、ConsumerContextFilter,分别用来拦截Dubbo服务提供者和消费者。1、ConsumerContextFilter消费端在执行Rpc调用之前,经过Filter处理, 会将一些信息(比如:服务调用信息)写入RpcContext。2、ContextFilter服务端在执行调用之前,也会经过Filter处理,将信息写入RpcContext;最后清空RpcContext。
一、前言前一段时间做了一个日志审计模块,其中会对HTTP调用、Dubbo接口之前做链路追踪,针对HTTP调用Dubbo接口、Dubbo接口中调用Dubbo接口的场景采用自定义Dubbo Filter(Provider/Consumer)的方式传递链路入口信息、操作用户、链路ID。其中牵扯到Dubbo RpcContext的使用,对着RpcContext以及遇到的坑,在下一篇文章中讨论;二、自定义Filter官方文档:https://dubbo.apache.org/zh/docsv2.7/dev/impls/filter/。我们注意到在https://mvnrepository.com/artifact/org.apache.dubbo/dubbo中,org.apache.dubbo组织下的dubbo版本最低只有2.7.0;<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.0</version> </dependency>那么dubbo2.7.0以下的版本呢?在mvnrepository中搜索dubbo,我们可以看到还存在一个com.alibaba组织下的dubbo;细想一下,dubbo最开始是没开源的;看来dubbo2.7.0之下的版本应该依赖都在com.alibaba组织下:谁闲着没事还会去看老版本呢?巧了老项目用dubbo2.6.X、dubbo2.5.X都太正常了;博主就遇到了,业务方引入我的日志审计SDK说,说用不了;博主作为一个新程序员,学的基本都是新版本、新技术,也只有通过老项目才会去了解老版本;在此总结一下。1、最新版本自定义Filter(dubbo2.7.X及以上版本)1)实现Filterpackage com.saint.dubbo; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; * @author Saint @Slf4j @Activate(group = {CommonConstants.PROVIDER}) public class DubboProviderFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 服务端从dubbo上下文中取出traceContext信息 String jsonStr = null; ...... return invoker.invoke(invocation); }2)Filter实现类映射到Spring容器在resources目录下增加META-INF/dubbo/org.apache.dubbo.rpc.Filter文件;内容如下:providerFilter=com.saint.dubbo.DubboProviderFilter其中:providerFilter 为将要在dubbo配置文件或yml文件中配置的Filter名,想写啥写啥;com.saint.dubbo.DubboProviderFilter为我们自定义的Filter类;假如我要写多个自定义的Filter呢?官方这里并没有说,很多博文也没说;其实再加一行就行。providerFilter=com.saint.dubbo.DubboProviderFilter consumerFilter=com.saint.dubbo.DubboConsumerFilter3)Filter实现类关联到Dubbo Consumer / Provider这里有两种方式(推荐使用第二种):1> 在dubbo的xml配置文件中添加如下配置:<dubbo:provider filter="providerFilter" /> <dubbo:consumer filter="consumerFilter" />2> 在property/yaml配置文件中的添加如下配置:dubbo: consumer: filter: consumerFilter provider: filter: providerFilter如果针对一个consumer或provider有多个filter呢?以英文,隔开即可;dubbo: consumer: filter: consumerFilter,userConsumerFilter provider: filter: providerFilter2、未开源前的版本自定义Filter(dubbo2.6.X及以下版本)Dubbo2.6.X及以下版本 与 Dubbo2.7.X及以上版本 在代码实现上的唯一区别点在于实现Filter的方式;Filter实现类映射到Spring容器的方式 和 Filter实现类关联到Dubbo Consumer / Provider的方式均一样。1)实现Filterpackage com.saint.dubbo; import lombok.extern.slf4j.Slf4j; import com.alibaba.dubbo.common.Constants; import com.alibaba.dubbo.common.extension.Activate; import com.alibaba.dubbo.rpc.*; * @author Saint @Slf4j @Activate(group = {Constants.CONSUMER}) public class DubboProviderFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 服务端从dubbo上下文中取出traceContext信息 String jsonStr = null; ...... return invoker.invoke(invocation); }这里和2.7.X及以上版本的区别如下:主要是引的包不一样;Dubbo2.6.X及以下版本使用的是com.alibaba.dubbo包,而Dubbo2.7.X及以上版本使用的是org.apache.dubbo包;其次,指定@Activate的group属性时,常量类不一样;Dubbo2.6.X及以下版本使用Constants常量类,Dubbo2.7.X及以上版本使用CommonConstants接口; 如下操作和Dubbo2.7.X一毛一样!!!2)Filter实现类映射到Spring容器在resources目录下增加META-INF/dubbo/org.apache.dubbo.rpc.Filter文件;内容如下:providerFilter=com.saint.dubbo.DubboProviderFilter其中:providerFilter 为将要在dubbo配置文件或yml文件中配置的Filter名,想写啥写啥;com.saint.dubbo.DubboProviderFilter为我们自定义的Filter类;假如我要写多个自定义的Filter呢?官方这里并没有说,很多博文也没说;其实再加一行就行。providerFilter=com.saint.dubbo.DubboProviderFilter consumerFilter=com.saint.dubbo.DubboConsumerFilter3)Filter实现类关联到Dubbo Consumer / Provider这里有两种方式(推荐使用第二种):1> 在dubbo的xml配置文件中添加如下配置:<dubbo:provider filter="providerFilter" /> <dubbo:consumer filter="consumerFilter" />2> 在property/yaml配置文件中的添加如下配置:dubbo: consumer: filter: consumerFilter provider: filter: providerFilter如果针对一个consumer或provider有多个filter呢?以英文,隔开即可;dubbo: consumer: filter: consumerFilter,userConsumerFilter provider: filter: providerFilter三、总结dubbo之所以有两种实现方式的根本原因,在于2018年2月份dubbo开源(阿里捐献给了Apache)后group由com.alibaba变为了org.apache;此外还有一些代码上的优化:比如 Dubbo2.6.X及以下版本常量采用Class类的静态常量维护,而Dubbo2.7.X及以上版本常量采用Interface接口中维护常量。希望这边文章对同样维护老项目的兄弟有所帮助。
前言1、为什么说Eureka是CAP理论中的AP?从CAP理论看,Eureka是一个AP系统,其优先保证可用性(A)和分区容错性(P),不保证强一致性(C),但能做到最终一致性。因为只要集群中任意一个实例不出现问题,Eureka服务就是可用的;即Eureka Client 在向某个 Eureka Server 注册时,如果发现连接失败,则会自动切换至其它节点;另外Eureka集群中没有主从的概念,各个节点都是平等的,节点间采用Replicate异步的方式来同步数据;也正因为Eureka 本身不保证数据的强一致性,所以在架构又设计了很多缓存。2、为什么要设计那么多缓存(三级缓存)?这个和MySQL用主从做读写分离很相似,数据库层面的读写分离是为了分摊主数据库的读压力;而Eureka的三级缓存也是为了做读写分离,使写操作不阻塞读操作。因为在写的时候,线程会持有ConcurrentHashmap相应Hash桶节点对象的锁,阻塞同一个Hash桶的其他读线程。这样可以有效的降低读写并发,避免读写读写争抢资源所带来的压力。那么加了那么多缓存,如何保证缓存数据的最终一致性?我们下面就详细聊一下Eureka是如何做的。缓存机制Eureka Server中有三个变量用来保存服务注册信息,分别是:registry、readWriteCacheMap、readOnlyCacheMap;默认情况下定时任务每30s将二级缓存readWriteCacheMap同步至三级缓存readOnlyCacheMap;1、Eureka Server每60s清理超过90s * 2(官方彩蛋)未续约的节点;2、Eureka Client每30s从readOnlyCacheMap更新服务注册信息;3、UI界面则从registry获取最新的服务注册信息。1、三级缓存分别是什么?缓存缓存类型所处类概述registry一级缓存ConcurrentHashMapAbstractInstanceRegistry实时更新,又名注册表,UI界面从这里获取服务注册信息;readWriteCacheMap<br/>二级缓存Guava Cache(LoadingCache)ResponseCacheImpl实时更新,缓存时间180秒;readOnlyCacheMap<br/>三级缓存ConcurrentHashMapResponseCacheImpl周期更新,默认每30s从二级缓存readWriteCacheMap中同步数据更新;Eureka Client默认从这里获取服务注册信息,可配为直接从readWriteCacheMap获取二级缓存又称读写缓存、三级缓存又称只读缓存;1)Eureka Server缓存相关配置配置名默认值概述eureka.server.useReadOnlyResponseCachetrueClient从readOnlyCacheMap更新数据,false则跳过readOnlyCacheMap直接从readWriteCacheMap更新eureka.server.responsecCacheUpdateIntervalMs30000readWriteCacheMap更新至readOnlyCacheMap的周期,默认30s2、缓存之间的数据同步AbstractInstanceRegistry类中的registry字段为注册表、并与保存一级缓存(实时最新数据);ResponseCacheImpl类中的readWriteCacheMap字段和readOnlyCacheMap字段分别表示二级缓存和三级缓存;下面我们就围绕这两个类、三个字段的数据同步展开讨论。1)注册一个服务实例Eureka Server中做的操作:向注册表registry中写入服务实例信息,并使得二级缓存失效;Eureka client第一次向Eureka Server注册服务或者发送心跳续约时,会进去到Eureka Serve中ApplicationResource#addInstance()方法中:最终进入到AbstractInstanceRegistry#register()方法中,往其registry字段中添加服务注册信息:这里以服务注册为例,我们知道了registry的数据来源;注意,在做服务注册的最后会将二级缓存清空/失效;下面我们接着来看一下Eureka的二级缓存和三级缓存是如何运作的?2)二级/三级缓存什么时候初始化?Eureka Server启动的时候会根据SpringBoot自动装配的特性,初始化EurekaServerContext接口的实现类DefaultEurekaServerContext;在DefaultEurekaServerContext初始化时会执行构造器后置逻辑initialize(),其中会初始化注册中心;接着进入到PeerAwareInstanceRegistry接口的实现类PeerAwareInstanceRegistryImpl#init()方法中,其中会通过调用initializedResponseCache()方法初始化二级/三级缓存;3)二级/三级缓存初始化都做了什么?这里可以看到initializedResponseCache()方法中直接new了一个ResponseCacheImpl类;我们接着进入到ResponseCacheImpl类中,看一下它的构造函数:总的来说,在初始化ResponseCacheImpl类时:会设置初始化二级缓存readWriteCacheMap(过期时间180s),设置二级缓存往三级缓存readOnlyCacheMap同步的时间间隔(默认30s)。设置是否使用三级缓存(默认使用),如果使用则启动一个定时任务,默认每隔30s从二级缓存中同步数据到三级缓存(只更新三级缓存中已存在的key);4、发现/寻找一个服务针对Eureka Client和UI界面,他们读取的服务注册信息的方式略有不同:针对Eureka Client:1、如果使用只读缓存(三级缓存)<默认使用>,则先从只读缓存中获取;如果获取不到,则从读写缓存(二级缓存)中获取,并将数据缓存到只读缓存;2、不使用只读缓存,则直接从读写缓存中获取;如果获取不到则触发guava的回调函数从注册表registry中同步数据(即从一级缓存 -- 注册表registry中取)而对于UI界面,则是实时从一级缓存(注册表registry)中取。对于Eureka Client无论是获取一个Application的信息(入口为ApplicationResource#getApplication())还是获取所有Application的信息(入口为ApplicationsResource#getContainers())都会进入到ResponseCacheImple#get(Key key)方法,然后进过此路径《get(key, shouldUseReadOnlyResponseCache) --> getValue(final Key key, boolean useReadOnlyCache)》最终走到真正从缓存中读取数据的逻辑:总结1、Eureka Server 在接收Eureka Client注册的时候,会将读写缓存(二级缓存)清空;2、Eureka Server启动时会做两件事:会初始化读写缓存(二级缓存),从注册表registry(一级缓存)中实时加载数据,默认180s过期;判定是否使用只读缓存(三级缓存),默认开启;如果使用则开启一个定时任务,默认每30s做一次读写缓存到只读缓存的数据同步;3、Eureka Client获取服务信息时,默认先从只读缓存获取;获取不到再从读写缓存中获取,并将数据缓存到只读缓存;获取不到,再触发guava的回调函数从注册表中同步(即从一级缓存 -- 注册表registry中取)。1、多级缓存带来的好处?尽可能的避免服务注册出现频繁的读写冲突,写阻塞读;提高Eureka Server服务的读写性能。面对频繁读写资源争抢、写阻塞读等情况,我可以考虑借鉴Eureka的多级缓存方案做读写分离。
@[TOC]一、前言更多内容见Seata专栏:https://blog.csdn.net/saintmm/category_11953405.html至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务分布式事务Seata源码解析八:本地事务执行流程(AT模式下)Seata最核心的全局事务执行流程,上文我们聊了本地事务是如何执行的?在本地事务执行的过程中涉及到分支事务如何注册到全局事务、undo log的构建,本文我们接着聊分支事务如何注册到全局事务。二、RM中分支事务注册入口在上一文(分布式事务Seata源码解析八:本地事务执行流程(AT模式下))中,提到ConnectionProxy#processGlobalTransactionCommit()最终处理本地事务的提交。其中register()方法向远程的TC中注册分支事务:private void register() throws TransactionException { if (!context.hasUndoLog() || !context.hasLockKey()) { return; // 分支事务注册:将事务类型AT、资源ID(资源在前面的流程已经注册过了)、事务xid、全局锁keys作为分支事务信息注册到seata server Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(), null, context.getXid(), context.getApplicationData(), context.buildLockKeys()); context.setBranchId(branchId); }注册分支事务、获取分支事务ID的入口流程如下:DefaultResourceManager.get() 获取单例形式的资源管理器DefaultResourceManager,通过其注册分支事务;再根据分支类型(AT、TCC、XA、SAGA)获取相应类型的ResourceManager;因为存在四种分布式事务的模式(AT、TCC、XA、SAGA),所以此处也正好对应四种ResourceManager:这四种ResourceManager都继承了AbstractResourceManager,并且都没有重写AbstractResourceManager的branchRegister()方法,所以无论是哪种全局事务模式,分支事务注册到全局事务的方式都一样,都体现在AbstractResourceManager的branchRegister()方法中;而分支事务的提交和回滚方式却各不相同。@Override public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid, String applicationData, String lockKeys) throws TransactionException { try { BranchRegisterRequest request = new BranchRegisterRequest(); // xid是全局事务ID request.setXid(xid); // 分布式事务要更新数据的全局锁keys request.setLockKey(lockKeys); // 分支事务对应的资源ID request.setResourceId(resourceId); // 分支事务类型 request.setBranchType(branchType); // 引用的数据 request.setApplicationData(applicationData); // 将请求通过RmNettyRemotingClient发送到seata-server BranchRegisterResponse response = (BranchRegisterResponse) RmNettyRemotingClient.getInstance().sendSyncRequest(request); if (response.getResultCode() == ResultCode.Failed) { throw new RmTransactionException(response.getTransactionExceptionCode(), String.format("Response[ %s ]", response.getMsg())); return response.getBranchId(); } catch (TimeoutException toe) { throw new RmTransactionException(TransactionExceptionCode.IO, "RPC Timeout", toe); } catch (RuntimeException rex) { throw new RmTransactionException(TransactionExceptionCode.BranchRegisterFailed, "Runtime", rex); }方法中构建一个BranchRegisterRequest,通过netty将请求发送到TC进行分支事务的注册;三、TC中处理分支事务注册在【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信一文中,我们聊了Seata Client 如何和Seata Server建立连接、通信;又在【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么一文中,我们知道了TC(Seata Server)启动之后,AbstractNettyRemotingServer的内部类ServerHandler负责接收并处理请求。ServerHandler类上有个@ChannelHandler.Sharable注解,其表示所有的连接都会共用这一个ChannelHandler;所以当消息处理很慢时,会降低并发。processMessage(ctx, (RpcMessage) msg)方法中会根据消息类型获取到 请求处理组件(消息的处理过程是典型的策略模式),如果消息对应的处理器设置了线程池,则放到线程池中执行;如果对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发;所以在seata-server中大部分处理器都有对应的线程池。/** * Rpc message processing. * @param ctx Channel handler context. * @param rpcMessage rpc message. * @throws Exception throws exception process message error. * @since 1.3.0 protected void processMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("%s msgId:%s, body:%s", this, rpcMessage.getId(), rpcMessage.getBody())); Object body = rpcMessage.getBody(); if (body instanceof MessageTypeAware) { MessageTypeAware messageTypeAware = (MessageTypeAware) body; // 根据消息的类型获取到请求处理组件和请求处理线程池组成的Pair final Pair<RemotingProcessor, ExecutorService> pair = this.processorTable.get((int) messageTypeAware.getTypeCode()); if (pair != null) { // 如果消息对应的处理器设置了线程池,则放到线程池中执行 if (pair.getSecond() != null) { try { pair.getSecond().execute(() -> { try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } finally { MDC.clear(); } catch (RejectedExecutionException e) { // 线程池拒绝策略之一,抛出异常:RejectedExecutionException LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(), "thread pool is full, current max pool size is " + messageExecutor.getActiveCount()); if (allowDumpStack) { String name = ManagementFactory.getRuntimeMXBean().getName(); String pid = name.split("@")[0]; long idx = System.currentTimeMillis(); try { String jstackFile = idx + ".log"; LOGGER.info("jstack command will dump to " + jstackFile); Runtime.getRuntime().exec(String.format("jstack %s > %s", pid, jstackFile)); } catch (IOException exx) { LOGGER.error(exx.getMessage()); allowDumpStack = false; } else { // 对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发; try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } else { LOGGER.error("This message type [{}] has no processor.", messageTypeAware.getTypeCode()); } else { LOGGER.error("This rpcMessage body[{}] is not MessageTypeAware type.", body); }Seata Serer接收到请求的执行链路为:1、BranchRegisterRequest又由于RM发送开启事务请求时的RPCMessage的body为BranchRegisterRequest,所以进入到:又由于在DefaultCoordinator#onRequest()方法中,将DefaultCoordinator自身绑定到了AbstractTransactionRequestToTC的handler属性中:所以会进入到:DefaultCore封装了AT、TCC、Saga、XA分布式事务模式的具体实现类。2、DefaultCore执行分支事务的注册DefaultCore#branchRegister()方法中会首先根据分布式事务模式获取到相应的AbstractCore,这里的处理方式和上面获取分布式事务模式对应的ResourceManager的处理方式一样;因为存在四种分布式事务的模式(AT、TCC、XA、SAGA),所以此处也正好对应四种AbstractCore:这四种Core都继承了AbstractCore,并且都没有重写AbstractResourceManager的branchRegister()方法,所以无论是哪种全局事务模式,分支事务注册到全局事务的方式都一样,都体现在AbstractCore的branchRegister()方法中;@Override public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid, String applicationData, String lockKeys) throws TransactionException { // 根据xid从DB中找到全局事务会话(会做一个DB查询操作) GlobalSession globalSession = assertGlobalSessionNotNull(xid, false); return SessionHolder.lockAndExecute(globalSession, () -> { // 检查全局事务会话的状态 globalSessionStatusCheck(globalSession); globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager()); // 分支事务会话 根据全局事务开启一个分支事务 BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId, applicationData, lockKeys, clientId); // 将branchID放到ThreadLocal中 MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(branchSession.getBranchId())); // 给分支事务加全局锁 branchSessionLock(globalSession, branchSession); try { // 将分支事务会话添加到全局事务会话 globalSession.addBranch(branchSession); } catch (RuntimeException ex) { branchSessionUnlock(branchSession); throw new BranchTransactionException(FailedToAddBranch, String .format("Failed to store branch xid = %s branchId = %s", globalSession.getXid(), branchSession.getBranchId()), ex); if (LOGGER.isInfoEnabled()) { LOGGER.info("Register branch successfully, xid = {}, branchId = {}, resourceId = {} ,lockKeys = {}", globalSession.getXid(), branchSession.getBranchId(), resourceId, lockKeys); return branchSession.getBranchId(); }方法做的事情如下:根据xid从DB / file / redis (由TC的store.mode配置决定)中找到全局事务会话(这里会做一个DB查询操作),并且断言globalSession不许为空;校验全局事务会话,全局事务会话必须是存活的,并且状态必须为:GlobalStatus.Begin;构建一个分支事务会话BranchSession;给分支事务加全局锁,出现锁冲突则直接报错,抛出异常BranchTransactionException;将分支事务会话添加到全局事务会话,持久化分支事务会话;1)获取全局事务会话GlobalSessionSessionHolder是会话管理者,其中包括四个会话管理器:// 用于管理所有的Setssion,以及Session的创建、更新、删除等 private static SessionManager ROOT_SESSION_MANAGER; // 用于管理所有的异步commit的Session,包括创建、更新以及删除 private static SessionManager ASYNC_COMMITTING_SESSION_MANAGER; // 用于管理所有的重试commit的Session,包括创建、更新以及删除 private static SessionManager RETRY_COMMITTING_SESSION_MANAGER; // 用于管理所有的重试rollback的Session,包括创建、更新以及删除 private static SessionManager RETRY_ROLLBACKING_SESSION_MANAGER;用于获取全局事务会话的管理器为:ROOT_SESSION_MANAGER;在初始化SessionHolder时,会根据store.mode对其进行赋值:例如博主的TC采用的store.mode是DB,所以找到:DataBaseSessionManager;DataBaseSessionManager#findGlobalSession()方法如下:==注意:在AbstractCore#branchRegister()方法中查询全局事务会话时,withBranchSessions = false,所以不会把分支事务查出来。==LogStoreLogStore是对全局事务、分支事务做DB操作的DAO层;LogStore接口只有一个实现LogStoreDataBaseDAO,其queryGlobalTransactionDO()方法内容很简单,就直接使用JDBC查表:@Override public GlobalTransactionDO queryGlobalTransactionDO(String xid) { String sql = LogStoreSqlsFactory.getLogStoreSqls(dbType).getQueryGlobalTransactionSQL(globalTable); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = logStoreDataSource.getConnection(); conn.setAutoCommit(true); ps = conn.prepareStatement(sql); ps.setString(1, xid); rs = ps.executeQuery(); if (rs.next()) { return convertGlobalTransactionDO(rs); } else { return null; } catch (SQLException e) { throw new DataAccessException(e); } finally { IOUtil.close(rs, ps, conn); }本文后面相似的DB操作不再赘述。2)校验全局事务会话GlobalSession全局事务会话必须是存活的,并且状态必须为:GlobalStatus.Begin;protected void globalSessionStatusCheck(GlobalSession globalSession) throws GlobalTransactionException { if (!globalSession.isActive()) { throw new GlobalTransactionException(GlobalTransactionNotActive, String.format( "Could not register branch into global session xid = %s status = %s, cause by globalSession not active", globalSession.getXid(), globalSession.getStatus())); if (globalSession.getStatus() != GlobalStatus.Begin) { throw new GlobalTransactionException(GlobalTransactionStatusInvalid, String .format("Could not register branch into global session xid = %s status = %s while expecting %s", globalSession.getXid(), globalSession.getStatus(), GlobalStatus.Begin)); }3)分支事务会话BranchSession加全局锁1> SessionHolder首先构建一个BranchSessionBranchSession的内容包括:全局事务xid、全局事务id、根据雪花算法生成的分支事务id、全局事务模式、RM资源Id、分支事务要加的全局锁keys、RM客户端ID、RM应用名;public static BranchSession newBranchByGlobal(GlobalSession globalSession, BranchType branchType, String resourceId, String applicationData, String lockKeys, String clientId) { BranchSession branchSession = new BranchSession(); branchSession.setXid(globalSession.getXid()); branchSession.setTransactionId(globalSession.getTransactionId()); branchSession.setBranchId(UUIDGenerator.generateUUID()); branchSession.setBranchType(branchType); branchSession.setResourceId(resourceId); branchSession.setLockKey(lockKeys); branchSession.setClientId(clientId); branchSession.setApplicationData(applicationData); return branchSession; }2> branchID放入ThreadLocalMDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(branchSession.getBranchId()));3> *给分支事务加全局锁AbstractCore中的branchSessionLock()方法没有具体的实现,并且在Seata中只有AT模式有全局锁的概念,因此只需要看ATCore的branchSessionLock()方法即可;默认情况下seata在给分支事务加全局锁的同时,会检查全局锁是否冲突;LockStoreDataBaseDAO#acquireLock()获取全局锁LockStoreDataBaseDAO#acquireLock()方法如下:@Override public boolean acquireLock(List<LockDO> lockDOs, boolean autoCommit, boolean skipCheckLock) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; // 已经存在的行锁key集合 Set<String> dbExistedRowKeys = new HashSet<>(); boolean originalAutoCommit = true; if (lockDOs.size() > 1) { lockDOs = lockDOs.stream().filter(LambdaUtils.distinctByKey(LockDO::getRowKey)).collect(Collectors.toList()); try { // 从全局锁数据源里获取到一个连接 conn = lockStoreDataSource.getConnection(); // 把自动提交事务关闭 if (originalAutoCommit = conn.getAutoCommit()) { conn.setAutoCommit(false); List<LockDO> unrepeatedLockDOs = lockDOs; //check lock // 是否跳过锁检查 if (!skipCheckLock) { boolean canLock = true; //query,针对全局锁表查询某个数据 String checkLockSQL = LockStoreSqlFactory.getLogStoreSql(dbType).getCheckLockableSql(lockTable, lockDOs.size()); ps = conn.prepareStatement(checkLockSQL); for (int i = 0; i < lockDOs.size(); i++) { ps.setString(i + 1, lockDOs.get(i).getRowKey()); rs = ps.executeQuery(); // 获取到当前要加全局锁的事务xid String currentXID = lockDOs.get(0).getXid(); boolean failFast = false; // 查询结果为空时,说明没有事务加全局锁 while (rs.next()) { String dbXID = rs.getString(ServerTableColumnsName.LOCK_TABLE_XID); // 如果加全局锁的是其他的全局事务xid if (!StringUtils.equals(dbXID, currentXID)) { if (LOGGER.isInfoEnabled()) { String dbPk = rs.getString(ServerTableColumnsName.LOCK_TABLE_PK); String dbTableName = rs.getString(ServerTableColumnsName.LOCK_TABLE_TABLE_NAME); long dbBranchId = rs.getLong(ServerTableColumnsName.LOCK_TABLE_BRANCH_ID); LOGGER.info("Global lock on [{}:{}] is holding by xid {} branchId {}", dbTableName, dbPk, dbXID, dbBranchId); if (!autoCommit) { int status = rs.getInt(ServerTableColumnsName.LOCK_TABLE_STATUS); if (status == LockStatus.Rollbacking.getCode()) { failFast = true; canLock = false; break; dbExistedRowKeys.add(rs.getString(ServerTableColumnsName.LOCK_TABLE_ROW_KEY)); // 不可以加全局锁,全局锁已经被其他事务占用 if (!canLock) { conn.rollback(); if (failFast) { throw new StoreException(new BranchTransactionException(LockKeyConflictFailFast)); return false; // If the lock has been exists in db, remove it from the lockDOs if (CollectionUtils.isNotEmpty(dbExistedRowKeys)) { unrepeatedLockDOs = lockDOs.stream().filter(lockDO -> !dbExistedRowKeys.contains(lockDO.getRowKey())) .collect(Collectors.toList()); if (CollectionUtils.isEmpty(unrepeatedLockDOs)) { conn.rollback(); return true; // lock if (unrepeatedLockDOs.size() == 1) { LockDO lockDO = unrepeatedLockDOs.get(0); // 加全局锁 if (!doAcquireLock(conn, lockDO)) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Global lock acquire failed, xid {} branchId {} pk {}", lockDO.getXid(), lockDO.getBranchId(), lockDO.getPk()); conn.rollback(); return false; } else { // 批量加全局锁 if (!doAcquireLocks(conn, unrepeatedLockDOs)) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Global lock batch acquire failed, xid {} branchId {} pks {}", unrepeatedLockDOs.get(0).getXid(), unrepeatedLockDOs.get(0).getBranchId(), unrepeatedLockDOs.stream().map(lockDO -> lockDO.getPk()).collect(Collectors.toList())); conn.rollback(); return false; conn.commit(); return true; } catch (SQLException e) { throw new StoreException(e); } finally { IOUtil.close(rs, ps); if (conn != null) { try { if (originalAutoCommit) { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { }增加全局行锁、检查全局锁冲突的逻辑如下:先对要加的全局行锁去重,然后关闭数据库连接的自动提交;如果跳过了全局锁冲突检查,则直接持久化全局行锁,然后提交全局锁数据持久化事务;如果需要进行全局锁冲突检查:首先根据分支事务传入的全局行锁构建查询全局锁的SQL;SQL模板(要上几个行锁,in后面就有几个?)select xid, transaction_id, branch_id, resource_id, table_name, pk, row_key, gmt_create, gmt_modified,status from lock_table where row_key in ( ? ) order by status desc 行锁的key值(数据库连接URL + 表明 + 主键id)jdbc:mysql://127.0.0.1:3306/seata_stock^^^stock_tbl^^^1如果根据查询全局行锁SQL没有从DB中查出记录,说明没有其他事务加当前分支事务所需要的全局行锁;则直接持久化全局行锁,然后提交全局锁数据持久化事务;如果根据查询全局行锁SQL从DB中查出了记录,并且加全局锁的全局事务xid不是当前全局事务的,则说明全局锁已经被其他全局事务占用;进而回滚当前提交全局锁数据持久化事务,返回false,表示加全局锁失败;方法返回到ATCore#branchSessionLock()方法中,如果加全局锁失败,则直接抛出异常BranchTransactionException。所谓的加全局锁操作,其实就是针对每一行记录 持久化一条行锁记录到lock_table表中:protected boolean doAcquireLock(Connection conn, LockDO lockDO) { PreparedStatement ps = null; try { //insert String insertLockSQL = LockStoreSqlFactory.getLogStoreSql(dbType).getInsertLockSQL(lockTable); ps = conn.prepareStatement(insertLockSQL); // 全局事务xid ps.setString(1, lockDO.getXid()); ps.setLong(2, lockDO.getTransactionId()); // 分支事务ID ps.setLong(3, lockDO.getBranchId()); ps.setString(4, lockDO.getResourceId()); ps.setString(5, lockDO.getTableName()); // 主键 ps.setString(6, lockDO.getPk()); // rowKey ps.setString(7, lockDO.getRowKey()); // 锁状态:Locked(已加锁) ps.setInt(8, LockStatus.Locked.getCode()); return ps.executeUpdate() > 0; } catch (SQLException e) { throw new StoreException(e); } finally { IOUtil.close(ps); }4)分支事务添加到全局事务1> 持久化分支事务将分支事务注册到全局事务之后,会触发Session生命周期监听器SessionLifecycleListener的onAddBranch()事件;// AbstractSessionManager 将 分支事务会话持久化到DB中 for (SessionLifecycleListener lifecycleListener : lifecycleListeners) { lifecycleListener.onAddBranch(this, branchSession); }在此处,SessionLifecycleListener只有一个实现AbstractSessionManager:DataBaseTransactionStoreManager是store.mode为db时的事务存储管理器,其writeSession()方法负责持久化全局事务、分支事务;@Override public boolean writeSession(LogOperation logOperation, SessionStorable session) { if (LogOperation.GLOBAL_ADD.equals(logOperation)) { return logStore.insertGlobalTransactionDO(SessionConverter.convertGlobalTransactionDO(session)); } else if (LogOperation.GLOBAL_UPDATE.equals(logOperation)) { return logStore.updateGlobalTransactionDO(SessionConverter.convertGlobalTransactionDO(session)); } else if (LogOperation.GLOBAL_REMOVE.equals(logOperation)) { return logStore.deleteGlobalTransactionDO(SessionConverter.convertGlobalTransactionDO(session)); } else if (LogOperation.BRANCH_ADD.equals(logOperation)) { return logStore.insertBranchTransactionDO(SessionConverter.convertBranchTransactionDO(session)); } else if (LogOperation.BRANCH_UPDATE.equals(logOperation)) { return logStore.updateBranchTransactionDO(SessionConverter.convertBranchTransactionDO(session)); } else if (LogOperation.BRANCH_REMOVE.equals(logOperation)) { return logStore.deleteBranchTransactionDO(SessionConverter.convertBranchTransactionDO(session)); } else { throw new StoreException("Unknown LogOperation:" + logOperation.name()); }前面也提到过LogStore可以看做是封装了JDBC、操作DB的工具类 / DAO层,其只有一个实现LogStoreDataBaseDAO;@Override public boolean insertGlobalTransactionDO(GlobalTransactionDO globalTransactionDO) { String sql = LogStoreSqlsFactory.getLogStoreSqls(dbType).getInsertGlobalTransactionSQL(globalTable); Connection conn = null; PreparedStatement ps = null; try { int index = 1; conn = logStoreDataSource.getConnection(); conn.setAutoCommit(true); ps = conn.prepareStatement(sql); ps.setString(index++, globalTransactionDO.getXid()); ps.setLong(index++, globalTransactionDO.getTransactionId()); ps.setInt(index++, globalTransactionDO.getStatus()); ps.setString(index++, globalTransactionDO.getApplicationId()); ps.setString(index++, globalTransactionDO.getTransactionServiceGroup()); String transactionName = globalTransactionDO.getTransactionName(); transactionName = transactionName.length() > transactionNameColumnSize ? transactionName.substring(0, transactionNameColumnSize) : transactionName; ps.setString(index++, transactionName); ps.setInt(index++, globalTransactionDO.getTimeout()); ps.setLong(index++, globalTransactionDO.getBeginTime()); ps.setString(index++, globalTransactionDO.getApplicationData()); return ps.executeUpdate() > 0; } catch (SQLException e) { throw new StoreException(e); } finally { IOUtil.close(ps, conn); }这里只是单纯的利用JDBC对数据做持久化,将数据持久化到branch_table。2> JVM层面分支事务添加到全局事务四、总结无论是AT、TCC、XA、SAGA哪种分布式事务模式,分支事务注册到全局事务的方式都一样;TC中对RM注册分支事务到全局事务的处理逻辑为:首先根据xid从DB中找到全局事务会话(这里会做一个DB查询操作);校验全局事务会话,全局事务会话必须是存活的,并且状态必须为:GlobalStatus.Begin;构建一个分支事务会话BranchSession;给分支事务加全局锁,出现锁冲突则直接报错,抛出异常BranchTransactionException;将分支事务会话添加到全局事务会话,持久化分支事务会话;TC在分支事务注册的同时,会同时增加全局行锁、检查全局锁冲突。
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务Seata最核心的全局事务执行流程,前面我们已经聊到了Seata全局事务的开启,本文接着聊Seata全局事务中执行具体业务操作时,DB操作是如何执行的(含:全局锁keys、undologs的构建(AT模式))?二、本地事务SQL执行流程全局事务的整体执行流程体现在TransactionalTemplate#execute()方法中:具体代码 和 注释:public Object execute(TransactionalExecutor business) throws Throwable { // 1. Get transactionInfo TransactionInfo txInfo = business.getTransactionInfo(); if (txInfo == null) { throw new ShouldNeverHappenException("transactionInfo does not exist"); // 1.1 Get current transaction, if not null, the tx role is 'GlobalTransactionRole.Participant'. // 获取当前事务,根据ThreadLocal,获取当前线程本地变量副本中的xid,进而判断是否存在一个全局事务 // 刚开始一个全局事务时,肯定是没有全局事务的 GlobalTransaction tx = GlobalTransactionContext.getCurrent(); // 1.2 Handle the transaction propagation. // 从全局事务的配置里 获取事务传播级别,默认是REQUIRED(如果存在则加入,否则开启一个新的) Propagation propagation = txInfo.getPropagation(); SuspendedResourcesHolder suspendedResourcesHolder = null; try { // 根据事务的隔离级别做不同的处理 switch (propagation) { case NOT_SUPPORTED: // If transaction is existing, suspend it. if (existingTransaction(tx)) { // 事务存在,则挂起事务(默认将xid从RootContext中移除) suspendedResourcesHolder = tx.suspend(); // Execute without transaction and return. return business.execute(); case REQUIRES_NEW: // If transaction is existing, suspend it, and then begin new transaction. if (existingTransaction(tx)) { suspendedResourcesHolder = tx.suspend(); tx = GlobalTransactionContext.createNew(); // Continue and execute with new transaction break; case SUPPORTS: // If transaction is not existing, execute without transaction. if (notExistingTransaction(tx)) { return business.execute(); // Continue and execute with new transaction break; case REQUIRED: // If current transaction is existing, execute with current transaction, // else continue and execute with new transaction. break; case NEVER: // If transaction is existing, throw exception. if (existingTransaction(tx)) { throw new TransactionException( String.format("Existing transaction found for transaction marked with propagation 'never', xid = %s" , tx.getXid())); } else { // Execute without transaction and return. return business.execute(); case MANDATORY: // If transaction is not existing, throw exception. if (notExistingTransaction(tx)) { throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'"); // Continue and execute with current transaction. break; default: throw new TransactionException("Not Supported Propagation:" + propagation); // 1.3 If null, create new transaction with role 'GlobalTransactionRole.Launcher'. if (tx == null) { // 创建全局事务(角色为事务发起者),并关联全局事务管理器 tx = GlobalTransactionContext.createNew(); // set current tx config to holder GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo); try { // 2. If the tx role is 'GlobalTransactionRole.Launcher', send the request of beginTransaction to TC, // else do nothing. Of course, the hooks will still be triggered. // 开启全局事务,如果事务角色是'GlobalTransactionRole.Launcher',发送开始事务请求到seata-server(TC) beginTransaction(txInfo, tx); Object rs; try { // Do Your Business // 执行业务方法,把全局事务ID通过 MVC拦截器 / dubbo filter传递到后面的分支事务; // 每个分支事务都会去运行 rs = business.execute(); } catch (Throwable ex) { // 3. The needed business exception to rollback. // 如果全局事务执行发生了异常,则回滚; completeTransactionAfterThrowing(txInfo, tx, ex); throw ex; // 4. everything is fine, commit. // 全局事务和分支事务运行无误,提交事务; commitTransaction(tx); return rs; } finally { //5. clear // 全局事务完成之后做一些清理工作 resumeGlobalLockConfig(previousConfig); triggerAfterCompletion(); cleanUp(); } finally { // If the transaction is suspended, resume it. if (suspendedResourcesHolder != null) { // 如果有挂起的全局事务,则恢复全局事务 tx.resume(suspendedResourcesHolder); }在前一篇文章: 【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务,我们已经聊到了开启全局事务,本文继续聊开启全局事务之后,本地事务中的SQL执行流程。1、DataSourceProxy 数据库资源代理入口在Spring Cloud 整合Seata实现分布式事务一文中有聊到Spring Cloud 集成Seata 的AT模式,需要写一个配置类DataSourceConfig,其中会注入一个Bean(DataSourceProxy):到这里,博主有一个问题:注入DataSourceProxy到Spring容器中之后,哪里会用到它?执行数据增删改查时如何切换到代理数据源?1)哪里使用了DataSourceProxy?从源码来看,有一个Spring AOP抽象类AbstractAutoProxyCreator的子类SeataAutoDataSourceProxyCreator;Spring 通过 AbstractAutoProxyCreator来创建 AOP 代理,其实现了BeanPostProcessor 接口,在 bean 初始化完成之后会创建它的代理,让后将代理对象增加到Spring容器。在Seata 中,SeataAutoDataSourceProxyCreator的主要作用是为数据源DataSource添加Advisor,当数据源执行操作时,便会进入到SeataAutoDataSourceProxyAdvice类中处理;因此,当数据源执行CRUD操作时,由于添加了AOP代理,会进入到SeataAutoDataSourceProxyAdvice#invoke()方法中:咦,这里没有DataSourceProxy呀,只有SeataDataSourceProxy,从命名来看,这俩类总感觉有点关系!2)SeataDataSourceProxy从DataSourceProxy类的继承结构来看,DataSourceProxy实现了SeataDataSourceProxy接口;因此SeataAutoDataSourceProxyAdvice#invoke()方法中动态代理类实际就是DataSourceProxy。2、本地事务SQL的执行流程(execute)1)执行本地事务SQL的入口JDBC的执行流程:第一步:注册驱动;第二步:获取与数据库的连接Connection;第三步:获取数据库操作对象Statement;第四步:执行sql语句(DQL、DML…),并且返回结果集;第五步:处理查询结果集;第六步:释放资源、关闭连接;try { //加载数据库驱动 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { // do something Connection conn = DriverManager.getConnection(URL, USER_NAME, PASSWORD); PreparedStatement pst = conn.prepareStatement("update user set name=? where id = ?"); pst.setString(1, "bobDog"); pst.setInt(2, 1); int updateRes = pst.executeUpdate(); if (updateRes > 0) { System.out.println("更新成功!"); }Seata代理的数据库资源DataSource底层也是JDBC操作数据库,所以也需要先获取数据库连接Connection、再根据数据库连接获取数据库操作对象Statement、接着再通过Statement#execute()执行SQL。在Seata中的表现为:先获取seata代理的数据库连接ConnectionProxy;再根据ConnectionProxy获取一个数据库操作对象 StatementProxy 或 PreparedStatementProxy;然后再利用数据库操作对象 StatementProxy 或 PreparedStatementProxy 的execute() 或 executeUpdate() 方法执行SQL语句。StatementProxy 或 PreparedStatementProxy 增强了所有的execute方法,由ExecuteTemplate选择需要的Executor执行来sql。下面以常见的更新操作(PreparedStatementProxy#executeUpdate())为例:ExecuteTemplate#execute()重载方法调用链路如下:public static <T, S extends Statement> T execute(List<SQLRecognizer> sqlRecognizers, StatementProxy<S> statementProxy, StatementCallback<T, S> statementCallback, Object... args) throws SQLException { // 没获取到全局锁,并且事务模式不是AT if (!RootContext.requireGlobalLock() && BranchType.AT != RootContext.getBranchType()) { // Just work as original statement return statementCallback.execute(statementProxy.getTargetStatement(), args); // 获取DB的类型 String dbType = statementProxy.getConnectionProxy().getDbType(); if (CollectionUtils.isEmpty(sqlRecognizers)) { sqlRecognizers = SQLVisitorFactory.get( statementProxy.getTargetSQL(), dbType); Executor<T> executor; if (CollectionUtils.isEmpty(sqlRecognizers)) { executor = new PlainExecutor<>(statementProxy, statementCallback); } else { if (sqlRecognizers.size() == 1) { SQLRecognizer sqlRecognizer = sqlRecognizers.get(0); // 数据库操作类型 switch (sqlRecognizer.getSQLType()) { case INSERT: executor = EnhancedServiceLoader.load(InsertExecutor.class, dbType, new Class[]{StatementProxy.class, StatementCallback.class, SQLRecognizer.class}, new Object[]{statementProxy, statementCallback, sqlRecognizer}); break; case UPDATE: executor = new UpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer); break; case DELETE: executor = new DeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer); break; case SELECT_FOR_UPDATE: executor = new SelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer); break; case INSERT_ON_DUPLICATE_UPDATE: switch (dbType) { case JdbcConstants.MYSQL: case JdbcConstants.MARIADB: executor = new MySQLInsertOrUpdateExecutor(statementProxy, statementCallback, sqlRecognizer); break; default: throw new NotSupportYetException(dbType + " not support to INSERT_ON_DUPLICATE_UPDATE"); break; default: executor = new PlainExecutor<>(statementProxy, statementCallback); break; } else { executor = new MultiExecutor<>(statementProxy, statementCallback, sqlRecognizers); T rs; try { // 通过Executor真正的执行 rs = executor.execute(args); } catch (Throwable ex) { if (!(ex instanceof SQLException)) { // Turn other exception into SQLException ex = new SQLException(ex); throw (SQLException) ex; return rs; }如果当前事务不需要获取全局锁,并且不是AT模式,则以original statement的方式执行。默认Seata Client层面不需要获取全局锁,事务模式是AT模式。获取到的DB类型,比如MySQL、Oracle.....,博主的项目DBType是MYSQL。获取SQL DML类型,并根据DML类型,选择不同的Executor。这里可以看做是策略模式。因为示例是Update类型,所以最终选择的Executor是UpdateExecutor。### 2)执行本地事务SQL逻辑UpdateExecutor#execute()方法中会执行本地事务SQL,UpdateExecutor的类继承图如下:除了数据更新前后的Image构造体现在UpdateExecutor类的方法中,其余方法均在其父类BaseTransactionalExecutor中,包括execute()方法。@Override public T execute(Object... args) throws Throwable { // 从全局事务上下文中获取xid String xid = RootContext.getXID(); if (xid != null) { // 将xid绑定到ConnectionContext中,后续提交本地事务时会用到 statementProxy.getConnectionProxy().bind(xid); // RootContext.requireGlobalLock()检查是否需要全局锁,默认不需要 statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock()); return doExecute(args); }开始执行本地事务SQL时:首先从全局事务上下文RootContext中获取到xid,如果存在全局事务xid,则将xid绑定到数据库连接的上下文ConnectionContext中;从全局事务上下文RootContext获取是否全局锁标识,默认不需要;如果需要获取全局锁,则将数据库连接上下文ConnectionContext中的isGlobalLockRequire设置为true;调用doExecute()方法真正开始执行SQL;UpdateExecutor#doExecutor()方法:开启了全局事务之后,DML语句的本地事务不会自动提交。即使自动提交没有关闭,AbstractDMLBaseExecutor#doExecute(Object… args)方法中也会先将其关闭,然后再以非自动提交的方式执行SQL,走ConnectionProxy提交本地事务,然后再将自动提交设置为true;这一块逻辑体现在executeAutoCommitTrue()方法中:protected T executeAutoCommitTrue(Object[] args) throws Throwable { ConnectionProxy connectionProxy = statementProxy.getConnectionProxy(); try { connectionProxy.changeAutoCommit(); return new LockRetryPolicy(connectionProxy).execute(() -> { T result = executeAutoCommitFalse(args); connectionProxy.commit(); return result; } catch (Exception e) { // when exception occur in finally,this exception will lost, so just print it here LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e); if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) { connectionProxy.getTargetConnection().rollback(); throw e; } finally { connectionProxy.getContext().reset(); connectionProxy.setAutoCommit(true); }正常情况下都是直接以非自动提交的方式执行,即执行executeAutoCommitFalse()方法:protected T executeAutoCommitFalse(Object[] args) throws Exception { if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && isMultiPk()) { throw new NotSupportYetException("multi pk only support mysql!"); // 根据SQL语句构建before image,目标SQL执行之前的数据镜像:从数据库根据ID主键等信息查询出更新前的数据; TableRecords beforeImage = beforeImage(); // 真正的去执行SQL语句,但是本地事务还没有提交 T result = statementCallback.execute(statementProxy.getTargetStatement(), args); int updateCount = statementProxy.getUpdateCount(); if (updateCount > 0) { // 目标SQL执行之后的数据镜像:从数据库根据ID主键等信息查询出更新后的数据; TableRecords afterImage = afterImage(beforeImage); // 准备好undo log数据 prepareUndoLog(beforeImage, afterImage); return result; }由于AbstractDMLBaseExecutor提供了公用的executeAutoCommitFalse()给Insert、Delete、Update类型的Executor使用,所以无论是Insert、Delete还是Update操作都会走AbstractDMLBaseExecutor#executeAutoCommitFalse()方法执行SQL。不过MySQL的MySQLInsertOrUpdateExecutor是个个例,其执行SQL的逻辑由自己实现(有兴趣可以自己看一下MySQLInsertOrUpdateExecutor)。以非自动提交执行SQL的流程如下:beforeImage() -- 根据SQL语句构建before image,查询目标sql执行前的数据快照;Update、Delete操作从数据库根据ID主键等信息查询出更新前的数据;Insert操作直接返回空的TableRecords,其中只包含TableMeta,没有数据记录;执行SQL语句,但是本地事务还没有提交;afterImage() -- 构建after image,查询目标SQL执行之后的数据快照;Insert、Update操作从数据库根据ID主键等信息查询出更新后的数据;Delete操作直接返回空的TableRecords,其中只包含TableMeta,没有数据记录;prepareUndoLog(beforeImage, afterImage) --> 将before image 和 after image合并作为回滚日志undo log,保存到当前数据库连接上下文ConnectionContext中。其中还包括构建当前本地事务要占用所有全局锁key信息,然后将其保存到当前数据库连接上下文ConnectionContext中。下面就这几步展开看一看;1> 构建before image此处依旧以Update为例:点个关注、订阅一下专栏(https://blog.csdn.net/saintmm/category_11953405.html),具体细节见下下篇文章(【微服务42】分布式事务Seata源码解析十:AT模式下如何构建undo log日志)2> 执行SQL最终使用源Statement执行SQL;3> 构建after image执行完SQL之后,再构建SQL查询出当前最新的数据记录作为after image;点个关注、订阅一下专栏(https://blog.csdn.net/saintmm/category_11953405.html),具体细节见下下篇文章(【微服务42】分布式事务Seata源码解析十:AT模式下如何构建undo log日志)4> 预处理undo log将before image 和 after image合并作为回滚日志undo log,存储到当前数据库连接上下文ConnectionContext中。protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException { if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) { return; if (SQLType.UPDATE == sqlRecognizer.getSQLType()) { if (beforeImage.getRows().size() != afterImage.getRows().size()) { throw new ShouldNeverHappenException("Before image size is not equaled to after image size, probably because you updated the primary keys."); ConnectionProxy connectionProxy = statementProxy.getConnectionProxy(); TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage; // 1、构建全局锁key信息,针对更新的一批数据主键ID构建这批数据的全局锁key // 例如:table_name:id_1101 String lockKeys = buildLockKey(lockKeyRecords); if (null != lockKeys) { // 将lockKeys信息保存到ConnectionContext中,在注册分支事务时,再将全局锁信息放入到TC中进行检查、存储 connectionProxy.appendLockKey(lockKeys); // 2、构建undo log SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage); // 将undo log信息保存到ConnectionContext中 connectionProxy.appendUndoLog(sqlUndoLog); }点个关注、订阅一下专栏(https://blog.csdn.net/saintmm/category_11953405.html),具体细节见下下篇文章(【微服务42】分布式事务Seata源码解析十:AT模式下如何构建undo log日志)由于关闭了AutoCommit,所以在Statement.execute()执行完SQL之后,需要“手动”提交本地事务。3、本地事务SQL的提交(commit)回到ConnectionProxy#commit()方法,这里是“手动”提交本地事务的入口;@Override public void commit() throws SQLException { try { // 由LockRetryPolicy负责提交事务,LockRetryPolicy中包含全局锁的概念,支持retry重试策略 lockRetryPolicy.execute(() -> { doCommit(); return null; } catch (SQLException e) { if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) { rollback(); throw e; } catch (Exception e) { throw new SQLException(e); }本地事务的提交又会委托给LockRetryPolicy的execute方法来执行;1)LockRetryPolicy重试机制LockRetryPolicy是ConnectionProxy的静态内部类,其中包含了全局锁的概念,支持retry策略,当出现全局锁冲突时支持多次重试获取全局锁。默认情况下execute()方法中:LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT为TRUE,可以通过配置client.rm.lock.retryPolicyBranchRollbackOnConflict=false属性改变;connection.getContext().isAutoCommitChanged()为FALSE;所以默认情况下,都会走重试获取全局锁的逻辑:doRetryOnLockConflict()方法。(当然可以选择开启自动提交事务、并设置属性client.rm.lock.retryPolicyBranchRollbackOnConflict=true,这样便不会走重试获取全局锁逻辑。)protected <T> T doRetryOnLockConflict(Callable<T> callable) throws Exception { LockRetryController lockRetryController = new LockRetryController(); while (true) { try { return callable.call(); } catch (LockConflictException lockConflict) { // 出现全局锁冲突,回滚本地事务 onException(lockConflict); // AbstractDMLBaseExecutor#executeAutoCommitTrue the local lock is released if (connection.getContext().isAutoCommitChanged() && lockConflict.getCode() == TransactionExceptionCode.LockKeyConflictFailFast) { lockConflict.setCode(TransactionExceptionCode.LockKeyConflict); // 线程睡眠10ms,然后再重试,超过重试次数,抛出异常结束流程 lockRetryController.sleep(lockConflict); } catch (Exception e) { // 出现非全局锁冲突的异常,则直接报错返回 onException(e); throw e; }在doRetryOnLockConflict()方法中:如果因为全局锁冲突导致提交本地事务失败,先回滚本地事务,然后会判断重试次数(lockRetryTimes,默认30次)再进行重试,重试之前会让线程睡眠一段时间(lockRetryInterval,默认10ms)。如果重试次数已经够了,则直接抛出异常结束流程。如果因为其他异常(包括超过重试次数)导致提交本地事务失败,直接回滚本地事务、抛出异常结束流程。上面的ConnectionProxy#onException()方法中负责回滚本地事务、清理当前连接的ConnectionContext中的undo log信息、全局锁keys信息;了解完了全局锁冲突引起的重试机制,下面接着看本地事务的提交流程。2)本地事务提交流程LockRetryPolicy#execute()方法中会运行方法的入参Callable,在ConnectionProxy#commit()方法中传入的到LockRetryPolicy#execute()方法中的Callable为:() -> { doCommit(); return null; }doCommit()方法:private void doCommit() throws SQLException { // 当前DML操作在全局事务中时,判定条件:ConnectionContext中包含xid if (context.inGlobalTransaction()) { processGlobalTransactionCommit(); } else if (context.isGlobalLockRequire()) { // 如果使用了@GlobalLock,需要获取全局锁 processLocalCommitWithGlobalLocks(); } else { // 不在分布式事务中,则以原生connection提交本地事务 targetConnection.commit(); }doCommit()方法中分三种情况进行不同的处理:如果当前DML操作在全局事务中,即:当前连接的ConnectionContext中包含xid,则以处理全局事务方式(processGlobalTransactionCommit()提交本地事务;如果使用了@GlobalLock,需要获取全局锁,再以原生connection提交本地事务;否则如果事务不在分布式事务中,则以原生connection提交本地事务;正常我们使用分布式事务,一般肯定是要以全局事务的方式执行DML操作;即:默认会进入到processGlobalTransactionCommit():private void processGlobalTransactionCommit() throws SQLException { try { // 向远程的TC中注册分支事务,并检查、增加全局行锁 register(); } catch (TransactionException e) { // 出现异常时,回滚本地事务 再重试。 // 大多数情况是因为全局锁冲突走到这里。 recognizeLockKeyConflictException(e, context.buildLockKeys()); try { // 回滚日志管理组件,持久化undo log UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this); // 提交本地事务 targetConnection.commit(); } catch (Throwable ex) { LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex); // 上报分支事务执行失败,用于监控 report(false); throw new SQLException(ex); // 上报分支事务执行成功,默认不会上报 if (IS_REPORT_SUCCESS_ENABLE) { report(true); // 重置连接的ConnectionContext context.reset(); }以全局事务的方式提交本地事务会做四件事:通过netty请求TC,注册分支事务,并检查、增加全局行锁;如果出现异常,则回滚本地事务。若异常类型为全局锁冲突LockConflictException,则进入重试策略;其他异常类型则直接抛出SQLException;将执行SQL时保存到ConnectionContext中的undo log 回滚日志 保存到DB;提交本地事务,真正将业务数据和回滚日志 持久化到DB;向TC上报本地事务提交结果;如果持久化undo log 或 提交本地事务出现异常,则上报分支事务执行失败;如果本地事务提交成功,上报分支事务执行成功;默认并不会上报。最后,清空当前数据库连接的ConnectionContext。点个关注、订阅一下专栏(https://blog.csdn.net/saintmm/category_11953405.html);分支事务的注册细节见下一篇文章(【微服务41】分布式事务Seata源码解析九:分支事务如何注册到全局事务);undo log持久化细节 见下下篇文章(【微服务42】分布式事务Seata源码解析十:AT模式下如何构建undo log日志);三、总结AT模式下本地事务的SQL执行流程,即RM的分支事务执行流程,主要包括一下几步:开始执行本地事务的SQL之前,从全局事务上下文RootContext中获取到xid,然后将xid绑定到数据库连接的上下文ConnectionContext中;构建before image,查询目标sql执行前的数据快照;执行目标SQL语句,但是本地事务还没有提交;构建after image,查询目标SQL执行之后的数据快照;将before image 和 after image合并作为回滚日志undo log,保存到当前数据库连接上下文ConnectionContext中;构建当前本地事务要占用所有全局锁key信息,然后将其保存到当前数据库连接上下文ConnectionContext中;通过netty请求TC,注册分支事务,并检查、增加全局行锁;这里可能会出现全局锁冲突 导致注册分支事务失败,所以有一个重试机制;将执行SQL时保存到ConnectionContext中的undo log 回滚日志 保存到DB(undo_log表);提交本地事务;向TC上报本地事务提交结果;最后清空当前数据库连接的ConnectionContext,恢复现场。整个SQL提交可以理解为两阶段提交:一阶段:先注册分支事务,检查全局锁。二阶段:插入undolog、提交本地事务。
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?本文正式进入Seata最核心的全局事务执行流程。二、全局事务执行的入口在【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务一文,我们知道了所谓的@GlobalTransactional注解开启全局事务,其实就是给类 或 类的方法上标注了@GlobalTransactional注解的类创建动态代理对象。但是动态代理对象是针对类的;1、拦截器GlobalTransactionalInterceptor当一个类中有多个方法并且类没有被@GlobalTransactional注解标注,但只有一个方法被@GlobalTransactional注解标注时,这里针对整个类生成了动态代理对象,当调用Bean时,拦截器GlobalTransactionalInterceptor会做进一步处理,保证只有加了@GlobalTransactional注解的方法才会开启全局事务。GlobalTransactionalInterceptor类的继承图:GlobalTransactionalInterceptor实现了MethodInterceptor接口,所以当每次执行添加了 GlobalTransactionalInterceptor拦截器的Bean的方法时,都会进入到GlobalTransactionalInterceptor类覆写MethodInterceptor接口的invoke()方法;@Override public Object invoke(final MethodInvocation methodInvocation) throws Throwable { // method invocation是一次方法调用,一定是针对某个对象的方法调用; // methodInvocation.getThis()就是拿到当前方法所属的对象; // AopUtils.getTargetClass()获取到当前实例对象所对应的Class Class<?> targetClass = methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null; // 通过反射获取到被调用目标Class的method方法 Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass); // 如果目标method不为空,并且方法的DeclaringClass不是Object if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) { // 通过BridgeMethodResolver寻找method的桥接方法 final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod); // 获取目标方法的@GlobalTransactional注解 final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, targetClass, GlobalTransactional.class); // 如果目标方法被@GlobalLock注解标注,获取到@GlobalLock注解内容 final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class); // 如果禁用了全局事务 或 开启了事务降级检查并且降级检查次数大于等于降级检查允许的次数 // 则localDisable等价于全局事务被禁用了 boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes); // 如果全局事务没有被禁用 if (!localDisable) { // 全局事务注解不为空 或者 AOP切面全局事务核心配置不为空 if (globalTransactionalAnnotation != null || this.aspectTransactional != null) { AspectTransactional transactional; if (globalTransactionalAnnotation != null) { // 构建一个AOP切面全局事务核心配置,配置的数据从全局事务注解中取 transactional = new AspectTransactional(globalTransactionalAnnotation.timeoutMills(), globalTransactionalAnnotation.name(), globalTransactionalAnnotation.rollbackFor(), globalTransactionalAnnotation.rollbackForClassName(), globalTransactionalAnnotation.noRollbackFor(), globalTransactionalAnnotation.noRollbackForClassName(), globalTransactionalAnnotation.propagation(), globalTransactionalAnnotation.lockRetryInterval(), globalTransactionalAnnotation.lockRetryTimes()); } else { transactional = this.aspectTransactional; // 真正处理全局事务的入口 return handleGlobalTransaction(methodInvocation, transactional); } else if (globalLockAnnotation != null) { // 获取事务锁 return handleGlobalLock(methodInvocation, globalLockAnnotation); // 直接运行目标方法 return methodInvocation.proceed(); }invoke()方法解析1)方法入参--MethodInvocationinvoke()方法的入参为MethodInvocation,MethodInvocation是一次方法调用,并且是针对某个对象的方法调用;methodInvocation.getThis()会拿到当前方法所属的对象;在通过methodInvocation.getThis()会拿到当前方法所属的对象时,如果获取到的是null,则使用AopUtils.getTargetClass()获取到当前实例对象所对应的Class(如果被AOP代理,则是代理类,否则是普通类)。2)判断目标方法是否需要开启全局事务直接通过反射拿到目标Class的method方法;如果method不为空,并且method所属的类不是Object类;再判断如果method直接或间接被GlobalTransactional注解标注,并且没有禁用全局事务,则再进一步判断全局事务是否被禁用,如果没有被禁用则执行全局事务。3)开始处理全局事务handleGlobalTransaction()方法中真正开始进行全局事务的处理。方法具体内容见<三、全局事务执行>2、不用开启全局事务的情况1)全局事务被禁用在判断完method直接或间接被@GlobalTransactional标注之后,会判断全局事务是否被禁用,如果被禁用则至今运行目标方法。禁用全局事务有两种方式:1> 显示的设置disable属性配置service.disableGlobalTransaction,默认为false,表示不禁用全局事务;2> 开启了事务降级检查,并且降级检查次数大于等于降级检查允许的次数配置client.tm.degradeCheck,默认为false,表示不开启事务降级检查;配置client.tm.degradeCheckAllowTimes,只有当开启事务降级检查,这个配置才有意义;2)某一个类被标注的注解,但Object超类下的所有方法仍都不会开启全局事务在GlobalTransactionalInterceptor#invoke()方法中会判断如果目标类的方法是Object类下的方法,则不会执行全局事务;3)某一个方法标注了事务注解,其余方法没标注,并且类没有被标注,其余方法都不会开启全局事务假如我们调用TradeService类中没有标注@GlobalTransactional注解的test()方法(且 TradeService类也没有标注@GlobalTransaction注解);invoke()方法中会再次判断 当前调用的bean的方法 或 方法所处的类上是否标注了@GlobalTransactional注解,如果没有标注,则执行运行目标方法;否则才会以全局事务的方式执行方法。三、全局事务执行在上面我们聊了GlobalTransactionalInterceptor#handleGlobalTransaction()方法会进行全局事务的处理;全局事务的执行会交给全局事务执行业务逻辑的模板TransactionalTemplate,并将目标方法封装到TransactionalExecutor中作为全局事务中执行业务逻辑的回调。全局事务执行模板TransactionalTemplate全局事务的整体执行流程体现在TransactionalTemplate#execute()方法中:具体代码 和 注释:public Object execute(TransactionalExecutor business) throws Throwable { // 1. Get transactionInfo TransactionInfo txInfo = business.getTransactionInfo(); if (txInfo == null) { throw new ShouldNeverHappenException("transactionInfo does not exist"); // 1.1 Get current transaction, if not null, the tx role is 'GlobalTransactionRole.Participant'. // 获取当前事务,根据ThreadLocal,获取当前线程本地变量副本中的xid,进而判断是否存在一个全局事务 // 刚开始一个全局事务时,肯定是没有全局事务的 GlobalTransaction tx = GlobalTransactionContext.getCurrent(); // 1.2 Handle the transaction propagation. // 从全局事务的配置里 获取事务传播级别,默认是REQUIRED(如果存在则加入,否则开启一个新的) Propagation propagation = txInfo.getPropagation(); SuspendedResourcesHolder suspendedResourcesHolder = null; try { // 根据事务的隔离级别做不同的处理 switch (propagation) { case NOT_SUPPORTED: // If transaction is existing, suspend it. if (existingTransaction(tx)) { // 事务存在,则挂起事务(默认将xid从RootContext中移除) suspendedResourcesHolder = tx.suspend(); // Execute without transaction and return. return business.execute(); case REQUIRES_NEW: // If transaction is existing, suspend it, and then begin new transaction. if (existingTransaction(tx)) { suspendedResourcesHolder = tx.suspend(); tx = GlobalTransactionContext.createNew(); // Continue and execute with new transaction break; case SUPPORTS: // If transaction is not existing, execute without transaction. if (notExistingTransaction(tx)) { return business.execute(); // Continue and execute with new transaction break; case REQUIRED: // If current transaction is existing, execute with current transaction, // else continue and execute with new transaction. break; case NEVER: // If transaction is existing, throw exception. if (existingTransaction(tx)) { throw new TransactionException( String.format("Existing transaction found for transaction marked with propagation 'never', xid = %s" , tx.getXid())); } else { // Execute without transaction and return. return business.execute(); case MANDATORY: // If transaction is not existing, throw exception. if (notExistingTransaction(tx)) { throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'"); // Continue and execute with current transaction. break; default: throw new TransactionException("Not Supported Propagation:" + propagation); // 1.3 If null, create new transaction with role 'GlobalTransactionRole.Launcher'. if (tx == null) { // 创建全局事务(角色为事务发起者),并关联全局事务管理器 tx = GlobalTransactionContext.createNew(); // set current tx config to holder GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo); try { // 2. If the tx role is 'GlobalTransactionRole.Launcher', send the request of beginTransaction to TC, // else do nothing. Of course, the hooks will still be triggered. // 开启全局事务,如果事务角色是'GlobalTransactionRole.Launcher',发送开始事务请求到seata-server(TC) beginTransaction(txInfo, tx); Object rs; try { // Do Your Business // 执行业务方法,把全局事务ID通过 MVC拦截器 / dubbo filter传递到后面的分支事务; // 每个分支事务都会去运行 rs = business.execute(); } catch (Throwable ex) { // 3. The needed business exception to rollback. // 如果全局事务执行发生了异常,则回滚; completeTransactionAfterThrowing(txInfo, tx, ex); throw ex; // 4. everything is fine, commit. // 全局事务和分支事务运行无误,提交事务; commitTransaction(tx); return rs; } finally { //5. clear // 全局事务完成之后做一些清理工作 resumeGlobalLockConfig(previousConfig); triggerAfterCompletion(); cleanUp(); } finally { // If the transaction is suspended, resume it. if (suspendedResourcesHolder != null) { // 如果有挂起的全局事务,则恢复全局事务 tx.resume(suspendedResourcesHolder); }整个全局事务的执行由八步组成:从线程本地变量副本中获取到xid,进而判断是否存在一个全局事务;根据事务的隔离级别,对已存在的全局事务做不同的处理,包括:挂起事务、新建一个事务.....最后如果事务为空,则创建一个新的全局事务(刚开始一个新的全局事务时,会走进这个逻辑)开启一个全局事务;执行业务方法,把全局事务ID通过 MVC拦截器 / dubbo filter传递到后面的分支事务;如果全局事务执行发生了异常,则通知TC回滚全局事务和所有的分支事务;如果全局事务和分支事务运行无误,提交事务;无论全局事务是否运行成功,都需要清理占用的全局锁资源;最后,如果存在被挂起的全局事务,则恢复全局事务。下面我们针对每一步具体来看;1、第一步:判断是否存在一个全局事务因为执行分支事务时,分支事务的业务方法也有可能被@GlobalTransactional注解直接或间接修饰,进而导致分支事务和全局事务的执行入口是一样的;所以需要先判断是否存在一个全局事务(而当存在全局事务时,分支事务应该如何执行,我们下一篇文章讨论)。刚开始执行一个全局事务时,当前线程本地变量副本中的xid为null,即不存在一个全局事务。2、第二步:根据事务的隔离级别做不同的处理默认事务的隔离级别为REQUIRED:即:如果当前存在一个事务,则加入事务;否者新建一个事务。由于刚开始执行一个全局事务时,不存在事务,所以默认会新建一个全局事务。GlobalTransactionContext.createNew()负责新建一个全局事务:6种事务隔离级别的具体逻辑1> NOT_SUPPORTED不支持事务: 如果事务存在,则挂起事务(默认将xid从RootContext中移除,记录下挂起的事务资源)2> REQUIRES_NEW新建一个事务:如果事务存在,则挂起事务,再新建一个事务。3> SUPPORTS支持事务:如果当前存在事务,则加入事务,不存在事务,则以非事务方式执行。4> REQUIRED(默认事务模式)必须有事务:如果当前存在一个事务,则加入事务;否者新建一个事务。5> NEVER不支持事务:如果当前存在事务,则报错;否则以非事务方式执行。6> MANDATORY强制使用事务:如果当前不存在事务,则报错;否则加入事务执行。3、第三步:开启全局事务在开启全局事务前后会有钩子函数,默认开启全局事务前后的两个钩子中没有任何实现,如果有需要可以自己定制。这个业务执行前后的钩子函数在Spring体系中随处可见。整个开启全局事务的逻辑如下:开启全局事务时,会首先判断事务的角色是否Launcher,即全局事务;刚开始执行一个全局事务时,创建出来的DefaultGlobalTransaction,其role就是Launcher,也就是说事务角色为全局事务。如果事务的角色不是全局事务,则会断言xid不许为null,否者抛出异常IllegalStateException;当事务为全局事务时,首选断言xid为null,否者抛出异常IllegalStateException;因为超时重试机制的缘故,会再次判断线程本地上下文中的xid是否为null,如果不为null,同样抛出异常IllegalStateException。请求TC(seata-server)开启全局事务,并获取到全局事务xid。请求TC开启全局事务之后,设置事务的状态为开启,并将全局事务xid绑定到线程本地变量副本上。下面着重看一下TM如何请求TC开启全局事务并获取到xid?TM如何请求TC开启全局事务全局事务发起者TM,会通过netty和TC进行网络通信;其中包括对seata-server集群的负载均衡,在获取到相应seata-server实例对应的channel之后,会进步处理请求的发送和相应结果的接收。在写Channel之前,channelWritableCheck()方法会检查channel是否可写。TM / RM 和TC的RPC通信均是异步进行的:TM / RM 发送请求时,将封装了CompletableFuture的MessageFuture放到futures(ConcurrentHashMap<Integer, MessageFuture>)中;TC处理完请求之后,会通过netty框架发送响应到TM / RM 的AbstractNettyRemoting中,其再将futures中的MessageFuture完成,发送请求的代码段中messageFuture.get()会获取到返回值,停止阻塞。TM发送请求之后,TC如何接收请求,如何处理请求?TC接收到TM的请求如何开启全局事务在【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信一文中,我们聊了Seata Client 如何和Seata Server建立连接、通信;又在【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么一文中,我们知道了TC(Seata Server)启动之后,AbstractNettyRemotingServer的内部类ServerHandler负责接收并处理请求。ServerHandler类上有个@ChannelHandler.Sharable注解,其表示所有的连接都会共用这一个ChannelHandler;所以当消息处理很慢时,会降低并发。processMessage(ctx, (RpcMessage) msg)方法中会根据消息类型获取到 请求处理组件(消息的处理过程是典型的策略模式),如果消息对应的处理器设置了线程池,则放到线程池中执行;如果对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发;所以在seata-server中大部分处理器都有对应的线程池。/** * Rpc message processing. * @param ctx Channel handler context. * @param rpcMessage rpc message. * @throws Exception throws exception process message error. * @since 1.3.0 protected void processMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("%s msgId:%s, body:%s", this, rpcMessage.getId(), rpcMessage.getBody())); Object body = rpcMessage.getBody(); if (body instanceof MessageTypeAware) { MessageTypeAware messageTypeAware = (MessageTypeAware) body; // 根据消息的类型获取到请求处理组件和请求处理线程池组成的Pair final Pair<RemotingProcessor, ExecutorService> pair = this.processorTable.get((int) messageTypeAware.getTypeCode()); if (pair != null) { // 如果消息对应的处理器设置了线程池,则放到线程池中执行 if (pair.getSecond() != null) { try { pair.getSecond().execute(() -> { try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } finally { MDC.clear(); } catch (RejectedExecutionException e) { // 线程池拒绝策略之一,抛出异常:RejectedExecutionException LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(), "thread pool is full, current max pool size is " + messageExecutor.getActiveCount()); if (allowDumpStack) { String name = ManagementFactory.getRuntimeMXBean().getName(); String pid = name.split("@")[0]; long idx = System.currentTimeMillis(); try { String jstackFile = idx + ".log"; LOGGER.info("jstack command will dump to " + jstackFile); Runtime.getRuntime().exec(String.format("jstack %s > %s", pid, jstackFile)); } catch (IOException exx) { LOGGER.error(exx.getMessage()); allowDumpStack = false; } else { // 对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发; try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } else { LOGGER.error("This message type [{}] has no processor.", messageTypeAware.getTypeCode()); } else { LOGGER.error("This rpcMessage body[{}] is not MessageTypeAware type.", body); }Seata Serer接收到请求的执行链路为:又由于TM发送开启事务请求时的RPCMessage的body为GlobalBeginRequest:所以进入到:又由于在DefaultCoordinator#onRequest()方法中,将DefaultCoordinator自身绑定到了AbstractTransactionRequestToTC的handler属性中:所以进入到:而AbstractExceptionHandler#exceptionHandleTemplate()方法只是运行方法的入参Callback,即接着会进入到:DefaultCore执行开启全局事务的业务逻辑DefaultCore#begin()方法负责开启全局事务的业务逻辑,方法的入参包括:开启全局事务的应用程序名称、事务服务分组名称、事务名称(开启全局事务的方法名以及方法的入参类型)、事务超时时间。@Override public String begin(String applicationId, String transactionServiceGroup, String name, int timeout) throws TransactionException { // 创建一个全局事务会话 GlobalSession session = GlobalSession.createGlobalSession(applicationId, transactionServiceGroup, name, timeout); // 通过MDC把XID放入线程本地变量ThreadLocal中(MDC是Slf4j提供的工具) MDC.put(RootContext.MDC_KEY_XID, session.getXid()); // 添加对全局事务会话生命周期的监听 session.addSessionLifecycleListener(SessionHolder.getRootSessionManager()); // 开启全局事务会话 session.begin(); // transaction start event // 发布全局事务开启事件 做指标监控 MetricsPublisher.postSessionDoingEvent(session, false); // 返回全局事务会话的xid return session.getXid(); }seata-server开启全局事务的流程:创建一个全局会话GlobalSession;通过MDC把XID放入线程本地变量ThreadLocal中,并添加对全局事务会话生命周期的监听;开启全局事务会话;发布全局事务开启事件 做指标监控;返回全局事务会话的xid。1> 第一步:创建全局会话GlobalSession创建全局会话的最主要的点是根据雪花算法生成全局事务ID(transactionId)、XID(seata server的IP、Port和transactionId使用:拼接到一起)。Seata如何使用雪花算法生成全局事务ID的见文章:【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?2> 第二步:把XID放入线程本地变量副本,并添加对全局事务会话生命周期的监听3> 第三步:开启全局事务会话开启全局事务会话的逻辑主要在遍历所有的生成周期监听函数,执行begin事件;根据我们启动Seata Server时选择的store.mode,会执行不同的SessionLifecycleListener:博主启动Seata Server时store.mode = db,所以我这里的SessionLifecycleListener为DataBaseSessionManager:DataBaseSessionManager执行begin事件的链路如下:这里其实就是将全局事务会话信息持久化到DB中:首先将全局事务会话信息封装到GlobalTransactionDO模型中;然后使用JDBC将全局事务会话信息持久化到表global_table中;所谓的开启全局事务会话,其实就是将全局事务会话信息持久化到Store.mode中。4> 第四步:发布全局事务开启事件 做指标监控这一块对了解seata事务的执行主流程没影响,不需要耗费特别大的精力关注,如果有指标监控的需求再重点看。5> 返回全局事务会话的xid4、第四步 --- 第八步:见下一篇博文点个关注、订阅订阅专栏,下一篇系列文章更精彩。执行业务方法(AT模式下)、全局事务回滚、全局事务提交、全局锁资源释放见下一篇博文。四、总结本文重点聊了Seata事务执行流程中TM、TC中如何开启全局事务;其中设计几个比较关键的类:TransactionalExecutor --> 全局事务执行组件TransactionalTemplate --> 全局事务生命周期模板管理组件,负责管理事务的生命周期;TransactionManager --> 全局事务管理组件,负责执行事务的业务逻辑;DefaultCore --> Seata Server端事务业务的执行逻辑,封装了AT、TCC、Saga、XA分布式事务模式的具体实现。
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务本文接着来看Seata的全局事务ID(transactionId)和分支事务ID(branchId)是如何生成的?二、分布式ID初始化在Seata Server启动的时候( 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】)会初始化UUID生成器UUIDGenerator;1、UUIDGeneratorpublic class UUIDGenerator { private static volatile IdWorker idWorker; * generate UUID using snowflake algorithm * @return UUID public static long generateUUID() { // DCL + volatile ,实现并发场景下保证idWorker的单例特性 if (idWorker == null) { synchronized (UUIDGenerator.class) { if (idWorker == null) { init(null); // 每次通过雪花算法实现的nextId()获取一个新的UUID return idWorker.nextId(); * 初始化IDWorker public static void init(Long serverNode) { idWorker = new IdWorker(serverNode); }UUIDGenerator会委托给其组合的IdWorker根据雪花算法生成分布式ID,生成的雪花Id由0、10位的workerId、41位的时间戳、12位的sequence序列号组成。2、IdWorkerIdWorker中有8个重要的成员变量/常量:/** * Start time cut (2020-05-03) private final long twepoch = 1588435200000L; * The number of bits occupied by workerId private final int workerIdBits = 10; * The number of bits occupied by timestamp private final int timestampBits = 41; * The number of bits occupied by sequence private final int sequenceBits = 12; * Maximum supported machine id, the result is 1023 private final int maxWorkerId = ~(-1 << workerIdBits); * business meaning: machine ID (0 ~ 1023) * actual layout in memory: * highest 1 bit: 0 * middle 10 bit: workerId * lowest 53 bit: all 0 private long workerId; * 又是一个雪花算法(64位,8字节) * timestamp and sequence mix in one Long * highest 11 bit: not used * middle 41 bit: timestamp * lowest 12 bit: sequence private AtomicLong timestampAndSequence; * 从一个long数组类型中抽取出一个时间戳伴随序列号,偏向一个辅助性质 * mask that help to extract timestamp and sequence from a long private final long timestampAndSequenceMask = ~(-1L << (timestampBits + sequenceBits));变量/常量解释:常量twepoch表示我们的时间戳时间从2020-05-03开始计算,即当前时间的时间戳需要减去twepoch的值1588435200000L;常量workerIdBits表示机器号workerId占10位;常量timestampBits表示时间戳timestamp占41位;常量sequenceBits表示序列化占12位;常量maxWorkerId表示机器号的最大值为1023;long类型的变量workerId本身也是一个雪花算法,只是从头往后数,第2位开始,一共10位用来表示workerId,其余位全是0;AtomicLong类型的变量timestampAndSequence,其本身也是一个雪花算法,头11位不使用,中间41位表示timestamp,最后12位表示sequence;long类型的常量timestampAndSequenceMask,用于从一个完整的雪花ID(long类型)中摘出timestamp 和 sequenceIdWorker构造器中会分别初始化TimestampAndSequence、WorkerId。public IdWorker(Long workerId) { // 初始化时间戳sequence initTimestampAndSequence(); // 初始化workerId initWorkerId(workerId); }1) 初始化时间戳和序列号initTimestampAndSequence()方法负责初始化timestamp和sequence;private void initTimestampAndSequence() { // 拿到当前时间戳 - (2020-05-03 时间戳)的数值,即当前时间相对2020-05-03的时间戳 long timestamp = getNewestTimestamp(); // 把时间戳左移12位,后12位留给sequence使用 long timestampWithSequence = timestamp << sequenceBits; // 把混合sequence(默认为0)的时间戳赋值给timestampAndSequence this.timestampAndSequence = new AtomicLong(timestampWithSequence); // 获取当前时间戳 private long getNewestTimestamp() { //当前时间的时间戳减去2020-05-03的时间戳 return System.currentTimeMillis() - twepoch; }2)初始化机器IDinitWorkerId(Long workerId)方法负责初始化workId,默认不会传过来workerId,如果传过来则使用传过来的workerId,并校验其不能大于1023,然后将其左移53位;private void initWorkerId(Long workerId) { if (workerId == null) { // workid为null时,自动生成一个workerId workerId = generateWorkerId(); // workerId最大只能是1023,因为其只占10bit if (workerId > maxWorkerId || workerId < 0) { String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId); throw new IllegalArgumentException(message); this.workerId = workerId << (timestampBits + sequenceBits); }1> 如果没传则基于MAC地址生成;机器id由两值相加:(byte值 & 0B11) << Byte.SIZE,即最大值为 0B11=3;然后左移8位为:1100000000; 所以此处最大十进制值为768;(byte值 & 0xFF) ;16进制的F对应二进制为 1111,所以最大十进制值FF: 255;然后:768 + 255 = 1023,即机器id(workerId)最大不会超过1023;0B对应二进制,0x对应十六进制2> 如果基于MAC地址生成workerId出现异常,则也1023为基数生成一个随机的workerId;最后同样,校验workerId不能大于1023,然后将其左移53位,用于拼接出分布式ID。三、分布式ID获取上面我们了解到在Seata Server启动时会初始化UUID生成器UUIDGenerator的成员IDWorker,以用于生成分布式ID;在后续TM开启全局事务、或RM创建分支事务加入到全局事务时,都会调用UUIDGenerator#generateUUID()方法生成分布式事务ID(全局事务ID transactionId、分支事务ID branchId);1、生成UUID的入口public static long generateUUID() { // DCL + volatile ,实现并发场景下保证idWorker的单例特性 if (idWorker == null) { synchronized (UUIDGenerator.class) { if (idWorker == null) { init(null); // 每次通过雪花算法实现的nextId()获取一个新的UUID return idWorker.nextId(); }idWorker变量被volatile关键字所修饰,确保其在多线程环境下的可见性,再结合DCL(Double Check Lock,双重检查锁)确保idWorker的单例性。每次要获取新的一个UUID时,会通过IdWorker#nextId()方法实现;2、如何生成一个UUIDIdWorker#nextId()方法负责生成一个UUID;其中:highest 1 bit: 最高位的1bit始终是0;next 10 bit: workerId 10个bit表示机器号;next 41 bit: timestamp 41个bit表示当前机器的时间戳(ms级别),每毫秒递增;lowest 12 bit: sequence 12位的序号,如果一台机器在一毫秒内有很多线程要来生成id,12bit的sequence会自增;public long nextId() { // 解决sequence序列号被用尽问题! waitIfNecessary(); // 自增时间戳的sequence,等于对一个毫秒内的sequence做累加操作,或 timestamp + 1、sequence置0 long next = timestampAndSequence.incrementAndGet(); // 把最新时间戳(包括序列号)和mask做一个与运算,得到真正的时间戳伴随的序列 long timestampWithSequence = next & timestampAndSequenceMask; // 最后和workerId做或运算,得到最终的UUID; return workerId | timestampWithSequence; }nextId()方法逻辑:解决sequence序列号被用尽问题;累加序列号sequence,Seata中设计的sequence是和timestamp放在同一个变量里,累加之后再和mask(后53位全是1)做一个与运算,得到真正的时间戳伴随的序列;将workerId 和 时间戳伴随的序列 通过或运算组合成最终的UUID。下面细看一下waitIfNecessary()是如何解决序列号被用尽的问题;1)如何解决序列号被用尽的问题waitIfNecessary()会解决序列号被用尽的问题;private void waitIfNecessary() { // 获取当前时间戳 以及相应的sequence序列号 long currentWithSequence = timestampAndSequence.get(); // 通过位运算拿到当前的时间戳 long current = currentWithSequence >>> sequenceBits; // 获取当前真实的时间戳 long newest = getNewestTimestamp(); // 如果`timestampAndSequence`中的当前时间戳 大于等于 真实的时间戳,说明当前机器时间之前的sequence序号 / 某个毫秒内的序列号 已经被耗尽了; if (current >= newest) { try { // 如果获取UUID的QPS过高,导致时间戳对应的sequence序号被耗尽了 // 线程休眠5毫秒 Thread.sleep(5); } catch (InterruptedException ignore) { // don't care }如果有大量的线程并发获取UUID、获取UUID的QPS过高,可能会导致从初始化IdWorker时间戳开始 到 当前时间戳的序列号全部用完了(也可以理解为某一个毫秒内的sequence耗尽);但是时间戳却累加了、进到下一个毫秒(或下几毫秒);然而当前实际时间却还没有到下一毫秒。如果恰巧此时重启了seata server,再初始化IdWorker时的时间戳就有可能会出现重复,进而导致UUID重复。所以Seata为了尽可能的保证UUID生成算法的稳定性;如果timestampAndSequence中的当前时间戳 大于等于 服务器真实的时间戳,会将线程睡眠5ms;博主看到这里时有两个问题:为什么判断时间戳时是大于等于,而不是大于?为什么就让线程睡眠了5ms?为什么判断时间戳时是大于等于,而不是大于?如果是大于(current > newest),而不是大于等于(current >= newest);考虑一种极端的场景,UUID的时间戳已经累加到了当前时间,此时Seata Server立马关机重启(假设这个过程耗时不到1ms),就会出现重复的UUID。不过这种场景仅存在于理论上;现实应该不会,所以我认为 大于(current > newest) 是没有问题的。为什么就让线程睡眠了5ms?这里就睡眠5ms,可能只是把所有的流量都往后均摊,因为往往高峰期时间也比较短;并且一个毫秒会有4096个序列号,而从seata Server启动开始也不会立刻就是高峰期,所以到当前时间之前 也会有很多的时间戳给UUID使用;不过这个简单粗暴的阻塞线程确实会浪费一些系统资源。为什么是睡眠5ms,而不是3ms、2ms,可能是出于压测的结论,也可能作者也没想那么多。2)时钟回拨问题的解决UUIDGenerator(或者说IdWorker)通过借用未来时间来解决sequence天然存在的并发限制,如果timestampAndSequence中的当前时间戳大约 服务器的当前时间,仅仅会睡眠5ms,起一个缓冲的作用;但timestampAndSequence仍会继续递增,使用未来的时间。Seata Server服务不重启基本没有问题,当接入Seata Server的服务们QPS比较高时,重启Seata Server就可能会出现新生成的UUID和历史UUID重复问题。四、总结和后续本文聊了Seata中分布式ID是使用雪花算法生成的,对一个64位的UUID,其最高位恒为0,10个bit表示机器号,41个bit表示当前机器的时间戳(ms级别),12位的序号。seata又对毫秒内序列号用尽、时钟回拨做了特殊处理。下一篇文章我们将聊Seata全局事务的执行流程。
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信本文接着Seata使用@GlobalTransactional是如何开启全局事务的?PS:前文中搭建的Seata案例,seata的版本为1.3.0,而本文开始的源码分析将基于当前(2022年8月)最新的版本1.5.2进行源码解析。二、@GlobalTransactional我们知道可以将@GlobalTransactional注解标注在类或方法上 开启全局事务,下面来看一下@GlobalTransactional是如何开启的全局事务?在 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么一文中,我们知道了SpringBoot启动过程中会自动装配GlobalTransactionScanner类;1、GlobalTransactionScanner类(BPP)先看GlobalTransactionScanner类的继承关系:GlobalTransactionScanner类继承了AbstractAutoProxyCreator,AbstractAutoProxyCreator类又实现了BeanPostProcessor接口;因此GlobalTransactionScanner类也是BPP(BeanPostProcessor)。下面简单看一下AbstractAutoProxyCreator类;1)AbstractAutoProxyCreator(自动创建动态代理)AbstractAutoProxyCreator是Spring AOP中的一个抽象类,其主要功能是自动创建动态代理;因为其实现了BeanPostProcessor接口,所以在类加载到Spring容器之前,会进入到其wrapIfNecessary()方法对Bean进行代理包装,后续调用Bean之将委托给指定的拦截器。另外其getAdvicesAndAdvisorsForBean()方法用于给子类实现,由子类决定一个Bean是否需要被代理(是否存在切面);并且它还可以返回只应用于特定Bean实例的附加拦截器;2)BeanPostProcessor(对Bean进行修改的入口)BeanPostProcessor是Bean的后置处理器,可以通过实现其 并 覆写其postProcessBeforeInitialization() 和 postProcessAfterInitialization() 方法在Bean初始化前后对其进行修改;AbstractAutoProxyCreator正是通过覆写BeanPostProcessor的 postProcessAfterInitialization() 创建并返回Bean的代理对象;下面从SpringBoot启动流程来看针对标注了@GlobalTransactional的类 或 类中包含标注了@GlobalTransactional方法的类 创建动态代理的入口。3)从SpringBoot启动流程来看入口以TradeService类为例:package com.saint.trade.service; import com.saint.trade.feign.OrderFeignClient; import com.saint.trade.feign.StockFeignClient; import io.seata.spring.annotation.GlobalTransactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; * @author Saint @Service @RequiredArgsConstructor public class TradeService { private final StockFeignClient stockFeignClient; private final OrderFeignClient orderFeignClient; * 减库存,下订单 * @param userId * @param commodityCode * @param orderCount @GlobalTransactional public void purchase(String userId, String commodityCode, int orderCount) { stockFeignClient.deduct(commodityCode, orderCount); orderFeignClient.create(userId, commodityCode, orderCount); public void test() { System.out.println("hhahaha"); TradeService类被@Component衍生注解@Service标注,TradeService类又在SpringBoot扫描路径中,因此SpringBoot启动时会扫描到TradeService类;TradeService类中包含两个方法:purchase()、test(),其中purchase()方法被@GlobalTransactional注解标注。下面来看创建Bean时设计到BeanPostProcessor的代码片段:AbstractBeanFactory抽象Bean工厂的实现类AbstractAutowireCapableBeanFactory的initializeBean()方法是初始化Bean的入口:在Bean初始化之后会调用BeanPostProcessor的 postProcessAfterInitialization() 创建并返回Bean的代理对象;整体线程栈帧信息如下:最终进入到GlobalTransactionScanner覆写AbstractAutoProxyCreator抽象类的wrapIfNecessary()方法创建并返回代理对象(如果需要创建动态代理的话)。4)是否 / 如何生成动态代理对象从上面我们知道了GlobalTransactionScanner类的wrapIfNecessary()是创建动态代理的入口;这里接着来看wrapIfNecessary()方法如何判断是否Bean是否需要生成动态代理?// 对于扫描到的@GlobalTransactional注解, bean和beanName // 判断类 或 类的某一个方法是否被@GlobalTransactional注解标注,进而决定当前Class是否需要创建动态代理;存在则创建。 @Override protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // do checkers,做一些检查,不用花过多精力关注 if (!doCheckers(bean, beanName)) { return bean; try { synchronized (PROXYED_SET) { if (PROXYED_SET.contains(beanName)) { return bean; interceptor = null; //check TCC proxy TCC的动态代理 if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) { // init tcc fence clean task if enable useTccFence TCCBeanParserUtils.initTccFenceCleanTask(TCCBeanParserUtils.getRemotingDesc(beanName), applicationContext); //TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName)); ConfigurationCache.addConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, (ConfigurationChangeListener) interceptor); } else { // 先获取目标Class的接口 Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean); Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean); // existsAnnotation()表示类或类方法是否有被@GlobalTransactional注解标注,进而决定类是否需要被动态代理 if (!existsAnnotation(new Class[]{serviceInterface}) && !existsAnnotation(interfacesIfJdk)) { return bean; if (globalTransactionalInterceptor == null) { // 构建一个全局拦截器 globalTransactionalInterceptor = new GlobalTransactionalInterceptor(failureHandlerHook); ConfigurationCache.addConfigListener( ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, (ConfigurationChangeListener) globalTransactionalInterceptor); interceptor = globalTransactionalInterceptor; LOGGER.info("Bean[{}] with name [{}] would use interceptor [{}]", bean.getClass().getName(), beanName, interceptor.getClass().getName()); // 如果当前Bean没有被AOP代理 if (!AopUtils.isAopProxy(bean)) { // 基于Spring AOP的AutoProxyCreator对当前Class创建全局事务动态动态代理类 bean = super.wrapIfNecessary(bean, beanName, cacheKey); } else { AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean); Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null)); int pos; for (Advisor avr : advisor) { // Find the position based on the advisor's order, and add to advisors by pos // 找到seata切面的位置 pos = findAddSeataAdvisorPosition(advised, avr); advised.addAdvisor(pos, avr); PROXYED_SET.add(beanName); return bean; } catch (Exception exx) { throw new RuntimeException(exx); }第一步1> 首先doCheckers()方法对Bean做一些检查,包括:Bean是否已经生成了代理类、Bean不允许生成代理类.....第二步2> 对已经创建了动态代理的Bean的Set集合PROXYED_SET加锁做同步操作,如果PROXYED_SET中存在当前Bean的代理对象,则直接返回。第三步3> 根据@TwoPhaseBusinessAction注解判断是否是TCC模式下的动态代理(默认是AT模式,即不是TCC模式);第四步4> 获取Bean的目标Class,再通过existsAnnotation()方法查看 类或类方法是否有被@GlobalTransactional注解标注,进而决定类是否需要被动态代理;existsAnnotation()方法用于判断类是否需要被动态代理existsAnnotation()方法判断类是否需要被动态代理时:首先判断类上是否标注了@GlobalTransactional注解,如果标注了,则直接返回true,表示类需要被动态代理;否者,接着利用反射获取类的所有public方法,只要存在一个方法被@GlobalTransactional 或 @GlobalLock 注解标注,则表示当前类需要被动态代理;PS:聪明的你肯定发现,当一个类中有多个方法并且类没有被@GlobalTransactional注解标注,但只有一个方法被@GlobalTransactional注解标注时,这里针对整个类生成了动态代理对象,那么调用没加@GlobalTransactional注解的方法也会进入到代理对象,会不会有问题呢? 继续往后看,拦截器GlobalTransactionalInterceptor中会对其进行处理。当目标Class需要被动态代理时,则会初始化一个拦截器GlobalTransactionalInterceptor,用于拦截后面对目标Class的调用;那么GlobalTransactionalInterceptor是如何被应用于目标Class上做拦截的?第五步5> 如果针对当前Bean的代理是JDK 或 CGLIB动态代理,则根据GlobalTransactionalInterceptor创建切面,并应用到Bean上;在第四步时,我们对GlobalTransactionalInterceptor如何被应用于目标Class上做拦截持有疑问,在前面介绍AbstractAutoProxyCreator我们提到过:AbstractAutoProxyCreator的getAdvicesAndAdvisorsForBean()方法用于给子类实现,由子类决定一个Bean是否需要被代理(是否存在切面);并且它还可以返回只应用于特定Bean实例的附加拦截器;>>GlobalTransactionScanner覆写了getAdvicesAndAdvisorsForBean()方法,将上面初始化后的GlobalTransactionalInterceptor作为切面返回给AbstractAutoProxyCreator,供其创建动态代理类时使用;创建完代理对象之后,将代理对象放入到GlobalTransactionScanner的动态代理Bean的Set集合PROXYED_SET,以快去获取Bean的代理对象 并 防止Bean代理对象的重复创建。最后将代理对象返回,创建Bean流程结束。==至此,我们知道了所谓的@GlobalTransactional注解开启全局事务,实际就是针对类 或 类的方法上标注了@GlobalTransactional注解的类创建动态代理对象==三、全局事务的执行(前戏)上面我们知道了所谓的@GlobalTransactional注解开启全局事务,其实就是类 或 类的方法上标注了@GlobalTransactional注解的类创建动态代理对象。但是动态代理对象是针对类的;当一个类中有多个方法并且类没有被@GlobalTransactional注解标注,但只有一个方法被@GlobalTransactional注解标注时,这里针对整个类生成了动态代理对象,当调用Bean时,拦截器GlobalTransactionalInterceptor会做进一步处理,保证只有加了@GlobalTransactional注解的方法才会开启全局事务。先看GlobalTransactionalInterceptor类的继承图:GlobalTransactionalInterceptor实现了MethodInterceptor接口,所以当每次执行添加了 GlobalTransactionalInterceptor拦截器的Bean的方法时,都会进入到GlobalTransactionalInterceptor类覆写MethodInterceptor接口的invoke()方法:@Override public Object invoke(final MethodInvocation methodInvocation) throws Throwable { // method invocation是一次方法调用,一定是针对某个对象的方法调用; // methodInvocation.getThis()就是拿到当前方法所属的对象; // AopUtils.getTargetClass()获取到当前实例对象所对应的Class Class<?> targetClass = methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null; // 通过反射获取到被调用目标Class的method方法 Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass); // 如果目标method不为空,并且方法的DeclaringClass不是Object if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) { // 通过BridgeMethodResolver寻找method的桥接方法 final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod); // 获取目标方法的@GlobalTransactional注解 final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, targetClass, GlobalTransactional.class); // 如果目标方法被@GlobalLock注解标注,获取到@GlobalLock注解内容 final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class); // 如果禁用了全局事务 或 开启了事务降级检查并且降级检查次数大于等于降级检查允许的次数 // 则localDisable等价于全局事务被禁用了 boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes); // 如果全局事务没有被禁用 if (!localDisable) { // 全局事务注解不为空 或者 AOP切面全局事务核心配置不为空 if (globalTransactionalAnnotation != null || this.aspectTransactional != null) { AspectTransactional transactional; if (globalTransactionalAnnotation != null) { // 构建一个AOP切面全局事务核心配置,配置的数据从全局事务注解中取 transactional = new AspectTransactional(globalTransactionalAnnotation.timeoutMills(), globalTransactionalAnnotation.name(), globalTransactionalAnnotation.rollbackFor(), globalTransactionalAnnotation.rollbackForClassName(), globalTransactionalAnnotation.noRollbackFor(), globalTransactionalAnnotation.noRollbackForClassName(), globalTransactionalAnnotation.propagation(), globalTransactionalAnnotation.lockRetryInterval(), globalTransactionalAnnotation.lockRetryTimes()); } else { transactional = this.aspectTransactional; // 真正处理全局事务的入口 return handleGlobalTransaction(methodInvocation, transactional); } else if (globalLockAnnotation != null) { // 获取事务锁 return handleGlobalLock(methodInvocation, globalLockAnnotation); // 直接运行目标方法 return methodInvocation.proceed(); }假如我们调用TradeService类中没有标注@GlobalTransactional注解的test()方法;invoke()方法中会再次判断 当前调用的bean的方法 或 方法所处的类上是否标注了@GlobalTransactional注解,如果没有标注,则执行运行目标方法;否则才会以全局事务的方式执行方法。四、总结所谓的@GlobalTransactional注解开启全局事务,实际就是针对类 或 类的方法上标注了@GlobalTransactional注解的类创建动态代理对象。在调用相应Bean的时候,会进入到动态代理对象的拦截器GlobalTransactionalInterceptor,
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么本文接着上文中的Seata Client聊的GlobalTransactionScanner来聊一聊Seata Client 如何 与Seata Server建立连接、通信?PS:前文中搭建的Seata案例,seata的版本为1.3.0,而本文开始的源码分析将基于当前(2022年8月)最新的版本1.5.2进行源码解析。二、概述在前文(【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么),我们聊了随着Spring容器初始化完毕,会调用GlobalTransactionScanner的初始化逻辑(即:afterPropertiesSet()方法),进而调用initClient()方法初始化seata client;初始化Seata Client时,TM和RM的逻辑不同,TM会直接和Seata Server建立长连接;而RM在AT模式下不会直接和Seata Server建立长连接。真正建立长连接的地方时实例化DataSourceProxy时。首先我们要知道Seata Client 与Seata Server的通信是借助Netty的Channel(网络通道)来完成的,即所谓的建立长连接就是通过Netty的Channel进行通信;本文就看一下TMClient.init()、RMClient.init()方法是如何将TM、RM与TC(Seata Server)建立连接通信的?对TMClient 和 RMClient而言,除自身之外,还会设计到额外的三个类,一定要先明确这几个类的关系,不然看到后面跳过来跳过去的很乱,其关系图如下:三、TM事务管理器初始化TM全称:Transaction Manager,中文名:事务管理器,其定义全局事务的范围:开始全局事务、提交或回滚全局事务。在GlobalTransactionScanner类的如下代码段会进行TM的初始化:TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);1、TM初始化流程图2、TM初始化流程TMClient.init()方法入参包括:当前应用的ID、事务组名称,其中获取一个TmNettyRemotingClient实例,然后对其进行初始化;1)获取TmNettyRemotingClient实例整体代码执行流程如下:在获取TmNettyRemotingClient实例时,首先直接从Spring容器中获取到一个的TmNettyRemotingClient实例,然后再将应用的ID、事务分组名称、鉴权信息设置到TmNettyRemotingClient实例中;这其中有几个细节点,下面展开聊一聊:1> TmNettyRemotingClient实例化实例化TmNettyRemotingClient时采用 DCL(Double Check Lock)保证多线程环境下只会实例化一个TmNettyRemotingClient实例:其中用于处理消息的线程池的配置如下:核心线程数和最大线程数都为16;线程过期时间为Integer.MAX_VALUE,单位为秒;阻塞队列为有界的、最大容量为2000的LinkedBlockingQueue;所用线程工厂中生产出的线程名称的前缀为:rpcDispatch_1,其中的1表示TransactionRole为TM。真正实例化TmNettyRemotingClient时首先会进入其父类AbstractNettyRemotingClient的构造器(下面聊),然后基于SPI扩展加载鉴权签名组件AuthSigner,接着获取开启批量发送请求的配置,默认不开启;注意注册一个配置变更的监听器。2> AbstractNettyRemotingClient实例化(1)构造器中首先调用父类AbstractNettyRemoting的构造器设置用于处理消息的线程池:(2)设置当前事务角色为TMROLE;(3)实例化一个NettyClientBootstrap,其中组成了原生netty的Bootstrap、EventLoopGroup、EventExecutorGroup;用于和seata server通信;其中netty工作线程名的前缀默认为:NettyClientSelector;工作线程池的数量为1;(4)给Bootstrap设置消息处理器Handler(ChannelOutboundHandler),其为:AbstractNettyRemotingClient.ClientHandler。(5) 实例化netty的channel管理器,用于管理channel连接;方法一路返回,进入到初始化TmNettyRemotingClient。2)初始化TmNettyRemotingClient初始化TmNettyRemotingClient时会做三件事:注册一些请求处理组件;调用其父类AbstractNettyRemotingClient的初始化方法定时对tx事务组进行重连、请求超时检查,启动netty客户端组件;如果事务分组不为空,通过长连接管理组件对事务分组建立一个长连接;下面细看一下:1> 注册一些请求处理组件seata server可以主动给seata client发送一些请求过来,对于netty里收到不同的请求需要有不同的请求处理组件;所以此处需要注册一些请求处理组件;消息处理器是用来处理消息的,其根据消息的不同类型选择不同的消息处理器来处理消息(属于典型的策略模式);请求处理组件分为两大类:TC响应处理组件(处理seata server的请求)、心跳消息处理组件。所谓的注册消息处理器本质上就是将处理器RemotingProcessor和处理消息的线程池ExecutorService包装成一个Pair,然后将Pair作为Value,messageType作为key放入一个Map(processorTable)中;/** * This container holds all processors. * processor type {@link MessageType} protected final HashMap<Integer/*MessageType*/, Pair<RemotingProcessor, ExecutorService>> processorTable = new HashMap<>(32);2> 初始化AbstractNettyRemotingClient注册完请求处理组件之后,会使用原子类型(AtomicBoolean)变量initialized + CAS确保AbstractNettyRemotingClient仅会初始化一次。AbstractNettyRemotingClient的初始化同样做了三件事:启动一个延时60s,每隔10s对tx事务分组(seata server 列表)发起一个重新连接请求;调用其父类AbstractNettyRemoting的初始化方法:启动一个延时3s,每3s执行一次的定时任务,做请求超时检查;启动netty客户端组件,其seata server可以与seata client通信;下面我们细看一下AbstractNettyRemoting的初始化、netty客户端组件的启动;(1)AbstractNettyRemoting初始化其中仅会启动一个延时3s,每3s执行一次的定时任务,做请求超时检查;请求超时检查的细节如下:所谓的请求超时检查,实际是指当seata client发送请求到seata server时,会使用MessageFuture(组合了CompletableFuture)来接收返回值,如果seata server及时返回结果会将MessageFuture从futures中移除。(2)启动netty客户端组件@Override public void start() { if (this.defaultEventExecutorGroup == null) { this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(nettyClientConfig.getClientWorkerThreads(), new NamedThreadFactory(getThreadPrefix(nettyClientConfig.getClientWorkerThreadPrefix()), nettyClientConfig.getClientWorkerThreads())); // 真正的基于netty API构建一个bootstrap this.bootstrap // 设置对应的NioEventGroup,工作线程组,默认一个线程就够了 .group(this.eventLoopGroupWorker) .channel(nettyClientConfig.getClientChannelClazz()) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis()) .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize()) .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize()); if (nettyClientConfig.enableNative()) { if (PlatformDependent.isOsx()) { if (LOGGER.isInfoEnabled()) { LOGGER.info("client run on macOS"); } else { bootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED) .option(EpollChannelOption.TCP_QUICKACK, true); // netty网络通信数据处理组件(PipeLine)进行初始化 bootstrap.handler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); // IdleStateHandler,空闲状态检查Handler,如果有数据通过 记录一下数据通过的时间 // 如果超过很长时间都空闲,没有数据过来,则触发一个user triggered event给ClientHandler进行处理 pipeline.addLast(new IdleStateHandler( nettyClientConfig.getChannelMaxReadIdleSeconds(), nettyClientConfig.getChannelMaxWriteIdleSeconds(), nettyClientConfig.getChannelMaxAllIdleSeconds())) // 基于seata通信协议的编码器和解码器 .addLast(new ProtocolV1Decoder()) .addLast(new ProtocolV1Encoder()); if (channelHandlers != null) { addChannelPipelineLast(ch, channelHandlers); if (initialized.compareAndSet(false, true) && LOGGER.isInfoEnabled()) { LOGGER.info("NettyClientBootstrap has started"); }NettyClientBootstrap在启动的过程中设置了4个ChannelHandler:IdleStateHandler:处理心跳ProtocolV1Decoder:消息解码器ProtocolV1Encoder:消息编码器AbstractNettyRemotingClient.ClientHandler:处理各种消息AbstractNettyRemotingClient.ClientHandler类ClientHandler类上有个@ChannelHandler.Sharable注解,其表示所有的连接都会共用这一个ChannelHandler;所以当消息处理很慢时,会降低并发。processMessage(ctx, (RpcMessage) msg)方法中会根据消息类型获取到 请求处理组件(消息的处理过程是典型的策略模式),如果消息对应的处理器设置了线程池,则放到线程池中执行;如果对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发;所以在seata-server中大部分处理器都有对应的线程池。AbstractNettyRemotingClient.ClientHandler处理消息的方式和seata server的AbstractNettyRemotingServer.ServerHandler一致,此处不再赘述。见文章:【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么。3> 和事务分组中的每个seata server建立一个长连接这里有一个异常信息相信大家比较眼熟:再看acquireChannel()方法和seata server建立长连接的代码执行流程:TM/RM 和 TC 通信的关键在于Channel的创建,seata中通过池化的方式(借助了common-pool中的对象池)方式来创建、管理Channel。(1)涉及到的common-pool中的主要类:GenericKeydObjectPool<K, V>:KV泛型对象池,提供对所有对象的存取管理,而对象的创建由其内部的工厂类来完成KeyedPoolableObjectFactory<K, V>:KV泛型对象工厂,负责池化对象的创建,被对象池持有(2)涉及到的Seata中对象池实现相关的主要类:被池化管理的对象就是Channel,对应common-pool中的泛型VNettyPoolKey:Channel对应的Key,对应common-pool中的泛型K,NettyPoolKey主要包含两个信息:address:创建Channel时,对应的TC Server地址message:创建Channel时,向TC Server发送的RPC消息体GenericKeydObjectPool<NettyPoolKey,Channel>:Channel对象池NettyPoolableFactory:创建Channel的工厂类;至此,TM的初始化就此完毕!四、RM资源管理器初始化RM全称:Resource Manager,中文名:资源管理器,其管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。在GlobalTransactionScanner类的如下代码段会进行RM的初始化:RMClient.init(applicationId, txServiceGroup);1、RM初始化流程图和TM初始化流程图基本一致,建议大家研究源码的过程自己画一个出来。2、RM初始化流程RM的初始化流程和TM类似,下面我们看一下差异。1、实例化RmNettyRemotingClient在实例化RmNettyRemotingClient时,处理消息的线程池ThreadPoolExecutor,阻塞队列的最大容量是20000,而TmNettyRemotingClient的2000;此外,RmNettyRemotingClient工作线程的前缀名为:rpcDispatch_2,而TmNettyRemotingClient的工作线程的前缀名为:rpcDispatch_1;RmNettyRemotingClient的构造器中,和TmNettyRemotingClient做了同样的事,区别在于RmNettyRemotingClient中不需要基于SPI扩展加载鉴权签名组件AuthSigner:在实例化RmNettyRemotingClient之后,初始化RmNettyRemotingClient之前,会给RmNettyRemotingClient设置资源管理器DefaultResourceManager、事务消息处理器DefaultRMHandler。2、配置RmNettyRemotingClient配置RmNettyRemotingClient是指实例化DefaultResourceManager、DefaultRMHandler将其赋值到RmNettyRemotingClient的字段:resourceManager、transactionMessageHandler上。1> 实例化DefaultResourceManagerDefaultResourceManager中使用静态内部类单例模式保证DefaultResourceManager的唯一性;实例化流程如下:实例化DefaultResourceManager时会基于SPI扩展加载资源管理器ResourceManager,一共加载出四个:DataSourceManger、SagaResourceManager、ResourceManagerXA、TCCResourceManager。博主就感觉这里的命名啊,真是一个人一个写法!!!做为一个开源组件,实现的命名方式不需要统一一下吗!!!!!和事务模式的对应关系如下:2> 实例化DefaultRMHandlerDefaultRMHandler中使用静态内部类单例模式保证DefaultRMHandler的唯一性;实例化流程如下:实例化DefaultRMHandler时会基于SPI扩展加载资源管理器RMHandler,一共加载出四个:RMHandlerXA、RMHandlerTCC、RMHandlerSaga、RMHandlerAT。这个命名就很顺眼了,ResourceManager那是啥!!!和事务模式的对应关系如下:3、初始化RmNettyRemotingClient整体初始化流程和TM一致,差异点在于最后在与seata server建立长连接时会额外判断资源管理器ResourceManager中是否已经加载了连接资源;默认没有加载连接资源,所以初始化RMClient时不会立刻和seata server建立长连接。而RM的初始化之所以要判断资源是否存在也很好理解,RM就是管理资源的,没有资源也就没有必要理解和Seata Server建立长连接。既然初始化时不会立刻建立长连接,定时任务每30s才会与seata server重新建立长连接,假如在RM初始化后、定时任务执行之前加载了数据库资源,开始要进行一个分布式事务的流程,此时RM还没有seata server(TC)建立channel通信,重大bug啊!!既然seata都开源运行了那么久,应该不会存在这个bug吧,我们大胆推测:在创建数据库资源时就会立刻让RM和TC建立长连接。五、DataSourceProxyDataSourceProxy是使用seata 实现分布式事务(AT模式)必要引入的DataSource代理,其对数据库操作进行代理。引入方式如下:@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 * @param druidDataSource The DruidDataSource * @return The default datasource @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); }1、实例化DataSourceProxy实例化DataSourceProxy的代码执行流程如下:实例化很简单,单纯的将要代理的DataSource组合到DataSourceProxy中,然后进行DataSourceProxy的初始化;2、初始化DataSourceProxy初始化DataSourceProxy时会做三件事:从数据库连接中获取出JDBC连接信息保存下来;初始化资源ID,针对单机MySQL默认为数据库连接地址(不包含?后面的字符);其用于注册到TC中做标识;将当前Resource资源注册到TC;下面细看一下:初始化资源ID、将当前Resource资源注册到TC。1)初始化资源ID代码执行流程如下:针对单机MySQL,ResourceId默认为数据库连接地址(不包含?后面的字符)。2)注册Resource到TC代码执行流程如下:在DataSourceManager#registerResource()方法中会将 数据库资源的resourceID作为key、数据库资源作为value保存到本地缓存dataSourceCache中;private final Map<String, Resource> dataSourceCache = new ConcurrentHashMap<>();在RMClient初始化时,会不会立即和TC建立长连接,相对TMClient而言,额外的一个判断条件正是dataSourceCache是否为空。后续的注册资源流程其实就只是从NettyClientChannelManager拿到和TC建立长连接的channel,然后向其发送注册RM请求RegisterRMRequest。注册成功之后,TC(Seata Server)端会打印日志:[rverHandlerThread_1_9_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/seata_stock', applicationId='stock-service', transactionServiceGroup='saint-trade-tx-group'},channel:[id: 0x56942c85, L:/127.0.0.1:8091 - R:/127.0.0.1:59778],client version:1.5.2加载完DataSourceProxy之前,如果定时任务向TC发起注册RM请求RegisterRMRequest(每30s执行一次和seata server重新连接),则在TC端没有resourceId标识:[ttyServerNIOWorker_1_5_16] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='stock-service', transactionServiceGroup='saint-trade-tx-group'},channel:[id: 0xf216813f, L:/127.0.0.1:8091 - R:/127.0.0.1:59369],client version:1.5.2六、总结和后续本文我们聊了TM / RM在实例化GlobalTransactionScanner之后 开始初始化 向TC发起注册请求、建立长连接,但是针对RMClient并不会在初始化时立即和TC建立长连接;而是等到DataSourceProxy加载之后,才会立即和TC建立长连接;或者等每30秒执行一次的定时任务和TC建立长连接,但是如果DataSourceProxy还没有加载,则建立长连接时,资源的标识resourceID为null。下一篇文章开聊seata如何开启全局事务?
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】本文着重聊一聊Seata Client启动时都做了什么PS:前文中搭建的Seata案例,seata的版本为1.3.0,而本文开始的源码分析将基于当前(2022年8月)最新的版本1.5.2进行源码解析。二、Seata-Client依赖调整SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系,从Spring-cloud-alibaba的github官网来看当前最新的SpringCloud、SpringCloudAlibaba版本依赖关系如下:可以看到Spring Cloud Alibaba最新版本中依赖的Seata版本是1.5.1,而我们看的源码是最新的1.5.2;所以需要做如下操作:(1)调整SpringCloud版本:在根pom下调整Spring Cloud、Spring Cloud Alibaba的全局依赖版本:<properties> <spring-boot.version>2.3.12.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version> </properties> <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> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement>(2)在seata client项目下调整seata version<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.5.2</version> </dependency>至此,seata client 的运行/DEBUG环境准备就绪;三、从SpringBoot自动装配来看SeataClient加载的内容在Seata client引入spring-cloud-starter-alibaba-seata依赖之后,seata client的External Libraries中会多出五个seata相关的jar包:我们在SpringBoot专栏(精通Spring Boot)中聊过:SpringCloud集成其他组件时,就看自动装配类(XXXAutoConfiguration、XXXConfiguration),感兴趣的自行前往阅读SpringBoot源码解析相关文章;下面我们就按顺序一个一个的看各个jar包的META-INF/spring.factories文件;1、spring-cloud-starter-alibaba-seata-2.2.8.RELEASE.jarspring-cloud-starter-alibaba-seata-2.2.8.RELEASE.jar包的META-IINF/spring.factories文件内容如下:从类的命名大致可以推测出其中会对seata client之间调用时做一些处理;因为Seata Client之间通信可以有很多种方式:RestTemplate、SpringMVC、OpenFeign、集成Hystrix限流,所以此处有四个类:SeataRestTemplateAutoConfiguration、SeataHandlerInterceptorConfiguration、SeataFeignClientAutoConfiguration、SeataHystrixAutoConfiguration;下面我们逐个看一下;1)SeataRestTemplateAutoConfiguration如果你是看着博主的文章一路走过来的,你会发现这里和Ribbon对RestTemplate做负载均衡的入口很像:对RestTemplate添加一个ClientHttpRequestInterceptor拦截器,每次调用RestTemplate时,都会先走进拦截器;此处的拦截器为SeataRestTemplateInterceptor:public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { // 1、对httpRequest做一个包装 HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest); // 2、从本地事务上下文(ThreadLocal)中获取全局事务xid; todo 什么时候将xid放到ThreadLocal中见下一篇博文 String xid = RootContext.getXID(); // 如果可以从本地事务上下文中获取到全局事务xid,则将其添加到httpRequest的请求头上; if (!StringUtils.isEmpty(xid)) { requestWrapper.getHeaders().add(RootContext.KEY_XID, xid); // 执行HTTP请求 return clientHttpRequestExecution.execute(requestWrapper, bytes); }方法中主要就做一件事:从本地事务上下文(线程本地变量ThreadLocal)中获取全局事务xid,如果可以获取到,则将其添加到请求中的请求头中,传递到下一个seata client中。那么xid是什么时候生成的?什么时候放到ThreadLocal中的呢?点个关注,敬请期待下一篇博文。2)SeataHandlerInterceptorConfigurationSeataHandlerInterceptorConfiguration中是对SpringMVC调用时做的拦截,拦截针对的路径为:/**,拦截器为SeataHandlerInterceptor:public class SeataHandlerInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory .getLogger(SeataHandlerInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String xid = RootContext.getXID(); String rpcXid = request.getHeader(RootContext.KEY_XID); if (log.isDebugEnabled()) { log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid); if (StringUtils.isBlank(xid) && rpcXid != null) { RootContext.bind(rpcXid); if (log.isDebugEnabled()) { log.debug("bind {} to RootContext", rpcXid); return true; @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) { if (StringUtils.isNotBlank(RootContext.getXID())) { String rpcXid = request.getHeader(RootContext.KEY_XID); if (StringUtils.isEmpty(rpcXid)) { return; String unbindXid = RootContext.unbind(); if (log.isDebugEnabled()) { log.debug("unbind {} from RootContext", unbindXid); if (!rpcXid.equalsIgnoreCase(unbindXid)) { log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid); if (unbindXid != null) { RootContext.bind(unbindXid); log.warn("bind {} back to RootContext", unbindXid); }SeataHandlerInterceptor做了两件事:执行请求之前,从本地事务上下文(线程本地变量ThreadLocal)中获取全局事务xid;从请求头中获取全局事务xid(rpcXid);如果xid不存在,但是rpcXid存在,则将rpcXid作为xid绑定到全局事务上下文(线程本地变量ThreadLocal)上;请求执行完毕之后;如果全局事务上下文中xid存在,但是请求头中rpcXid不存在,则不对xid进行处理,但是如果全局事务上下文中xid存在 并且 请求头中包括rpcXid,则将xid从全局事务上下文中移除,如果移除的xid和rpcXid不相等,则将rpcXid作为xid绑定到全局事务上下文中。聪明的你肯定发现,这里只是将xid保存到事务上下文RootContext中,并没有xid在seata client之间传递的迹象!!而xid在seata client之间传递体现在远程调用的工具(OpenFeign、RestTemplate)中3)SeataFeignClientAutoConfiguration其实也和上面两个差不多;无论FeignClient是否集成Hystrix、Sentinel,在构建请求的Feign.Builder中,Client都是SeataFeignClient;关于Feign的源码解析请看博主的Feign系列文章。看看SeataFeignClient的逻辑:public class SeataFeignClient implements Client { private final Client delegate; private final BeanFactory beanFactory; private static final int MAP_SIZE = 16; SeataFeignClient(BeanFactory beanFactory) { this.beanFactory = beanFactory; this.delegate = new Client.Default(null, null); SeataFeignClient(BeanFactory beanFactory, Client delegate) { this.delegate = delegate; this.beanFactory = beanFactory; @Override public Response execute(Request request, Request.Options options) throws IOException { Request modifiedRequest = getModifyRequest(request); return this.delegate.execute(modifiedRequest, options); private Request getModifyRequest(Request request) { String xid = RootContext.getXID(); if (StringUtils.isEmpty(xid)) { return request; Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE); headers.putAll(request.headers()); List<String> seataXid = new ArrayList<>(); seataXid.add(xid); headers.put(RootContext.KEY_XID, seataXid); return Request.create(request.method(), request.url(), headers, request.body(), request.charset()); }在通过OpenFeign指定请求之前,也是会从事务上下文中获取xid,然后将其放到请求头中;4)SeataHystrixAutoConfigurationSeataHystrixConcurrencyStrategy类关键内容如下:Hystrix会将请求包装成Command命令执行,然后再将Command通过线程交给SpringMVC、RestTemplate等HTTP框架执行,这里只是将xid绑定到RootContext中;2、seata-all-1.5.2.jarseata-all-1.5.2.jar中不存在META-INf/spring.factories文件,所以此处并没有SpringBoot自动装配特性的应用,但是请注意io.seata.integration.http路径下的AbstractHttpExecutor类,其内容有如下逻辑从事务上下文中获取xid,然后将其设置到请求的请求头中(做seata client之间传递全局事务xid用):全局搜索这个类的使用,我们可以惊奇的发现,它的使用居然只体现在TEST测试类中,没有加载到Spring容器中!其底层使用的是HTTPClient,如果我们想在项目中使用HTTPClient做远程调用,可以使用如下方式:HttpResponse response = DefaultHttpExecutor.getInstance().executePost("http://127.0.0.1:8082", "/product/xxk",params, HttpResponse.class);3、seata-spring-autoconfigure-client:1.5.1.jarseata-spring-autoconfigure-client:1.5.1.jar下的META-INF/spring.facotries文件中有两个自动装配类:SeataTCCFenceAutoConfiguration、SeataClientEnvironmentPostProcessor;其中:SeataTCCFenceAutoConfiguration负责加载TCC的一些配置;SeataClientEnvironmentPostProcessor负责设置一些seata.client的配置信息4、seata-spring-autoconfigure-core:1.5.1.jarseata-spring-autoconfigure-core:1.5.1.jar下的META-INF/spring.facotries文件中有两个自动装配类:SeataCoreAutoConfiguration、SeataCoreEnvironmentPostProcessor;其中:SeataCoreAutoConfiguration中实例化了一个SpringApplicationContextProvider,其继承了ApplicationContextAware,可以用来从ApplicationContext中获取对象;SeataCoreEnvironmentPostProcessor负责设置一些seata.core的配置信息5、seata-spring-boot-starter:1.5.1.jarseata-spring-boot-starter:1.5.1.jar下的META-INF/spring.facotries文件内容如下:1)SeataDataSourceAutoConfigurationSeataDataSourceAutoConfiguration中仅负责实例化一个Bean(SeataAutoDataSourceProxyCreator),SeataAutoDataSourceProxyCreator是AT模式下自动代理数据库资源的切面,其继承自AbstractAutoProxyCreator类,使用CGLIB对DataSource做动态代理,后续对DataSource的访问都会进入到它内部,即它可以看做是一个拦截器;public class SeataAutoDataSourceProxyCreator extends AbstractAutoProxyCreator { private static final Logger LOGGER = LoggerFactory.getLogger(SeataAutoDataSourceProxyCreator.class); private final Set<String> excludes; private final String dataSourceProxyMode; private final Object[] advisors; public SeataAutoDataSourceProxyCreator(boolean useJdkProxy, String[] excludes, String dataSourceProxyMode) { setProxyTargetClass(!useJdkProxy); this.excludes = new HashSet<>(Arrays.asList(excludes)); this.dataSourceProxyMode = dataSourceProxyMode; this.advisors = buildAdvisors(dataSourceProxyMode); private Object[] buildAdvisors(String dataSourceProxyMode) { Advice advice = new SeataAutoDataSourceProxyAdvice(dataSourceProxyMode); return new Object[]{new DefaultIntroductionAdvisor(advice)}; @Override protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource customTargetSource) { return advisors; @Override protected boolean shouldSkip(Class<?> beanClass, String beanName) { if (excludes.contains(beanClass.getName())) { return true; return SeataProxy.class.isAssignableFrom(beanClass); @Override protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // we only care DataSource bean if (!(bean instanceof DataSource)) { return bean; // when this bean is just a simple DataSource, not SeataDataSourceProxy if (!(bean instanceof SeataDataSourceProxy)) { Object enhancer = super.wrapIfNecessary(bean, beanName, cacheKey); // this mean this bean is either excluded by user or had been proxy before if (bean == enhancer) { return bean; // else, build proxy, put <origin, proxy> to holder and return enhancer DataSource origin = (DataSource) bean; SeataDataSourceProxy proxy = buildProxy(origin, dataSourceProxyMode); DataSourceProxyHolder.put(origin, proxy); return enhancer; * things get dangerous when you try to register SeataDataSourceProxy bean by yourself! * if you insist on doing so, you must make sure your method return type is DataSource, * because this processor will never return any subclass of SeataDataSourceProxy LOGGER.warn("Manually register SeataDataSourceProxy(or its subclass) bean is discouraged! bean name: {}", beanName); SeataDataSourceProxy proxy = (SeataDataSourceProxy) bean; DataSource origin = proxy.getTargetDataSource(); Object originEnhancer = super.wrapIfNecessary(origin, beanName, cacheKey); // this mean origin is either excluded by user or had been proxy before if (origin == originEnhancer) { return origin; // else, put <origin, proxy> to holder and return originEnhancer DataSourceProxyHolder.put(origin, proxy); return originEnhancer; SeataDataSourceProxy buildProxy(DataSource origin, String proxyMode) { if (BranchType.AT.name().equalsIgnoreCase(proxyMode)) { return new DataSourceProxy(origin); if (BranchType.XA.name().equalsIgnoreCase(proxyMode)) { return new DataSourceProxyXA(origin); throw new IllegalArgumentException("Unknown dataSourceProxyMode: " + proxyMode); }2)SeataAutoConfigurationSeataAutoConfiguration是一个特别特别重要的自动装配类,其中仅实例化了两个类到Spring容器中,一个FailureHandler、一个GlobalTransactionScanner;1> FailureHandlerFailureHandler是事务执行失败的处理器,默认为DefaultFailureHandlerImpl,内容如下:public class DefaultFailureHandlerImpl implements FailureHandler { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultFailureHandlerImpl.class); * Retry 1 hours by default private static final int RETRY_MAX_TIMES = 6 * 60; private static final long SCHEDULE_INTERVAL_SECONDS = 10; private static final long TICK_DURATION = 1; private static final int TICKS_PER_WHEEL = 8; private HashedWheelTimer timer = new HashedWheelTimer( new NamedThreadFactory("failedTransactionRetry", 1), TICK_DURATION, TimeUnit.SECONDS, TICKS_PER_WHEEL); @Override public void onBeginFailure(GlobalTransaction tx, Throwable cause) { LOGGER.warn("Failed to begin transaction. ", cause); @Override public void onCommitFailure(GlobalTransaction tx, Throwable cause) { LOGGER.warn("Failed to commit transaction[" + tx.getXid() + "]", cause); timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.Committed), SCHEDULE_INTERVAL_SECONDS, TimeUnit.SECONDS); @Override public void onRollbackFailure(GlobalTransaction tx, Throwable originalException) { LOGGER.warn("Failed to rollback transaction[" + tx.getXid() + "]", originalException); timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.Rollbacked), SCHEDULE_INTERVAL_SECONDS, TimeUnit.SECONDS); @Override public void onRollbackRetrying(GlobalTransaction tx, Throwable originalException) { StackTraceLogger.warn(LOGGER, originalException, "Retrying to rollback transaction[{}]", new String[] {tx.getXid()}); timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.RollbackRetrying), SCHEDULE_INTERVAL_SECONDS, TimeUnit.SECONDS); protected class CheckTimerTask implements TimerTask { private final GlobalTransaction tx; private final GlobalStatus required; private int count = 0; private boolean isStopped = false; protected CheckTimerTask(final GlobalTransaction tx, GlobalStatus required) { this.tx = tx; this.required = required; @Override public void run(Timeout timeout) throws Exception { if (!isStopped) { if (++count > RETRY_MAX_TIMES) { LOGGER.error("transaction [{}] retry fetch status times exceed the limit [{} times]", tx.getXid(), RETRY_MAX_TIMES); return; isStopped = shouldStop(tx, required); timer.newTimeout(this, SCHEDULE_INTERVAL_SECONDS, TimeUnit.SECONDS); private boolean shouldStop(final GlobalTransaction tx, GlobalStatus required) { try { GlobalStatus status = tx.getStatus(); LOGGER.info("transaction [{}] current status is [{}]", tx.getXid(), status); if (status == required || status == GlobalStatus.Finished) { return true; } catch (TransactionException e) { LOGGER.error("fetch GlobalTransaction status error", e); return false; }2> GlobalTransactionScannerGlobalTransactionScanner是Seata的核心所在,在类图如下:部分类/接口作用如下:AbstractAutoProxyCreator: Spring框架内动态代理创建组件ConfigurationChangeListener: 关注配置变更事件监听器InitializingBean: Bean初始化回调ApplicationContextAware: 感知到SPring容器DisposableBean: 支持可抛弃Bean伴随着Spring容器初始化完毕,会调用GlobalTransactionScanner的初始化逻辑(即:afterPropertiesSet()方法),进而调用initClient()方法初始化seata client;初始化Seata Client时,TM和RM的逻辑不同,TM会直接和Seata Server建立长连接;而RM在AT模式下不会直接和Seata Server建立长连接。真正建立长连接的地方时实例化DataSourceProxy时。seata client和seata server见下一篇文章!下一篇重点对GlobalTransactionScanner类进行解析。3)HttpAutoConfigurationHttpAutoConfiguration继承SpringMVC的WebMvcConfigurerAdapter,而 WebMvcConfigurerAdapter又实现了WebMvcConfigurer接口;而HttpAutoConfiguration的作用其实和spring-cloud-starter-alibaba-seata-2.2.8.RELEASE.jar中的SeataHandlerInterceptorConfiguration类似;一样是对RootContext进行处理,给SpringMVC添加一个拦截器;就SpringMVC链路传递xid而言,使用spring-cloud-starter-alibaba-seata依赖 或 seata-spring-boot-starter依赖可以实现一样的效果;不过当需要用到OpenFeign、RestTemplate时需要使用spring-cloud-starter-alibaba-seata依赖来实现xid在seata client间传递的效果。4)SeataSagaAutoConfigurationSeataSagaAutoConfiguration主要为SAGA模式服务,具体细节聊到SAGA模式时再说,此处mock处理。四、总结和后续本文以SpringBoot的自动装配特性为基调出发,通过对每一个自动装配类内容的分析,可以知道xid是如何在seata client之间传递的、seata client的初始化逻辑、seata client和seata server建立长连接的入口、AT模式下RM如何RC建立长连接;
@[TOC]一、前言至此,seata系列的内容包括:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server本文着重聊一聊seata-server启动时都做了什么?PS:前文中搭建的Seata案例,seata的版本为1.3.0,而本文开始的源码分析将基于当前(2022年8月)最新的版本1.5.2进行源码解析。二、Seata Server启动Seata Server包含几个主要模块:Config(配置TC)、Store(TC运行时全局事务以及分支事务的相关信息通过Store持久化)、Coordinator(TC实现事务协调的核心)、Netty-RPC(负责TC与TM/RM交互)、Lock(资源全局锁的实现);1、找入口当要启动一个seata-server时,只需要执行压缩包中bin/目录下的seata-server.sh,在这个脚本中会运行seata-server.jar;即对应于源码工程中的server目录 / seata-server 模块,由于seata-server是一个SpringBoot项目,找到其启动类ServerApplication,里面仅仅指定了一个包扫描路径为io.seata,并无其余特殊配置;在启动类的同级目录下,有一个ServerRunner类;ServerRunner类实现了CommandLineRunner接口:而CommandLineRunner接口主要用于实现在Spring容器初始化后执行,并且在整个应用生命周期内只会执行一次;也就是说在Spring容器初始化后会执行ServerRunner#run()方法;ServerRunner#run()方法中仅仅调用了Server#start()方法;因此可以确定入口为io.seata.server.Server类的start()方法;2、整体执行流程Server#start()方法:public class Server { * The entry point of application. * @param args the input arguments public static void start(String[] args) { // create logger final Logger logger = LoggerFactory.getLogger(Server.class); //initialize the parameter parser //Note that the parameter parser should always be the first line to execute. //Because, here we need to parse the parameters needed for startup. // 1. 对配置文件做参数解析:包括registry.conf、file.conf的解析 ParameterParser parameterParser = new ParameterParser(args); // 2、初始化监控,做metric指标采集 MetricsManager.get().init(); // 将Store资源持久化方式放到系统的环境变量store.mode中 System.setProperty(ConfigurationKeys.STORE_MODE, parameterParser.getStoreMode()); // seata server里netty server 的io线程池(核心线程数50,最大线程数100) ThreadPoolExecutor workingThreads = new ThreadPoolExecutor(NettyServerConfig.getMinServerPoolSize(), NettyServerConfig.getMaxServerPoolSize(), NettyServerConfig.getKeepAliveTime(), TimeUnit.SECONDS, new LinkedBlockingQueue<>(NettyServerConfig.getMaxTaskQueueSize()), new NamedThreadFactory("ServerHandlerThread", NettyServerConfig.getMaxServerPoolSize()), new ThreadPoolExecutor.CallerRunsPolicy()); // 3、创建TC与RM/TM通信的RPC服务器--netty NettyRemotingServer nettyRemotingServer = new NettyRemotingServer(workingThreads); // 4、初始化UUID生成器(雪花算法) UUIDGenerator.init(parameterParser.getServerNode()); //log store mode : file, db, redis // 5、设置事务会话的持久化方式,有三种类型可选:file/db/redis SessionHolder.init(parameterParser.getSessionStoreMode()); LockerManagerFactory.init(parameterParser.getLockStoreMode()); // 6、创建并初始化事务协调器,创建时后台会启动一堆线程 DefaultCoordinator coordinator = DefaultCoordinator.getInstance(nettyRemotingServer); coordinator.init(); // 将DefaultCoordinator作为Netty Server的transactionMessageHandler; // 用于做AT、TCC、SAGA等不同事务类型的逻辑处理 nettyRemotingServer.setHandler(coordinator); // let ServerRunner do destroy instead ShutdownHook, see https://github.com/seata/seata/issues/4028 // 7、注册ServerRunner销毁(Spring容器销毁)的回调钩子函数 ServerRunner.addDisposable(coordinator); //127.0.0.1 and 0.0.0.0 are not valid here. if (NetUtil.isValidIp(parameterParser.getHost(), false)) { XID.setIpAddress(parameterParser.getHost()); } else { String preferredNetworks = ConfigurationFactory.getInstance().getConfig(REGISTRY_PREFERED_NETWORKS); if (StringUtils.isNotBlank(preferredNetworks)) { XID.setIpAddress(NetUtil.getLocalIp(preferredNetworks.split(REGEX_SPLIT_CHAR))); } else { XID.setIpAddress(NetUtil.getLocalIp()); // 8、启动netty Server,用于接收TM/RM的请求 nettyRemotingServer.init(); }Server端的启动流程大致做了八件事:对配置文件(包括registry.conf、file.conf)做参数解析;初始化监控,做metric指标采集;创建TC与RM/TM通信的RPC服务器(NettyRemotingServer)--netty;初始化UUID生成器(雪花算法),用于生成全局事务id和分支事务id;设置事务会话(SessionHolder)、全局锁(LockManager)的持久化方式并初始化,有三种类型可选:file/db/redis;创建并初始化事务协调器(DefaultCoordinator),后台启动一堆线程做定时任务,并将DefaultCoordinator绑定到RPC服务器上做为transactionMessageHandler;注册ServerRunner销毁(Spring容器销毁)的回调钩子函数DefaultCoordinator;启动netty Server,用于接收TM/RM的请求;1)对配置文件做参数解析具体代码执行流程如下:ParameterParser的init()方法中:首先从启动命令(运行时参数)中解析;接着判断server端是否在容器中启动,是则从容器环境中获取seata环境、host、port、serverNode、storeMode存储模式等信息;如果storeMode不存在,则从配置中心/文件中获取配置。// 解析运行期参数,默认什么里面什么都没有 private void getCommandParameters(String[] args) { JCommander jCommander = JCommander.newBuilder().addObject(this).build(); jCommander.parse(args); if (help) { jCommander.setProgramName(PROGRAM_NAME); jCommander.usage(); System.exit(0); // server端在容器中启动,则从容器环境中读取环境、host、port、server节点以及StoreMode存储模式 private void getEnvParameters() { // 设置seata的环境 if (StringUtils.isBlank(seataEnv)) { seataEnv = ContainerHelper.getEnv(); // 设置Host if (StringUtils.isBlank(host)) { host = ContainerHelper.getHost(); // 设置端口号 if (port == 0) { port = ContainerHelper.getPort(); if (serverNode == null) { serverNode = ContainerHelper.getServerNode(); if (StringUtils.isBlank(storeMode)) { storeMode = ContainerHelper.getStoreMode(); if (StringUtils.isBlank(sessionStoreMode)) { sessionStoreMode = ContainerHelper.getSessionStoreMode(); if (StringUtils.isBlank(lockStoreMode)) { lockStoreMode = ContainerHelper.getLockStoreMode(); }2)初始化监控默认不开启,此处不做过多介绍3)创建TC与RM/TM通信的RPC服务器单纯的new一个NettyRemotingServer,也没啥可说的;4)初始化UUID生成器UUID底层采用雪花算法,其用于生成全局事务id和分支事务id;代码执行流程如下:UUIDGenerator会委托IdWorker来生成雪花id,生成的雪花Id由0、10位的workerId、41位的时间戳、12位的sequence序列号组成。IdWorkerIdWorker中有8个重要的成员变量/常量:/** * Start time cut (2020-05-03) private final long twepoch = 1588435200000L; * The number of bits occupied by workerId private final int workerIdBits = 10; * The number of bits occupied by timestamp private final int timestampBits = 41; * The number of bits occupied by sequence private final int sequenceBits = 12; * Maximum supported machine id, the result is 1023 private final int maxWorkerId = ~(-1 << workerIdBits); * business meaning: machine ID (0 ~ 1023) * actual layout in memory: * highest 1 bit: 0 * middle 10 bit: workerId * lowest 53 bit: all 0 private long workerId; * 又是一个雪花算法(64位,8字节) * timestamp and sequence mix in one Long * highest 11 bit: not used * middle 41 bit: timestamp * lowest 12 bit: sequence private AtomicLong timestampAndSequence; * 从一个long数组类型中抽取出一个时间戳伴随序列号,偏向一个辅助性质 * mask that help to extract timestamp and sequence from a long private final long timestampAndSequenceMask = ~(-1L << (timestampBits + sequenceBits));变量/常量解释:常量twepoch表示我们的时间戳时间从2020-05-03开始计算,即当前时间的时间戳需要减去twepoch的值1588435200000L;常量workerIdBits表示机器号workerId占10位;常量timestampBits表示时间戳timestamp占41位;常量sequenceBits表示序列化占12位;常量maxWorkerId表示机器号的最大值为1023;long类型的变量workerId本身也是一个雪花算法,只是从开头往后数,第2位开始,一共10位用来表示workerId,其余位全是0;AtomicLong类型的变量timestampAndSequence,其本身也是一个雪花算法,头11位不使用,中间41位表示timestamp,最后12位表示sequence;long类型的常量timestampAndSequenceMask,用于从一个完整的雪花ID(long类型)中摘出timestamp 和 sequenceIdWorker构造器中会分别初始化TimestampAndSequence、WorkerId。1> initTimestampAndSequence()initTimestampAndSequence()方法负责初始化timestamp和sequence;private void initTimestampAndSequence() { // 拿到当前时间戳 - (2020-05-03 时间戳)的数值,即当前时间相对2020-05-03的时间戳 long timestamp = getNewestTimestamp(); // 把时间戳左移12位,后12位流程sequence使用 long timestampWithSequence = timestamp << sequenceBits; // 把混合sequence(默认为0)的时间戳赋值给timestampAndSequence this.timestampAndSequence = new AtomicLong(timestampWithSequence); // 获取当前时间戳 private long getNewestTimestamp() { //当前时间的时间戳减去2020-05-03的时间戳 return System.currentTimeMillis() - twepoch; }2> initWorkerId(Long)initWorkerId(Long workerId)方法负责初始化workId,默认不会传过来workerId,如果传过来则使用传过来的workerId,并校验其不能大于1023,然后将其左移53位;private void initWorkerId(Long workerId) { if (workerId == null) { // workid为null时,自动生成一个workerId workerId = generateWorkerId(); // workerId最大只能是1023,因为其只占10bit if (workerId > maxWorkerId || workerId < 0) { String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId); throw new IllegalArgumentException(message); this.workerId = workerId << (timestampBits + sequenceBits); }如果没传则基于MAC地址生成;如果基于MAC地址生成workerId出现异常,则也1023为基数生成一个随机的workerId;最后同样,校验workerId不能大于1023,然后将其左移53位;5)设置事务会话(SessionHolder)、全局锁(LockManager)的持久化方式并初始化1> SessionHolderSessionHolder负责事务会话Session的持久化,一个session对应一个事务,事务又分为全局事务和分支事务;SessionHolder支持db,file和redis的持久化方式,其中redis和db支持集群模式,项目上推荐使用redis或db模式;SessionHolder有五个重要的属性,如下:// 用于管理所有的Setssion,以及Session的创建、更新、删除等 private static SessionManager ROOT_SESSION_MANAGER; // 用于管理所有的异步commit的Session,包括创建、更新以及删除 private static SessionManager ASYNC_COMMITTING_SESSION_MANAGER; // 用于管理所有的重试commit的Session,包括创建、更新以及删除 private static SessionManager RETRY_COMMITTING_SESSION_MANAGER; // 用于管理所有的重试rollback的Session,包括创建、更新以及删除 private static SessionManager RETRY_ROLLBACKING_SESSION_MANAGER; // 用于管理分布式锁 private static DistributedLocker DISTRIBUTED_LOCKER;这五个属性在SessionHolder#init()方法中初始化,init()方法源码如下:public static void init(String mode) { if (StringUtils.isBlank(mode)) { mode = CONFIG.getConfig(ConfigurationKeys.STORE_SESSION_MODE, CONFIG.getConfig(ConfigurationKeys.STORE_MODE, SERVER_DEFAULT_STORE_MODE)); StoreMode storeMode = StoreMode.get(mode); // 根据storeMode采用SPI机制初始化SessionManager // db模式 if (StoreMode.DB.equals(storeMode)) { ROOT_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.DB.getName()); ASYNC_COMMITTING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.DB.getName(), new Object[]{ASYNC_COMMITTING_SESSION_MANAGER_NAME}); RETRY_COMMITTING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.DB.getName(), new Object[]{RETRY_COMMITTING_SESSION_MANAGER_NAME}); RETRY_ROLLBACKING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.DB.getName(), new Object[]{RETRY_ROLLBACKING_SESSION_MANAGER_NAME}); DISTRIBUTED_LOCKER = DistributedLockerFactory.getDistributedLocker(StoreMode.DB.getName()); } else if (StoreMode.FILE.equals(storeMode)) { // 文件模式 String sessionStorePath = CONFIG.getConfig(ConfigurationKeys.STORE_FILE_DIR, DEFAULT_SESSION_STORE_FILE_DIR); if (StringUtils.isBlank(sessionStorePath)) { throw new StoreException("the {store.file.dir} is empty."); ROOT_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.FILE.getName(), new Object[]{ROOT_SESSION_MANAGER_NAME, sessionStorePath}); ASYNC_COMMITTING_SESSION_MANAGER = ROOT_SESSION_MANAGER; RETRY_COMMITTING_SESSION_MANAGER = ROOT_SESSION_MANAGER; RETRY_ROLLBACKING_SESSION_MANAGER = ROOT_SESSION_MANAGER; DISTRIBUTED_LOCKER = DistributedLockerFactory.getDistributedLocker(StoreMode.FILE.getName()); } else if (StoreMode.REDIS.equals(storeMode)) { // redis模式 ROOT_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.REDIS.getName()); ASYNC_COMMITTING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.REDIS.getName(), new Object[]{ASYNC_COMMITTING_SESSION_MANAGER_NAME}); RETRY_COMMITTING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.REDIS.getName(), new Object[]{RETRY_COMMITTING_SESSION_MANAGER_NAME}); RETRY_ROLLBACKING_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.REDIS.getName(), new Object[]{RETRY_ROLLBACKING_SESSION_MANAGER_NAME}); DISTRIBUTED_LOCKER = DistributedLockerFactory.getDistributedLocker(StoreMode.REDIS.getName()); } else { // unknown store throw new IllegalArgumentException("unknown store mode:" + mode); // 根据storeMode重新加载 reload(storeMode); }init()方法中根据storeMode采用SPI机制初始化SessionManager,SessionManager有三个实现类:2> LockerManager和SessionHolder一样,LockManagerFactory#init()方法同样根据storeMode采用SPI机制初始化LockManager,LockManager有三个实现类:6)创建并初始化事务协调器(DefaultCoordinator)DefaultCoordinator是事务协调的核心,比如:开启、提交、回滚全局事务,注册、提交、回滚分支事务都是通过DefaultCoordinator进行协调处理的。(1)先来看DefaultCoordinator的创建;使用Double Check Lock(DCL-双重检查锁)机制获取到单例的DefaultCoordinator;如果DefaultCoordinator为实例化过,则new一个:在DefaultCoordinator的类构造器中,首先绑定远程通信的Server的具体实现到内部成员中,然后实例化一个DefaultCore,DefaultCore是AT、TCC、XA、Saga四种分布式事务模式的具体实现类;DefaultCore的类构造器中首先通过SPI机制加载出所有的AbstractCore的子类,一共有四个:ATCore、TccCore、SagaCore、XACore;然后将AbstractCore子类可以处理的事务模式作为Key、AbstractCore子类作为Value存储到一个缓存Map(Map<BranchType, AbstractCore> coreMap)中;private static Map<BranchType, AbstractCore> coreMap = new ConcurrentHashMap<>();后续通过BranchType(分支类型)就可以从coreMap中获取到相应事务模式的具体AbstractCore实现类。(2)初始化DefaultCoordinator;所谓的初始化,其实就是后台启动一堆线程做定时任务;去定时处理重试回滚、重试提交、异步提交、超时的检测,以及定时清理undo_log。除定时清理undo_log外,其余定时任务的处理逻辑基本都是:首先获取所有可回滚的全局事务会话Session,如果可回滚的分支事务为空,则直接返回;否者,遍历所有的可回滚Session;为了防止重复回滚,如果session的状态是正在回滚中并且session不是死亡的,则直接返回;如果Session重试回滚超时,从缓存中删除已经超时的回滚Session;发布session回滚完成事件给到Metric,对回滚中的Session添加Session生命周期的监听;使用DefaultCoordinator组合的DefaultCore执行全局回滚。以处理重试回滚的方法handleRetryRollbacking()为例:protected void handleRetryRollbacking() { SessionCondition sessionCondition = new SessionCondition(rollbackingStatuses); sessionCondition.setLazyLoadBranch(true); // 获取所有的可回滚的全局事务session Collection<GlobalSession> rollbackingSessions = SessionHolder.getRetryRollbackingSessionManager().findGlobalSessions(sessionCondition); // 如果可回滚的分支事务为空,则直接返回 if (CollectionUtils.isEmpty(rollbackingSessions)) { return; long now = System.currentTimeMillis(); // 遍历所有的可回滚Session, SessionHelper.forEach(rollbackingSessions, rollbackingSession -> { try { // prevent repeated rollback // 防止重复回滚:如果session的状态是正在回滚中并且session不是死亡的,则直接返回。 if (rollbackingSession.getStatus().equals(GlobalStatus.Rollbacking) && !rollbackingSession.isDeadSession()) { // The function of this 'return' is 'continue'. return; // 判断回滚是否重试超时 if (isRetryTimeout(now, MAX_ROLLBACK_RETRY_TIMEOUT.toMillis(), rollbackingSession.getBeginTime())) { if (ROLLBACK_RETRY_TIMEOUT_UNLOCK_ENABLE) { rollbackingSession.clean(); // Prevent thread safety issues // 删除已经超时的回滚Session SessionHolder.getRetryRollbackingSessionManager().removeGlobalSession(rollbackingSession); LOGGER.error("Global transaction rollback retry timeout and has removed [{}]", rollbackingSession.getXid()); SessionHelper.endRollbackFailed(rollbackingSession, true); // rollback retry timeout event // 发布session回滚完成事件给到Metric MetricsPublisher.postSessionDoneEvent(rollbackingSession, GlobalStatus.RollbackRetryTimeout, true, false); //The function of this 'return' is 'continue'. return; // 对回滚中的Session添加Session生命周期的监听 rollbackingSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager()); // 使用DefaultCoordinator组合的DefaultCore执行全局回滚 core.doGlobalRollback(rollbackingSession, true); } catch (TransactionException ex) { LOGGER.info("Failed to retry rollbacking [{}] {} {}", rollbackingSession.getXid(), ex.getCode(), ex.getMessage()); }7)注册ServerRunner销毁(Spring容器销毁)的回调钩子函数DefaultCoordinator8)启动NettyServer(NettyRemotingServer)启动NettyRemotingServer时会做两件事:注册消息处理器、初始化并启动NettyServerBootstrap;1> 首先注册消息处理器消息处理器是用来处理消息的,其根据消息的不同类型选择不同的消息处理器来处理消息(属于典型的策略模式);每个消息类型和对应的处理器关系如下:所谓的注册消息处理器本质上就是将处理器RemotingProcessor和处理消息的线程池ExecutorService包装成一个Pair,然后将Pair作为Value,messageType作为key放入一个Map(processorTable)中;/** * This container holds all processors. * processor type {@link MessageType} protected final HashMap<Integer/*MessageType*/, Pair<RemotingProcessor, ExecutorService>> processorTable = new HashMap<>(32);2> 初始化NettyRemotingServer在初始化NettyRemotingServer之前会通过AtomicBoolean类型的原子变量initialized + CAS操作确保仅会有一个线程进行NettyRemotingServer的初始化;再看NettyRemotingServer的类继承图:CAS成功后进入到NettyRemotingServer的父类AbstractNettyRemotingServer#init()方法;方法中:(1)首先调用父类AbstractNettyRemoting的init()方法:启动一个延时3s,每3s执行一次的定时任务,做请求超时检查;(2)紧接着启动ServerBootstrap(就正常的nettyServer启动):NettyRemotingServer在启动的过程中设置了4个ChannelHandler:IdleStateHandler:处理心跳ProtocolV1Decoder:消息解码器ProtocolV1Encoder:消息编码器AbstractNettyRemotingServer.ServerHandler:处理各种消息AbstractNettyRemotingServer.ServerHandler类ServerHandler类上有个@ChannelHandler.Sharable注解,其表示所有的连接都会共用这一个ChannelHandler;所以当消息处理很慢时,会降低并发。processMessage(ctx, (RpcMessage) msg)方法中会根据消息类型获取到 请求处理组件(消息的处理过程是典型的策略模式),如果消息对应的处理器设置了线程池,则放到线程池中执行;如果对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发;所以在seata-server中大部分处理器都有对应的线程池。/** * Rpc message processing. * @param ctx Channel handler context. * @param rpcMessage rpc message. * @throws Exception throws exception process message error. * @since 1.3.0 protected void processMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("%s msgId:%s, body:%s", this, rpcMessage.getId(), rpcMessage.getBody())); Object body = rpcMessage.getBody(); if (body instanceof MessageTypeAware) { MessageTypeAware messageTypeAware = (MessageTypeAware) body; // 根据消息的类型获取到请求处理组件和请求处理线程池组成的Pair final Pair<RemotingProcessor, ExecutorService> pair = this.processorTable.get((int) messageTypeAware.getTypeCode()); if (pair != null) { // 如果消息对应的处理器设置了线程池,则放到线程池中执行 if (pair.getSecond() != null) { try { pair.getSecond().execute(() -> { try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } finally { MDC.clear(); } catch (RejectedExecutionException e) { // 线程池拒绝策略之一,抛出异常:RejectedExecutionException LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(), "thread pool is full, current max pool size is " + messageExecutor.getActiveCount()); if (allowDumpStack) { String name = ManagementFactory.getRuntimeMXBean().getName(); String pid = name.split("@")[0]; long idx = System.currentTimeMillis(); try { String jstackFile = idx + ".log"; LOGGER.info("jstack command will dump to " + jstackFile); Runtime.getRuntime().exec(String.format("jstack %s > %s", pid, jstackFile)); } catch (IOException exx) { LOGGER.error(exx.getMessage()); allowDumpStack = false; } else { // 对应的处理器没有设置线程池,则直接执行;如果某条消息处理特别慢,会严重影响并发; try { pair.getFirst().process(ctx, rpcMessage); } catch (Throwable th) { LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th); } else { LOGGER.error("This message type [{}] has no processor.", messageTypeAware.getTypeCode()); } else { LOGGER.error("This rpcMessage body[{}] is not MessageTypeAware type.", body); }三、总结和后续本文我们聊了Seata Server启动时都做了哪些事?博主总结一共八件事:对配置文件(包括registry.conf、file.conf)做参数解析;初始化监控,做metric指标采集;创建TC与RM/TM通信的RPC服务器(NettyRemotingServer)--netty;初始化UUID生成器(雪花算法),用于生成全局事务id和分支事务id;设置事务会话(SessionHolder)、全局锁(LockManager)的持久化方式并初始化,有三种类型可选:file/db/redis;创建并初始化事务协调器(DefaultCoordinator),后台启动一堆线程做定时任务,并将DefaultCoordinator绑定到RPC服务器上做为transactionMessageHandler;注册ServerRunner销毁(Spring容器销毁)的回调钩子函数DefaultCoordinator;启动netty Server,用于接收TM/RM的请求;下一篇文章我们聊一下Seata Client(AT模式下仅作为RM时)启动时都做了什么?
一、前言至此,博主介绍了一些Seata环境搭建的常见坑、Seata的两种案例(SpringCloud集成Seata、SpringCloud集成Nacos + Seata):can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】从本文开始,进入到seata源码解析系列文章;前面几篇将即AT模式为例解析Seata-Server 和 Seata-Client的交互运作流程;在真正开始解析源码之前,我们先在IDEA中把Seata Server跑起来;PS:前文中搭建的Seata案例,seata的版本为1.3.0,而本文开始的源码分析将基于当前(2022年8月)最新的版本1.5.2进行源码解析。 读者请根据: 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版) 一文自行调整SpringBoot、SpringCloud、Spring Cloud Alibaba、Seata的版本(参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系),具体使用的版本见下一篇文章。二、IDEA中运行Seata Server先问自己个问题:为什么要在本地运行seata server?就博文个人感觉:seata server里面的类嵌套特别深,有时会感觉没那么必要;有一些细节点需要通过debug的方式验证推测;实践出真知!!!本着最小依赖、验证核心功能的初衷,本文Seata Server的本地运行基于File的配置/注册方式、DB的存储数据方式;1、把源码从Github中荡下来源码GitHub地址:https://github.com/seata/seata/tree/v1.5.2;通过New Project from Version Control...的方式将源码项目拉倒本地;选择Git代码分支:origin/1.5.2,整体项目结构如下:Seata Server使用到的DB进入当前源码项目的如下目录: 在本地MySQL数据库新建数据库seata_server,然后在其中运行mysql.sql文件,生成的表结构如下:表的作用如下:branch_table用于保存分支事务数据global_table用于保存全局事务数据lock_table用于保存全局锁数据distributed_lock用于保存分布式锁数据mysql.sql如下:-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid_and_branch_id` (`xid` , `branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);2、调整seata-server的配置下载并运行seata-server时,如果采用file的方式,我们需要在registry.conf中配置seata-server的服务注册信息、file.conf中配置seata-server的服务配置信息;按照这个思路,以file.conf为例全局搜索(command + shift + o):file.conf;可以看到查出了很多file.conf文件,但是很多都是在test目录下,肯定不是我们想要的;非test目录有两个file.conf文件,文件目录分别为:script/client/conf、config/seata-config/core/src/main/resources;又因为script/目录是一个使用seata-server时需要的一些信息目录,所以锁定config/seata-config/core/src/main/resources目录,然后对其做出必要修改,包括seata-server的数据存储方式采用DB。registy.conf同样操作;运行项目后发现,配置并没有生效,因为seata-server的数据存储方式依旧采用默认的file:seata-server进入到seata-server目录,可以发现这是一个SpringBoot项目,其中包括了一个application.example.yml文件;application.example.yml文件中部分内容如下:这个不就是在file.conf中配置的seata-server的数据存储方式吗!我们大胆推测IDEA中本地运行seata-server时,配置信息需要写在application.yml文件中;示例内容如下:server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata console: user: username: seata password: seata seata: server: service-port: 8091 #If not configured, the default is '${server.port} + 1000' max-commit-retry-timeout: -1 max-rollback-retry-timeout: -1 rollback-retry-timeout-unlock-enable: false enable-check-auth: true enable-parallel-request-handle: true retry-dead-threshold: 130000 xaer-nota-retry-timeout: 60000 recovery: handle-all-session-period: 1000 undo: log-save-days: 7 log-delete-period: 86400000 session: branch-async-queue-size: 5000 #branch async remove queue size enable-branch-async-remove: false #enable to asynchronous remove branchSession config: # support: nacos 、 consul 、 apollo 、 zk 、 etcd3 type: file registry: type: file security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login store: # support: file 、 db 、 redis mode: db session: mode: db lock: mode: db file: dir: sessionStore max-branch-session-size: 16384 max-global-session-size: 512 file-write-buffer-cache-size: 16384 session-reload-read-size: 100 flush-disk-mode: async datasource: druid db-type: mysql driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata_server?rewriteBatchedStatements=true user: root password: 123456 min-conn: 5 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 100 max-wait: 5000 redis: mode: single database: 0 min-conn: 1 max-conn: 10 password: max-total: 100 query-limit: 100 single: host: 127.0.0.1 port: 6379 sentinel: master-name: sentinel-hosts: metrics: enabled: false registry-type: compact exporter-list: prometheus exporter-prometheus-port: 9898 transport: rpc-tc-request-timeout: 30000 enable-tc-server-batch-send-response: false shutdown: wait: 3 thread-factory: boss-thread-prefix: NettyBoss worker-thread-prefix: NettyServerNIOWorker boss-thread-size: 1这里有一点比较有意思:如果我们没有配置seata-server通信(netty)的端口号,则默认为${server.port} + 1000,当然如果我们不配置,由于server.port默认是7091,所以seata-server通信(netty)的端口号默认为8091。3、运行seata-serverseata-server项目是一个正常的SpringBoot项目,就直接按照运行SpringBoot的方式运行其即可;在Server类中打个端点,验证我们对seata-server的配置是否生效;运行结果如我们预期,seata-server的数据模式修改为了db;三、总结和后续IDEA中运行seata-server等价于是在本地运行一个SringBoot项目,进需把需要的配置配置在application.yml文件中即可;后续正式开始seata的源码解析。
@[TOC]一、前言至此,微服务系列正式开启分布式事务篇;捎带一提,seata官方给的案例是真的******,版本之间的差异并未说明,据悉官方案例属于政治任务!在开启案例之前,博主和网友们踩过一些坑,具体见文章:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;版本信息如下:<properties> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <mysql.version>8.0.22</mysql.version> </properties>二、Seata简介Seata 是一款开源的分布式事务解决方案,全称:Simple extensiable autonomous transaction architecture;意思是:简单的、可扩展的、自治的事务架构。Seata致力于提供高性能和简单易用的分布式事务服务;Seata 为用户提供了 AT、TCC、SAGA 和 XA 四种分布式事务模式;1> AT模式:提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB;2> TCC 模式:支持 TCC 模式并可与 AT 混用,灵活度更高;3> SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式(开发中);4> XA 模式:支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB;官方文档:[https://seata.io/zh-cn/docs/overview/what-is-seata.html](https://seata.io/zh-cn/docs/overview/what-is-seata.html)三个角色1> TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。2> TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。3> RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。案例中三个角色的交互三、SpringCloud 集成Seata(注册和配置均采用Nacos)本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:仓储服务(stock-service):对给定的商品扣除仓储数量。订单服务(order-service):根据采购需求创建订单。帐户服务(account-service):从用户帐户中扣除余额。此外,trade-center为交易中心,是处理用户请求的入口;0、业务架构图1、MySQL数据库信息必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。0)一键执行所有SQL1> 案例中seata-client相关的所有业务库、业务表、undo_log表创建SQL;2> seata-server保存数据的表;#Account DROP SCHEMA IF EXISTS seata_account; CREATE SCHEMA seata_account; USE seata_account; CREATE TABLE `account_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO account_tbl (id, user_id, money) VALUES (1, '1001', 10000); INSERT INTO account_tbl (id, user_id, money) VALUES (2, '1002', 10000); 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; #Order DROP SCHEMA IF EXISTS seata_order; CREATE SCHEMA seata_order; USE seata_order; CREATE TABLE `order_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', `money` INT(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; #Stock DROP SCHEMA IF EXISTS seata_stock; CREATE SCHEMA seata_stock; USE seata_stock; CREATE TABLE `stock_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO stock_tbl (id, commodity_code, count) VALUES (1, '2001', 1000); 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- the table to store GlobalSession data DROP SCHEMA IF EXISTS seata_server; CREATE SCHEMA seata_server; USE seata_server; CREATE TABLE IF NOT EXISTS `global_table` `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;1)undo_log 事务回滚日志表SEATA AT 模式需要 undo_log 表,用于事务回滚使用。所以上面三个服务每个服务都要有一个undo_log表。表结构如下:建表SQL:CREATE TABLE `undo_log` `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;undo_log表的结构是从哪里找的?为什么它是这个?看了一些文章并没有说这个,本文简要说明一下;undo_log表结构从哪里找?1> 在GitHub中找到seata的源码,选择响应版本的代码分支:源码地址:https://github.com/seata/seata/tree/1.3.0注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。2> 进入目录/script/client/at/db,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:2)仓储服务(stock-service)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_stock; CREATE SCHEMA seata_stock; USE seata_stock; CREATE TABLE `stock_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO stock_tbl (id, commodity_code, count) VALUES (1, '2001', 1000);3)订单服务(order-service)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_order; CREATE SCHEMA seata_order; USE seata_order; CREATE TABLE `order_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', `money` INT(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;4)账户服务(account)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_account; CREATE SCHEMA seata_account; USE seata_account; CREATE TABLE `account_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO account_tbl (id, user_id, money) VALUES (1, '1001', 10000); INSERT INTO account_tbl (id, user_id, money) VALUES (2, '1002', 10000);5)seata-server表结构当seata-server配置信息中 store配置的是db时,需要使用到三张表:global_table(记录全局事务)、branch_table(记录分支事务)、lock_table(记录全局锁);当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。1> global_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `global_table` `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;2> branch_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `branch_table` `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;3> lock_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `lock_table` `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;seata-server相关表结构从哪里找?1> 在GitHub中找到seata的源码,选择响应版本的代码分支:源码地址:https://github.com/seata/seata/tree/1.3.0注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。2> 进入目录/script/server/db,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?2、seata-serverseata-server下载地址:https://seata.io/zh-cn/blog/download.html,其中binary选项为seata-server可执行程序,source为相应版本源码。1)seata-server配置将下载下来的seata-server-1.3.0.tar.gz压缩包解压,解压后的文件目录为:seata-server-1.3.0;# 进入seata-server主目录 cd seata-server-1.3.0 # 进入seata-server配置目录 cd conf修改registry.conf和file.conf配置文件,内容如下:1> registry.confseata-server的配置中心和注册中心均采用nacos的方式,设置type为nacos、设置serverAddr为nacos节点地址,serverAddr不能带‘http://’前缀;特别注意:registry.nacos.cluster(注册到的nacos集群名称)要和事务分组名称一致,即:下面config.txt文件中service.vgroupMapping.saint-trade-tx-group对应的值seata-server-sh;registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "seata-server-sh" config { # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" 为什么要用service.vgroupMapping.saint-trade-tx-group对应的值作为seata-server注册到nacos注册中心的集群名?==因为采用nacos作为注册中心时,seata-client是通过服务名(seata-server) + 集群名(seata-server-sh)+ 分组(SEATA_GROUP)去nacos注册中心中找到所有的seata-server实例地址;==如果不设置相应的集群名,则在从Nacos获取服务实例列表时会报错:i.s.c.r.netty.NettyClientChannelManager : no available service 'seata-server-sh' found, please make sure registry config correct这里涉及到Nacos的源码,感兴趣的可以看博主的另一篇文章:图文详述Nacos服务发现源码分析。2> seata-server相关配置上传到Nacos配置中心方法一:(1) 在GitHub中找到seata的源码,选择响应版本的代码分支:源码地址:https://github.com/seata/seata/tree/1.3.0注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。(2)进入/script/config-center目录,其中config.txt是上传到配置中心的配置所在的文件,nacos目录中是将nacos上传到Nacos配置中心的脚本(nacos-config.sh、nacos-config.py):(3)将上面的config.txt文件复制到seata-server的根目录,将nacos-config.sh复制到seat-server的bin目录下:注意:config.txt文件必须要在nacos-config.sh的上一级目录(../)因为在nacos-config.sh脚本中,是通过cat $(dirname "$PWD")的方式去查找config.txt所在是目录;而cat $(dirname "$PWD")返回的正是nacos-config.sh脚本所在目录的上级目录;(4)修改config.txt文件:重点关注以下配置:事务分组名称的配置、事务分组名称对应的Seata Server服务实例列表;注意,高版本的vgroupMapping后面的如: saint-trade-tx-group 不能定义为 saint_trade_tx_groupSeata Server数据存放的模式,如果是模式db,配置db信息;transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableClientBatchSendRequest=false transport.threadFactory.bossThreadPrefix=NettyBoss transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler transport.threadFactory.shareBossWorker=false transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector transport.threadFactory.clientSelectorThreadSize=1 transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread transport.threadFactory.bossThreadSize=1 transport.threadFactory.workerThreadSize=default transport.shutdown.wait=3 service.vgroupMapping.saint-trade-tx-group=seata-server-sh service.seata-server-sh.grouplist=127.0.0.1:8091 service.enableDegrade=false service.disableGlobalTransaction=false client.rm.asyncCommitBufferLimit=10000 client.rm.lock.retryInterval=10 client.rm.lock.retryTimes=30 client.rm.lock.retryPolicyBranchRollbackOnConflict=true client.rm.reportRetryCount=5 client.rm.tableMetaCheckEnable=false client.rm.sqlParserType=druid client.rm.reportSuccessEnable=false client.rm.sagaBranchRegisterEnable=false client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true store.db.user=root store.db.password=123456 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false client.undo.dataValidation=true client.undo.logSerialization=jackson client.undo.onlyCareUpdateColumns=true server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 client.undo.logTable=undo_log client.log.exceptionRate=100 transport.serialization=seata transport.compressor=none metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898(5)执行nacos-config.sh脚本将配置上传到nacos配置中心:方法二:通过dataId配置从seata的v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,只需要额外添加一个dataId配置项。首先在nacos新建配置,此处dataId为seataServer.properties,配置内容参考https://github.com/seata/seata/tree/develop/script/config-center 的config.txt并按需修改(见方法一的修改)保存;在client参考如下配置进行修改:seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 group : "SEATA_GROUP" namespace: "" dataId: "seataServer.properties" username: "nacos" password: "nacos"2)启动seata-server进入seata-server-1.3.0/bin目录,然后运行seata-server.sh shell脚本;cd ../bin sh seata-server.shseata-server.sh脚本中的参数具体参考官方文档:https://github.com/seata/seata/tree/develop/script/config-center;v1.5.0开始支持的 Nacos相关启动脚本参数介绍:3、seata-client一般Spring Cloud集成seata大致会分为5步:1> 第一步:添加Spring Cloud Alibaba 依赖管理工具和 Seata 依赖;<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>另外,spring-cloud-starter-alibaba-seata依赖中seata相关的只依赖了spring-cloud-alibaba-seata,所以在项目中添加spring-cloud-starter-alibaba-seata 和spring-cloud-alibaba-seata是一样的;2> 第二步:在application.yml配置文件中设置Seata相关配置,(不再需要file.conf文件,registry.conf也不需要)3> 第三步:注入数据源;Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入 io.seata.rm.datasource.DataSourceProxy, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory;4> 第四步:在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据;5> 第五步:在业务的发起方的方法上使用@GlobalTransactional开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;0)最上层父项目spring-cloud-center的pom.xml文件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modules> <module>trade-center</module> <module>stock-service</module> <module>order-service</module> <module>account-service</module> </modules> <groupId>com.saint</groupId> <artifactId>transaction-seata</artifactId> <version>0.0.1-SNAPSHOT</version> <name>transaction-seata</name> <description>transaction-seata</description> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <druid.version>1.2.8</druid.version> <mysql.version>8.0.22</mysql.version> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.10</version> </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> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。1)account-serviceaccount-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>account-service</artifactId> <name>account-service</name> <dependencies> <!--protostuff序列化依赖--> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigpackage com.saint.account.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; * 数据源配置 * @author Saint @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 * @param druidDataSource The DruidDataSource * @return The default datasource @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); }3> AccountControllerpackage com.saint.account.controller; import com.saint.account.service.AccountService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class AccountController { private final AccountService accountService; @RequestMapping("/debit") public Boolean debit(String userId, BigDecimal money) { accountService.debit(userId, money); return true; }4> Accountpackage com.saint.account.entity; import lombok.Data; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.math.BigDecimal; * @author Saint @Entity @Table(name = "account_tbl") @DynamicUpdate @DynamicInsert @Data public class Account { private Long id; private String userId; private BigDecimal money; }5> AccountDAOpackage com.saint.account.repository; import com.saint.account.entity.Account; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface AccountDAO extends JpaRepository<Account, Long> { Account findByUserId(String userId); }6> AccountServicepackage com.saint.account.service; import com.saint.account.entity.Account; import com.saint.account.repository.AccountDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class AccountService { private final AccountDAO accountDAO; private static final String ERROR_USER_ID = "1002"; @Transactional(rollbackFor = Exception.class) public void debit(String userId, BigDecimal num) { Account account = accountDAO.findByUserId(userId); account.setMoney(account.getMoney().subtract(num)); accountDAO.save(account); if (ERROR_USER_ID.equals(userId)) { throw new RuntimeException("account branch exception"); 7> AccountApplicationpackage com.saint.account; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableFeignClients @EnableJpaRepositories public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); }8> application.ymlserver: port: 9031 spring: application: name: account-service cloud: nacos: # 这里配置的是当前服务所要注册到的Nacos地址 discovery: server-addr: 127.0.0.1:8848 group: TRADE_GROUP datasource: url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group # 采用nacos作为配置中心 config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表) registry: type: nacos nacos: # seata-server的应用名称 application: seata-server # seata-server注册到的nacos服务地址 server-addr: 127.0.0.1:8848 group: SEATA_GROUP2)order-serviceorder-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;application.yml配置文件;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>order-service</artifactId> <name>order-service</name> <dependencies> <!--protostuff序列化依赖--> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--负载均衡器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigJPA的数据源配置和account-service中的一样;3> OrderControllerpackage com.saint.order.controller; import com.saint.order.service.OrderService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class OrderController { private final OrderService orderService; @GetMapping("/create") public Boolean create(String userId, String commodityCode, Integer count) { orderService.create(userId, commodityCode, count); return true; }4> Orderpackage com.saint.order.entity; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; import java.math.BigDecimal; * @author Saint @Entity @Table(name = "order_tbl") @DynamicUpdate @DynamicInsert @NoArgsConstructor @Data public class Order { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id") private String userId; @Column(name = "commodity_code") private String commodityCode; @Column(name = "money") private BigDecimal money; @Column(name = "count") private Integer count; }5> AccountFeignClientAccountFeignClient用于通过OpenFeign调用account-service;package com.saint.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; * @author Saint @FeignClient(name = "account-service") public interface AccountFeignClient { @GetMapping("/debit") Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money); }6> OrderDAOpackage com.saint.order.repository; import com.saint.order.entity.Order; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface OrderDAO extends JpaRepository<Order, Long> { }7> OrderServicepackage com.saint.order.service; import com.saint.order.entity.Order; import com.saint.order.feign.AccountFeignClient; import com.saint.order.repository.OrderDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class OrderService { private final AccountFeignClient accountFeignClient; private final OrderDAO orderDAO; @Transactional public void create(String userId, String commodityCode, Integer count) { BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5)); Order order = new Order(); order.setUserId(userId); order.setCommodityCode(commodityCode); order.setCount(count); order.setMoney(orderMoney); orderDAO.save(order); accountFeignClient.debit(userId, orderMoney); }8> OrderApplicationpackage com.saint.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableFeignClients @EnableJpaRepositories public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); }9> application.ymlserver: port: 9021 spring: application: name: order-service cloud: nacos: # 这里配置的是当前服务所要注册到的Nacos地址 discovery: server-addr: 127.0.0.1:8848 group: TRADE_GROUP datasource: url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group # 采用nacos作为配置中心 config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表) registry: type: nacos nacos: # seata-server的应用名称 application: seata-server # seata-server注册到的nacos服务地址 server-addr: 127.0.0.1:8848 group: SEATA_GROUP3)stock-servicestck-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>stock-service</artifactId> <name>stock-service</name> <dependencies> <!--protostuff序列化依赖--> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigJPA的数据源配置和account-service中的一样;3> StockControllerpackage com.saint.stock.controller; import com.saint.stock.service.StockService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class StockController { private final StockService stockService; @GetMapping(path = "/deduct") public Boolean deduct(String commodityCode, Integer count) { stockService.deduct(commodityCode, count); return true; }4> Stockpackage com.saint.stock.entity; import lombok.Data; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; * @author Saint @Entity @Table(name = "stock_tbl") @DynamicUpdate @DynamicInsert @Data public class Stock { private Long id; private String commodityCode; private Integer count; }5> StockDAOpackage com.saint.stock.repository; import com.saint.stock.entity.Stock; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface StockDAO extends JpaRepository<Stock, String> { Stock findByCommodityCode(String commodityCode); }6> StockServicepackage com.saint.stock.service; import com.saint.stock.entity.Stock; import com.saint.stock.repository.StockDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class StockService { private final StockDAO stockDAO; @Transactional public void deduct(String commodityCode, int count) { Stock stock = stockDAO.findByCommodityCode(commodityCode); stock.setCount(stock.getCount() - count); stockDAO.save(stock); }7> StockApplicationpackage com.saint.stock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableJpaRepositories public class StockApplication { public static void main(String[] args) { SpringApplication.run(StockApplication.class, args); }8> application.ymlserver: port: 9011 spring: application: name: stock-service cloud: nacos: # 这里配置的是当前服务所要注册到的Nacos地址 discovery: server-addr: 127.0.0.1:8848 group: TRADE_GROUP datasource: url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group # 采用nacos作为配置中心 config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表) registry: type: nacos nacos: # seata-server的应用名称 application: seata-server # seata-server注册到的nacos服务地址 server-addr: 127.0.0.1:8848 group: SEATA_GROUP4)trade-centertrade-center整体代码结构目录如下:整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;application.yml配置文件;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>trade-center</artifactId> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <name>trade-center</name> <dependencies> <!--protostuff序列化依赖--> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--负载均衡器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> <!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>2> TradeControllerpackage com.saint.trade.controller; import com.saint.trade.service.TradeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController public class TradeController { @Autowired private TradeService businessService; * 购买下单,模拟全局事务提交 * @return @RequestMapping("/purchase/commit") public Boolean purchaseCommit() { businessService.purchase("1001", "2001", 1); return true; * 购买下单,模拟全局事务回滚 * @return @RequestMapping("/purchase/rollback") public Boolean purchaseRollback() { try { businessService.purchase("1002", "2001", 1); // businessService.failToPurchase("1001", "2001", 1); } catch (Exception e) { e.printStackTrace(); return false; return true; }3> OrderFeignClienttrade-center通过OpenFeign调用order-service;package com.saint.trade.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; * @author Saint @FeignClient(name = "order-service") public interface OrderFeignClient { @GetMapping("/create") void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count); }4> StockFeignClienttrade-center通过OpenFeign调用stock-service;package com.saint.trade.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; * @author Saint @FeignClient(name = "stock-service") public interface StockFeignClient { @GetMapping("/deduct") void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count); }5> TradeServiceTradeService中通过@GlobalTransactional开启分布式事务;package com.saint.trade.service; import com.saint.trade.feign.OrderFeignClient; import com.saint.trade.feign.StockFeignClient; import io.seata.spring.annotation.GlobalTransactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; * @author Saint @Service @RequiredArgsConstructor public class TradeService { private final StockFeignClient stockFeignClient; private final OrderFeignClient orderFeignClient; * 减库存,下订单 * @param userId * @param commodityCode * @param orderCount @GlobalTransactional public void purchase(String userId, String commodityCode, int orderCount) { stockFeignClient.deduct(commodityCode, orderCount); orderFeignClient.create(userId, commodityCode, orderCount); * 减库存,下订单(有异常) * @param userId * @param commodityCode * @param orderCount @GlobalTransactional public void failToPurchase(String userId, String commodityCode, int orderCount) { stockFeignClient.deduct(commodityCode, orderCount); orderFeignClient.create(userId, commodityCode, orderCount); throw new RuntimeException("Error!"); }6> TradeApplicationpackage com.saint.trade; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; * @author Saint @SpringBootApplication @EnableFeignClients public class TradeApplication { public static void main(String[] args) { SpringApplication.run(TradeApplication.class, args); }7> application.ymlserver: port: 9001 spring: application: name: trade-center cloud: nacos: # 这里配置的是当前服务所要注册到的Nacos地址 discovery: server-addr: 127.0.0.1:8848 group: TRADE_GROUP # cloud: # alibaba: # seata: # tx-service-group: saint-trade-tx-group seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group # 采用nacos作为配置中心 config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表) registry: type: nacos nacos: # seata-server的应用名称 application: seata-server # seata-server注册到的nacos服务地址 server-addr: 127.0.0.1:8848 group: SEATA_GROUP4、AT模式分布式事务效果演示分别启动trade-center、stock-service、order-service、account-service;都启动成功之后,Nacos服务信息如下:四个服务的分组名称均为我们通过spring.cloud.nacos.discovery.group属性配置的;1)请求正常分布式事务成功,模拟正常下单、扣库存请求访问:http://127.0.0.1:9001/purchase/commit2)请求异常分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚请求访问:http://127.0.0.1:9001/purchase/rollback;四、总结和后续当前文章讲述了Spring Cloud + Nacos + Seata + OpenFeign + JPA实现分布式事务的案例。如果你运行不起来,私信我。官方案例文档:https://github.com/Saint9768/seata-samples/tree/master/springcloud-nacos-seata;明人不说暗话,里面全是坑,基本是搭不起来的;下一篇文章正式开启Seata源码分析,期间会穿插聊一下Seata的TCC模式实现案例;
@[TOC]一、前言至此,微服务系列正式开启分布式事务篇;捎带一提,seata官方给的案例是真的******,版本之间的差异并未说明,据悉官方案例属于政治任务!在开启案例之前,博主和网友们踩过一些坑,具体见文章:can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;版本信息如下:<properties> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <mysql.version>8.0.22</mysql.version> </properties>二、Seata简介Seata 是一款开源的分布式事务解决方案,全称:Simple extensiable autonomous transaction architecture;意思是:简单的、可扩展的、自治的事务架构。Seata致力于提供高性能和简单易用的分布式事务服务;Seata 为用户提供了 AT、TCC、SAGA 和 XA 四种分布式事务模式;1> AT模式:提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB;2> TCC 模式:支持 TCC 模式并可与 AT 混用,灵活度更高;3> SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式(开发中);4> XA 模式:支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB;官方文档:[https://seata.io/zh-cn/docs/overview/what-is-seata.html](https://seata.io/zh-cn/docs/overview/what-is-seata.html)三个角色1> TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。2> TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。3> RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。案例中三个角色的交互三、SpringCloud 集成Seata(注册和配置均采用file方式)本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:仓储服务(stock-service):对给定的商品扣除仓储数量。订单服务(order-service):根据采购需求创建订单。帐户服务(account-service):从用户帐户中扣除余额。此外,trade-center为交易中心,是处理用户请求的入口;0、业务架构图1、MySQL数据库信息必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。0)一键执行所有SQL1> 案例中seata-client相关的所有业务库、业务表、undo_log表创建SQL;2> seata-server保存数据的表;#Account DROP SCHEMA IF EXISTS seata_account; CREATE SCHEMA seata_account; USE seata_account; CREATE TABLE `account_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO account_tbl (id, user_id, money) VALUES (1, '1001', 10000); INSERT INTO account_tbl (id, user_id, money) VALUES (2, '1002', 10000); 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; #Order DROP SCHEMA IF EXISTS seata_order; CREATE SCHEMA seata_order; USE seata_order; CREATE TABLE `order_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', `money` INT(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; #Stock DROP SCHEMA IF EXISTS seata_stock; CREATE SCHEMA seata_stock; USE seata_stock; CREATE TABLE `stock_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO stock_tbl (id, commodity_code, count) VALUES (1, '2001', 1000); 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- the table to store GlobalSession data DROP SCHEMA IF EXISTS seata_server; CREATE SCHEMA seata_server; USE seata_server; CREATE TABLE IF NOT EXISTS `global_table` `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;1)undo_log 事务回滚日志表SEATA AT 模式需要 undo_log 表,用于事务回滚使用。所以上面三个服务每个服务都要有一个undo_log表。表结构如下:建表SQL:CREATE TABLE `undo_log` `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;undo_log表的结构是从哪里找的?为什么它是这个?看了一些文章并没有说这个,本文简要说明一下;undo_log表结构从哪里找?1> 在GitHub中找到seata的源码,选择响应版本的代码分支:源码地址:https://github.com/seata/seata/tree/1.3.0注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。2> 进入目录/script/client/at/db,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:2)仓储服务(stock-service)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_stock; CREATE SCHEMA seata_stock; USE seata_stock; CREATE TABLE `stock_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO stock_tbl (id, commodity_code, count) VALUES (1, '2001', 1000);3)订单服务(order-service)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_order; CREATE SCHEMA seata_order; USE seata_order; CREATE TABLE `order_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `commodity_code` VARCHAR(255) DEFAULT NULL, `count` INT(11) DEFAULT '0', `money` INT(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;4)账户服务(account)业务表1> 表结构:2> 建表SQL:DROP SCHEMA IF EXISTS seata_account; CREATE SCHEMA seata_account; USE seata_account; CREATE TABLE `account_tbl` `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO account_tbl (id, user_id, money) VALUES (1, '1001', 10000); INSERT INTO account_tbl (id, user_id, money) VALUES (2, '1002', 10000);5)seata-server表结构当seata-server配置信息中 store配置的是db时,需要使用到三张表:global_table(记录全局事务)、branch_table(记录分支事务)、lock_table(记录全局锁);当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。1> global_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `global_table` `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;2> branch_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `branch_table` `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;3> lock_table1> 表结构:2> 建表SQL:CREATE TABLE IF NOT EXISTS `lock_table` `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;seata-server相关表结构从哪里找?1> 在GitHub中找到seata的源码,选择响应版本的代码分支:源码地址:https://github.com/seata/seata/tree/1.3.0注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。2> 进入目录/script/server/db,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?2、seata-serverseata-server下载地址:https://seata.io/zh-cn/blog/download.html,其中binary选项为seata-server可执行程序,source为相应版本源码。1)seata-server配置将下载下来的seata-server-1.3.0.tar.gz压缩包解压,解压后的文件目录为:seata-server-1.3.0;# 进入seata-server主目录 cd seata-server-1.3.0 # 进入seata-server配置目录 cd conf修改registry.conf和file.conf配置文件,内容如下:1> registry.confseata-server的配置中心和注册中心均采用file的方式:registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" 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 = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" consul { serverAddr = "127.0.0.1:8500" apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" etcd3 { serverAddr = "http://localhost:2379" file { name = "file.conf" 2> file.confseata-server的配置中心采用file时,具体配置如下: ## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "db" ## database store property ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata_server" user = "root" password = "123456" minConn = 5 maxConn = 30 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true # the client batch send request enable enableClientBatchSendRequest = false #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThreadPrefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT bossThreadSize = 1 #auto default pin or 8 workerThreadSize = "default" shutdown { # when destroy server, wait seconds wait = 3 serialization = "seata" compressor = "none" transaction { undo.data.validation = true undo.log.serialization = "jackson" ## metrics configuration, only used in server side metrics { enabled = false registryType = "compact" # multi exporters use comma divided exporterList = "prometheus" exporterPrometheusPort = 9898 }2)启动seata-server进入seata-server-1.3.0/bin目录,然后运行seata-server.sh shell脚本;cd ../bin sh seata-server.shseata-server.sh脚本中的参数官方文档介绍:https://github.com/seata/seata/tree/develop/script/config-center;采用File配置方式时不需要关注,当使用其他配置中心时再关注即可(留坑集成Nacos时处理)。3、seata-client一般Spring Cloud集成seata大致会分为5步:1> 第一步:添加Spring Cloud Alibaba 依赖管理工具和 Seata 依赖;<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>另外,spring-cloud-starter-alibaba-seata依赖中seata相关的只依赖了spring-cloud-alibaba-seata,所以在项目中添加spring-cloud-starter-alibaba-seata 和spring-cloud-alibaba-seata是一样的;2> 第二步:添加Seata配置文件,包括:registry.conf、file.conf(如果使用file作为配置中心),其中:registry.conf用于指定 TC 的注册中心和配置文件,默认都是 file; 如果使用其他的注册中心,要求 Seata-Server 也注册到该配置中心上;file.conf用于指定TC的相关属性;如果使用注册中心也可以将配置添加到配置中心;3> 第三步:注入数据源;Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入 io.seata.rm.datasource.DataSourceProxy, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory;4> 第四步:在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据;5> 第五步:在业务的发起方的方法上使用@GlobalTransactional开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;0、最上层父项目spring-cloud-center的pom.xml文件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modules> <module>trade-center</module> <module>stock-service</module> <module>order-service</module> <module>account-service</module> </modules> <groupId>com.saint</groupId> <artifactId>transaction-seata</artifactId> <version>0.0.1-SNAPSHOT</version> <name>transaction-seata</name> <description>transaction-seata</description> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.4.2</spring-boot.version> <spring-cloud.version>2020.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version> <druid.version>1.2.8</druid.version> <mysql.version>8.0.22</mysql.version> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.10</version> </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> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。1)account-serviceaccount-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>account-service</artifactId> <name>account-service</name> <dependencies> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigpackage com.saint.account.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; * 数据源配置 * @author Saint @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 * @param druidDataSource The DruidDataSource * @return The default datasource @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); }3> AccountControllerpackage com.saint.account.controller; import com.saint.account.service.AccountService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class AccountController { private final AccountService accountService; @RequestMapping("/debit") public Boolean debit(String userId, BigDecimal money) { accountService.debit(userId, money); return true; }4> Accountpackage com.saint.account.entity; import lombok.Data; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.math.BigDecimal; * @author Saint @Entity @Table(name = "account_tbl") @DynamicUpdate @DynamicInsert @Data public class Account { private Long id; private String userId; private BigDecimal money; }5> AccountDAOpackage com.saint.account.repository; import com.saint.account.entity.Account; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface AccountDAO extends JpaRepository<Account, Long> { Account findByUserId(String userId); }6> AccountServicepackage com.saint.account.service; import com.saint.account.entity.Account; import com.saint.account.repository.AccountDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class AccountService { private final AccountDAO accountDAO; private static final String ERROR_USER_ID = "1002"; @Transactional(rollbackFor = Exception.class) public void debit(String userId, BigDecimal num) { Account account = accountDAO.findByUserId(userId); account.setMoney(account.getMoney().subtract(num)); accountDAO.save(account); if (ERROR_USER_ID.equals(userId)) { throw new RuntimeException("account branch exception"); 7> AccountApplicationpackage com.saint.account; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableFeignClients @EnableJpaRepositories public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); }8> application.ymlserver: port: 9031 spring: application: name: account-service datasource: url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group9> file.conftransport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true # the client batch send request enable enableClientBatchSendRequest = true #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT bossThreadSize = 1 #auto default pin or 8 workerThreadSize = "default" shutdown { # when destroy server, wait seconds wait = 3 serialization = "seata" compressor = "none" service { #transaction service group mapping vgroupMapping.saint-trade-tx-group = "seata-server-sh" #only support when registry.type=file, please don't set multiple addresses seata-server-sh.grouplist = "127.0.0.1:8091" #degrade, current not support enableDegrade = false #disable seata disableGlobalTransaction = false client { asyncCommitBufferLimit = 10000 lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true reportRetryCount = 5 tableMetaCheckEnable = false reportSuccessEnable = false commitRetryCount = 5 rollbackRetryCount = 5 undo { dataValidation = true logSerialization = "jackson" logTable = "undo_log" log { exceptionRate = 100 }10> registry.confregistry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" nacos { application = "seata-server" serverAddr = "127.0.0.1" namespace = "" username = "" password = "" file { name = "file.conf" config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig type = "file" nacos { serverAddr = "127.0.0.1" namespace = "" group = "SEATA_GROUP" username = "" password = "" file { name = "file.conf" }2)order-serviceorder-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>order-service</artifactId> <name>order-service</name> <dependencies> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigJPA的数据源配置和account-service中的一样;3> OrderControllerpackage com.saint.order.controller; import com.saint.order.service.OrderService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class OrderController { private final OrderService orderService; @GetMapping("/create") public Boolean create(String userId, String commodityCode, Integer count) { orderService.create(userId, commodityCode, count); return true; }4> Orderpackage com.saint.order.entity; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.*; import java.math.BigDecimal; * @author Saint @Entity @Table(name = "order_tbl") @DynamicUpdate @DynamicInsert @NoArgsConstructor @Data public class Order { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id") private String userId; @Column(name = "commodity_code") private String commodityCode; @Column(name = "money") private BigDecimal money; @Column(name = "count") private Integer count; }5> AccountFeignClientAccountFeignClient用于通过OpenFeign调用account-service;package com.saint.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; * @author Saint @FeignClient(name = "account-service", url = "127.0.0.1:9031") public interface AccountFeignClient { @GetMapping("/debit") Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money); }6> OrderDAOpackage com.saint.order.repository; import com.saint.order.entity.Order; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface OrderDAO extends JpaRepository<Order, Long> { }7> OrderServicepackage com.saint.order.service; import com.saint.order.entity.Order; import com.saint.order.feign.AccountFeignClient; import com.saint.order.repository.OrderDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class OrderService { private final AccountFeignClient accountFeignClient; private final OrderDAO orderDAO; @Transactional public void create(String userId, String commodityCode, Integer count) { BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5)); Order order = new Order(); order.setUserId(userId); order.setCommodityCode(commodityCode); order.setCount(count); order.setMoney(orderMoney); orderDAO.save(order); accountFeignClient.debit(userId, orderMoney); }8> OrderApplicationpackage com.saint.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableFeignClients @EnableJpaRepositories public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); }9> application.ymlserver: port: 9021 spring: application: name: order-service datasource: url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group10> file.conf 和 registry.conffile.conf 和 registry.conf与account-service的一样;3)stock-servicestck-service整体代码结构目录如下:整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <artifactId>stock-service</artifactId> <name>stock-service</name> <dependencies> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> </dependencies> </project>2> DataSourceConfigJPA的数据源配置和account-service中的一样;3> StockControllerpackage com.saint.stock.controller; import com.saint.stock.service.StockService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class StockController { private final StockService stockService; @GetMapping(path = "/deduct") public Boolean deduct(String commodityCode, Integer count) { stockService.deduct(commodityCode, count); return true; }4> Stockpackage com.saint.stock.entity; import lombok.Data; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; * @author Saint @Entity @Table(name = "stock_tbl") @DynamicUpdate @DynamicInsert @Data public class Stock { private Long id; private String commodityCode; private Integer count; }5> StockDAOpackage com.saint.stock.repository; import com.saint.stock.entity.Stock; import org.springframework.data.jpa.repository.JpaRepository; * @author Saint public interface StockDAO extends JpaRepository<Stock, String> { Stock findByCommodityCode(String commodityCode); }6> StockServicepackage com.saint.stock.service; import com.saint.stock.entity.Stock; import com.saint.stock.repository.StockDAO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; * @author Saint @Service @RequiredArgsConstructor(onConstructor = @_(@Autowired)) public class StockService { private final StockDAO stockDAO; @Transactional public void deduct(String commodityCode, int count) { Stock stock = stockDAO.findByCommodityCode(commodityCode); stock.setCount(stock.getCount() - count); stockDAO.save(stock); }7> StockApplicationpackage com.saint.stock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; * @author Saint @SpringBootApplication @EnableJpaRepositories public class StockApplication { public static void main(String[] args) { SpringApplication.run(StockApplication.class, args); }8> application.ymlserver: port: 9011 spring: application: name: stock-service datasource: url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver show-sql: true seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group9> file.conf 和 registry.conffile.conf 和 registry.conf和account-service一样;4)trade-centertrade-center整体代码结构目录如下:整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;1> pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>transaction-seata</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>trade-center</artifactId> <version>0.0.1-SNAPSHOT</version> <groupId>com.saint</groupId> <name>trade-center</name> <dependencies> <!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>2> TradeControllerpackage com.saint.trade.controller; import com.saint.trade.service.TradeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; * @author Saint @RestController public class TradeController { @Autowired private TradeService businessService; * 购买下单,模拟全局事务提交 * @return @RequestMapping("/purchase/commit") public Boolean purchaseCommit() { businessService.purchase("1001", "2001", 1); return true; * 购买下单,模拟全局事务回滚 * @return @RequestMapping("/purchase/rollback") public Boolean purchaseRollback() { try { businessService.purchase("1002", "2001", 1); // businessService.failToPurchase("1001", "2001", 1); } catch (Exception e) { e.printStackTrace(); return false; return true; }3> OrderFeignClienttrade-center通过OpenFeign调用order-service;package com.saint.trade.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; * @author Saint @FeignClient(name = "order-service", url = "127.0.0.1:9021") public interface OrderFeignClient { @GetMapping("/create") void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count); }4> StockFeignClienttrade-center通过OpenFeign调用stock-service;package com.saint.trade.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; * @author Saint @FeignClient(name = "stock-service", url = "127.0.0.1:9011") public interface StockFeignClient { @GetMapping("/deduct") void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count); }5> TradeServiceTradeService中通过@GlobalTransactional开启分布式事务;package com.saint.trade.service; import com.saint.trade.feign.OrderFeignClient; import com.saint.trade.feign.StockFeignClient; import io.seata.spring.annotation.GlobalTransactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; * @author Saint @Service @RequiredArgsConstructor public class TradeService { private final StockFeignClient stockFeignClient; private final OrderFeignClient orderFeignClient; * 减库存,下订单 * @param userId * @param commodityCode * @param orderCount @GlobalTransactional public void purchase(String userId, String commodityCode, int orderCount) { stockFeignClient.deduct(commodityCode, orderCount); orderFeignClient.create(userId, commodityCode, orderCount); * 减库存,下订单(有异常) * @param userId * @param commodityCode * @param orderCount @GlobalTransactional public void failToPurchase(String userId, String commodityCode, int orderCount) { stockFeignClient.deduct(commodityCode, orderCount); orderFeignClient.create(userId, commodityCode, orderCount); throw new RuntimeException("Error!"); }6> TradeApplicationpackage com.saint.trade; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; * @author Saint @SpringBootApplication @EnableFeignClients public class TradeApplication { public static void main(String[] args) { SpringApplication.run(TradeApplication.class, args); }7> application.ymlserver: port: 9001 spring: application: name: trade-center seata: # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致 # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置 tx-service-group: saint-trade-tx-group8> file.conf 和 registry.conffile.conf 和 registry.conf 与 account-service的一样;4、AT模式分布式事务效果演示分别启动trade-center、stock-service、order-service、account-service;1)请求正常分布式事务成功,模拟正常下单、扣库存请求访问:http://127.0.0.1:9001/purchase/commit2)请求异常分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚请求访问:http://127.0.0.1:9001/purchase/rollback;四、总结和后续当前文章讲述了Spring Cloud + JPA + OpenFeign + Seata实现分布式事务的案例。下一篇文章为:Spring Cloud 整合Seata + Nacos。
@[TOC]一、前言最近在公司遇到分布式事务嵌套子事务的问题,用的也是seata,于是就准备自己研究一下seata。在搭建项目的过程中,发现一直无法将服务注册到Seata服务中,报错如下:can not get cluster name in registry config 'service.vgroupMapping.my-tx-group', please make sure registry config correct从日志来看,每10s疯狂报错:我们下面从两种模式:file和nacos进行介绍;二、配置说明:seata-server的配置从file.conf文件中取。1、Seata-server配置register.conf中 配置的config(配置中心)为type="file"# 配置中心 # 如果type=file,则从本地file.conf中获取配置参数,并且只有这种情况才从file.conf中加载配置参数 config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" file { name = "file.conf" }再看file.conf:service { # 事务组名称 vgroup_mapping.my-tx-group = "seata-server" disableGlobalTransaction = false }注意:1、vgroup_mapping.my-tx-group = "seata-server"为事务组名称,这里的值需要和TC中配置的service.vgroup-mapping.my-tx-group一致; 2、my-tx-group为自定义名称,可以随便取,seata-server这个值也是一样; 3、事务组的命名不要用下划线'_',可以用'-'因为在seata的高版本中使用underline下划线 将导致service not to be found。2、seata-client配置application.yml# seata 配置 seata: # 使用的事务组 tx-service-group: my-tx-group enabled: true看一些大佬的方式是在resources/目录下增加file.conf配置事务组my-tx-group的值,如下所示:然后运行服务就崩了,大佬不会坑我吧,然后百思不得其解;于是便开始了百度、Google、再到StackOverflow的过程;最后在seata官方的issue 411中发现了一个类似的问题:https://github.com/seata/seata-samples/issues/411。不同的是issue中采用的是nacos作为配置中心,而我采用的是file。其中提到了一点,必须要在nacos配置一个service.vgroupMapping.my-tx-group=seata-server。要不我直接切成nacos做为配置中心吧,但是我的问题还没解决啊,作为一个有专研精神的群体,不能就此放弃呀。分析一下这个异常,can not get cluster name in registry config,在注册表配置中获取不到群集名称;其在resources/file.conf中配置了呀。我丢,nacos、consul等配置中心本质上只是把application.yml中的配置提到了云端;会不会是要在application.yml中配置的?去官方看看配置说明:https://seata.io/zh-cn/docs/user/configurations.html全局搜索 command(Ctrl) + F 搜索service.vgroupMapping我们再去application.yml中配置上试试看:# seata 配置 seata: # 使用哪个事务组 tx-service-group: my-tx-group service: # 事务组对应的集群民称 vgroup-mapping.my-tx-group: seata-server # seata-server的地址 grouplist.seata-server: 127.0.0.1:8091 enabled: true运行,内心OS(阿门,给我成):注册RM成功,再看看seata-server端日志:OK,搞定!三、采用Nacos作为配置中心在nacos中新增一个dataId为对应事务组的数据即可,可以参考上面提到的seata官方issue411:https://github.com/seata/seata-samples/issues/411。四、Seata的其他排坑、实战、源码解析文章can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?五、总结虽说三人行必有我师焉,但听完要实践验证加固记忆。比如这里我就犯了一个比较低级的错误,SpringBoot项目的配置怎么就从resources目录下的非application.yml、bootstrap.yml中拿了呢?当我们也不确定谁对谁错的时候,找源码入口,debug源码后答案自见分晓。我们这里的报错提示是在NettyClientChannelManager类中,我们就进入这个类找一下入口,怎么知道是哪个方法呢?从方法的命名我们来猜,猜了几次还不对,就打断点看。这里一共就这几个方法,从命名来看,经验告诉我们应该和Server信息相关,会不会是getAvailServerList()方法呢,试试看看吧,打断点hold住,debug启动:下面大家顺着往下走,会发现出问题的地方(这里记得把application.yml中的事务组信息注掉):希望我们都会用、善用这种方式解决问题。回想自己以前负责的一个服务中因为引入配置中心consul,导致项目中的定时任务全部失效,也是通过这种方式找到了问题所在。
@[TOC]一、报错详情1、环境信息Spring Boot : 2.4.2Spring Cloud:2020.0.1Spring Cloud Alibaba:2021.1Seata:1.3.0mysql-connector-java:8.0.28和报错相关的是Seata 和 mysql-connector-java的版本信息2、Seata undo_log日志表结构其中 log_created、log_modified字段的类型是datetime。3、详细异常信息在分布式全局事务执行失败进行全局回滚时,需要通过Seata 的 undoLog回滚的服务会出现报错信息,如下:2022-08-05 18:04:03.583 ERROR 16225 --- [h_RMROLE_1_8_16] i.s.r.d.u.parser.JacksonUndoLogParser : json decode exception, Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.1.5:8091:298880381343518720","branchId":298880382744416257,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"stock_tbl","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"stock_tbl","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.dataso"[truncated 12232 bytes]; line: 1, column: 3987] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[1]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[6]->io.seata.rm.datasource.sql.struct.Field["value"]) com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.1.5:8091:298880381343518720","branchId":298880382744416257,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"stock_tbl","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"stock_tbl","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.dataso"[truncated 12232 bytes]; line: 1, column: 3987] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[1]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[6]->io.seata.rm.datasource.sql.struct.Field["value"]) at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1615) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1077) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1332) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:199) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromAny(AsPropertyTypeDeserializer.java:195) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:710) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:292) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:318) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:292) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:318) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:292) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:318) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4526) ~[jackson-databind-2.11.4.jar:2.11.4] at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3529) ~[jackson-databind-2.11.4.jar:2.11.4] at io.seata.rm.datasource.undo.parser.JacksonUndoLogParser.decode(JacksonUndoLogParser.java:139) ~[seata-all-1.3.0.jar:1.3.0] at io.seata.rm.datasource.undo.AbstractUndoLogManager.undo(AbstractUndoLogManager.java:276) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.datasource.DataSourceManager.branchRollback(DataSourceManager.java:178) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.AbstractRMHandler.doBranchRollback(AbstractRMHandler.java:125) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:67) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:63) [seata-all-1.3.0.jar:1.3.0] at io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate(AbstractExceptionHandler.java:116) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.AbstractRMHandler.handle(AbstractRMHandler.java:63) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.DefaultRMHandler.handle(DefaultRMHandler.java:63) [seata-all-1.3.0.jar:1.3.0] at io.seata.core.protocol.transaction.BranchRollbackRequest.handle(BranchRollbackRequest.java:35) [seata-all-1.3.0.jar:1.3.0] at io.seata.rm.AbstractRMHandler.onRequest(AbstractRMHandler.java:150) [seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.handleBranchRollback(RmBranchRollbackProcessor.java:63) [seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.process(RmBranchRollbackProcessor.java:58) [seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.netty.AbstractNettyRemoting.lambda$processMessage$2(AbstractNettyRemoting.java:265) [seata-all-1.3.0.jar:1.3.0] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_275] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_275] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-all-4.1.58.Final.jar:4.1.58.Final] at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_275] 二、原因分析全局事务进行回滚时,会去查询undo_log 表中的回滚信息,回滚信息中记录了当前数据操作前后的镜像。Seata 默认采用的序列化方式是 jackson,报错信息显示jackson 反序列化失败,我们定位到JacksonUndoLogParser类的decode()方法:在数据库中,undio_log的时间字段 log_created和log_modified设置的是datetime 类型;Seata 会自己去查询数据库字段的属性、值并缓存,在插入回滚信息时,将数据库的datetime 被解析为java.time.LocalDateTime。而Seata 在使用Jackson 序列化器时,没有对java.time.LocalDateTime类型序列化进行配置,导致报错。三、解决方案方案1、降低mysql-connector-java版本调整mysql-connector-java版本 <= 8.0.22;<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency>在该版本下,数据库的datetime 被解析为java.sql.Timestamp,因此也就不存在java.time.LocalDateTime无法反序列化的问题了。这种方案最简单、快速,推荐;方案2、更换Seata的undo_log序列化方式seata支持以下几种序列化方式:FastjsonUndoLogParser:Fastjson序列化工具FstUndoLogParser:Fst序列化工具(seata 1.4.x版本开始支持)JacksonUndoLogParser:Jackson序列化工具KryoUndoLogParser:Kryo序列化工具ProtostuffUndoLogParser:Protostuff序列化工具有些序列化方式是不行的,实测结果如下:仅有Protostuff、Kryo、Fst三种序列化方式是可行的;Fastjson 和 Jackson均不支持;此外,seata 1.5.x版本修复了 FastjsonUndoLogParser 中 LocalDataTime 类型无法回滚的问题,也就是说seata 1.5.x版本Fastjson序列化方式也是可行的。1)Seata undo配置类UndoProperties中包含四个配置项:dataValidatioin:表示二阶段回滚镜像数据时是否进行校验,默认true开启;logSerialization:undo序列化方法,默认是jackson;logTable:undo表名,默认为undo_logonlyCareUpdateColumns:是否只检验SQL更新的字段,默认true开启;相关常量:public static final String SEATA_PREFIX = "seata"; public static final String CLIENT_PREFIX = SEATA_PREFIX + ".client"; public static final String UNDO_PREFIX = CLIENT_PREFIX + ".undo"; public static final boolean DEFAULT_TRANSACTION_UNDO_DATA_VALIDATION = true; // 默认采用jsckson序列化协议 public static final String DEFAULT_TRANSACTION_UNDO_LOG_SERIALIZATION = "jackson"; public static final String DEFAULT_TRANSACTION_UNDO_LOG_TABLE = "undo_log"; public static final boolean DEFAULT_ONLY_CARE_UPDATE_COLUMNS = true;2)自定义Seata undo_log序列化修改log-serialization配置即可,默认使用的是jackson;如果需要修改为其他的,则需要在pom.xml中引入相关的序列化包依赖;一般不建议修改序列化方式,因为jackson是Sring Boot已集成的,无需再添加第三方包。以protostuff序列化为例;1> application.yml文件:seata: client: undo: # 指定undo的序列化协议为protostuff log-serialization: protostuff如果seata使用的是file配置方式,也可以选择直接在file.conf中修改:client { undo { dataValidation = true logSerialization = "protostuff" logTable = "undo_log" }2> pom.xml中引入protostuff相关依赖包:<dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.8.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.8.0</version> </dependency>方案3、将数据库字段类型从datetime修改为timestamp这种方式不推荐,尤其是对于正常业务表,如果业务已经上线,再修改数据类型就很难搞。方案4、使Jackson可以解析java.time.LocalDateTime类型从seata 1.4.2版本开始 ;JacksonUndoLogParser源码中增加了对LocalDateTime类的序列化和反序列化处理;从代码逻辑来看,我们需要一个实现JacksonSerializer的类,其中负责处理LocalDateTime类型,具体操作步骤如下:1> 自定义支持可以解析java.time.OffsetDateTime类型的解析类;例如:public class JsonDateTimeSerializer implements JacksonSerializer<LocalDateTime> { private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public Class<LocalDateTime> type() { return LocalDateTime.class; @Override public JsonSerializer<LocalDateTime> ser() { return new LocalDateTimeSerializer(DATETIME_FORMAT); @Override public JsonDeserializer<? extends LocalDateTime> deser() { return new LocalDateTimeDeserializer(DATETIME_FORMAT); }2>在 META-INF/services 目录中定义名称为io.seata.rm.datasource.undo.UndoLogParser的文件,内容为JacksonSerializer实现类的全路径名;
@[TOC]一、详细报错信息springcloud 集成 seata1.3.0 时报错:2022-08-04 00:00:00.000 ERROR 78958 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : Failed to get available servers: endpoint format should like ip:port java.lang.IllegalArgumentException: endpoint format should like ip:port at io.seata.discovery.registry.FileRegistryServiceImpl.lookup(FileRegistryServiceImpl.java:95) ~[seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.netty.NettyClientChannelManager.getAvailServerList(NettyClientChannelManager.java:217) ~[seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.netty.NettyClientChannelManager.reconnect(NettyClientChannelManager.java:162) ~[seata-all-1.3.0.jar:1.3.0] at io.seata.core.rpc.netty.AbstractNettyRemotingClient$1.run(AbstractNettyRemotingClient.java:106) [seata-all-1.3.0.jar:1.3.0] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_275] at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [na:1.8.0_275] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_275] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [na:1.8.0_275] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_275] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_275] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-all-4.1.58.Final.jar:4.1.58.Final] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_275] 二、原因分析本文基于seata1.3.0版本,代表这1.X的版本,0.x版本的略有不同。在文件类型的注册服务时,会通过FileRegistryServiceImpl#lookup(String)方法根据VgroupMapping的key去寻找可用的Seata-server实例;==报错的体现正是无法根据service.VgroupMapping这个配置找到具体的Seata实例(IP:PORT信息);==几个字符串常量值如下: String PREFIX_SERVICE_MAPPING = "vgroupMapping."; String PREFIX_SERVICE_ROOT = "service"; String CONFIG_SPLIT_CHAR = "."; private static final String POSTFIX_GROUPLIST = ".grouplist"; private static final String ENDPOINT_SPLIT_CHAR = ";"; private static final String IP_PORT_SPLIT_CHAR = ":";获取当前服务组名称对应的seata集群名称的代码逻辑如下:针对 service.vgroupMapping,官方案例:Spring Cloud 快速集成 Seata是这么说的:划重点:需要注意的是 service.vgroupMapping这个配置,在 Spring Cloud 中默认是${spring.application.name}-fescar-service-group再看博主debug时的默认 service.vgroupMapping配置,居然是${spring.application.name}-seata-service-group;我程序的spring.application.name为trade-centerservice.vgroupMapping的配置为trade-center-seata-service-group,不说好的是${spring.application.name}-fescar-service-group吗!!!!!!!等下我们再继续说,先让我喷一会:***********,官方文档不更新的吗?不注意版本之间的差异吗?不标明版本差异吗?******。所以原因本质上只有一个:无法根据service.VgroupMapping这个配置找到具体的Seata实例(IP:PORT信息);但导致该原因产生的方式会有很多种;原因1:service.vgroupMapping配置的服务组名称不符合Seata默认要求;前提:不在application.yml配置文件中手动通过seata.tx-service-group属性指定seata服务组名称。默认 service.vgroupMapping这个配置,在 Spring Cloud 中默认是${spring.application.name}-seata-service-group所以,当我们未按${spring.application.name}-seata-service-group这个规则配置service.vgroupMapping时会报错。报错配置样例:解决方案对应下面的方案一、方案二。原因2:service.vgroupMapping配置的seata集群名称没有对应的grouplist比如:这里我配置的service.vgroupMapping值为seata-server-sh,而配置的grouplist是属于default的;所以报错是因为通过service.vgroupMapping配置找到seata集群名称seata-server-sh,但是seata-server-sh没有对应的grouplist,即seata实例信息。解决方案:把grouplist的的所属方调整为和service.vgroupMapping配置的seata集群名称一致;三、解决方案方案1、将file.conf中service.vgroupMapping配置调整为${spring.application.name}-seata-service-group;就博主程序而言,application.yml、file.conf配置如下:1> application.yml:spring: application: name: trade-center2> file.conf关键配置:方案二、在application.yml中指定seata.tx-service-group就博主程序而言,application.yml、file.conf配置如下:1> application.yml:spring: application: name: trade-center seata: # tx-service-group的值一定要和file.conf中service.vgroupMapping配置对应上 tx-service-group: saint_trade_tx_group或者使用:spring: application: name: trade-center cloud: alibaba: seata: # tx-service-group的值一定要和file.conf中service.vgroupMapping配置对应上 tx-service-group: saint_trade_tx_group2> file.conf关键配置:我们注意到可以使用spring.cloud.alibaba.seata.tx-service-group 或 seata.tx-service-group 属性指定service.vgroupMapping配置,为啥呢?spring.cloud.alibaba.seata.tx-service-group 和 seata.tx-service-groupspring.cloud.alibaba.seata.tx-service-group 属于SpringCloudAlibabaConfiguration类:seata.tx-service-group 属性属于SeataProperties类:而SeataProperties类又被注解@EnableConfigurationProperties(SpringCloudAlibabaConfiguration.class)标注,所以会使SpringCloudAlibabaConfiguration的配置生效;又SeataProperties类中通过@Autowired的方式组合了SpringCloudAlibabaConfiguration,在通过getTxServiceGroup()方法获取txServiceGroup属性时如果SeataProperties类自己没有配置txServiceGroup,则从SpringCloudAlibabaConfiguration中获取;简单来说;就是优先取 seata.tx-service-group 属性值,没有则再取spring.cloud.alibaba.seata.tx-service-group属性值。四、seata集群名称的坑seata-server和 项目程序 部署在不同的机器上时,可能会出现 can not connect to services-server。如果配置的 default.grouplist = "192.168.7.254:8091",则并不会生效,default.grouplist读取的还是默认的127.0.0.1:8091;建议把seata集群名称调整为非dafault,例如:service { vgroupMapping.saint_trade_tx_group = "seata-server-sh" seata-server-sh.grouplist = "127.0.0.1:8091"
一、环境准备1、项目下载/服务配置官方下载地址:https://github.com/alibaba/nacos/releases/download/2.0.3/nacos-server-2.0.3.zip1)解压上述压缩包,进入到conf目录,修改配置(外部MySQL连接),如下:### 端口号 server.port=8848 ### Specify local server's IP,本机启动nacos服务IP nacos.inetutils.ip-address=127.0.0.1 ### 数据存储方式使用MySQL spring.datasource.platform=mysql ### Count of DB: db.num=1 ### Connect URL of DB,数据库连接信息 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=123456注意:数据连接信息中的db.url中最后一定要加上&serverTimezone=UTC,否则在一些老版本中启动nacos server会报异常。2)在 Nacos 的解压目录 nacos/conf 目录下,复制配置文件 cluster.conf.example 并重命名为 cluster.conf,每行配置为每个nacos服务所在机器的 ip:port;例如:### 将示例文件改为集群配置文件 cp cluster.conf.example cluster.conf vim cluster.conf ### 将3个机器的IP和端口写到集群配置文件中 127.0.0.1:8848 127.0.0.1:8849 127.0.0.1:88502、MySQL数据库创建Nacos项目源码官方下载地址:https://github.com/alibaba/nacos/archive/refs/tags/2.0.3.zipSQL源文件地址在 nacos-2.0.3 解压目录./distribution/conf 的目录下;首先要自己创建nacos_config数据库,然后在运行nacos-mysql.sql文件内容:/******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info */ /******************************************/ CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN'); 运行脚本后的表结构如下:3、服务启动1)单机方式启动一个实例### 进入解压目录 nohup sh ./bin/startup.sh -m standalone &注意:如果数据库连接信息错误或者 老版本Nacos的db.url信息中没有加&serverTimezone=UTC,会报如下错误: 2021-12-07 11:31:20,839 ERROR Startup errors : {} org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'configOpsController' defined in URL [jar:file:/Users/zhouxin/Desktop/info/software/nacosCluster/nacos8848/target/nacos-server.jar!/BOOT-INF/lib/nacos-config-1.4.1.jar!/com/alibaba/nacos/config/server/controller/ConfigOpsController.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'externalDumpService': Invocation of init method failed; nested exception is ErrCode:500, ErrMsg:Nacos Server did not start because dumpservice bean construction failure : No DataSource set at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:769) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:218) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1338) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1185) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:554) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:514) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:321) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:319) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391) at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204) at com.alibaba.nacos.Nacos.main(Nacos.java:35) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) at org.springframework.boot.loader.Launcher.launch(Launcher.java:107) at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:467) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'externalDumpService': Invocation of init method failed; nested exception is ErrCode:500, ErrMsg:Nacos Server did not start because dumpservice bean construction failure : No DataSource set at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:139) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:413) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1761) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:514) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:321) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:319) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1276) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1196) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760) ... 27 common frames omitted Caused by: com.alibaba.nacos.api.exception.NacosException: Nacos Server did not start because dumpservice bean construction failure : No DataSource set at com.alibaba.nacos.config.server.service.dump.DumpService.dumpOperate(DumpService.java:203) at com.alibaba.nacos.config.server.service.dump.ExternalDumpService.init(ExternalDumpService.java:50) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:363) at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:307) at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:136) ... 40 common frames omitted Caused by: java.lang.IllegalStateException: No DataSource set at org.springframework.util.Assert.state(Assert.java:73) at org.springframework.jdbc.support.JdbcAccessor.obtainDataSource(JdbcAccessor.java:77) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:371) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:452) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:462) at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:473) at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:480) at com.alibaba.nacos.config.server.service.repository.extrnal.ExternalStoragePersistServiceImpl.findConfigMaxId(ExternalStoragePersistServiceImpl.java:553) at com.alibaba.nacos.config.server.service.dump.processor.DumpAllProcessor.process(DumpAllProcessor.java:51) at com.alibaba.nacos.config.server.service.dump.DumpService.dumpConfigInfo(DumpService.java:260) at com.alibaba.nacos.config.server.service.dump.DumpService.dumpOperate(DumpService.java:172) ... 48 common frames omitted 2021-12-07 11:31:34,810 WARN [WatchFileCenter] start close 2021-12-07 11:31:34,811 WARN [WatchFileCenter] start to shutdown this watcher which is watch : /Users/zhouxin/Desktop/info/software/nacosCluster/nacos8848/conf 2021-12-07 11:31:34,811 WARN [WatchFileCenter] already closed 2021-12-07 11:31:34,811 WARN [NotifyCenter] Start destroying Publisher 2021-12-07 11:31:34,811 WARN [NotifyCenter] Destruction of the end 2021-12-07 11:31:34,811 ERROR Nacos failed to start, please see /Users/zhouxin/Desktop/info/software/nacosCluster/nacos8848/logs/nacos.log for more details. 2021-12-07 11:31:34,826 INFO Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2021-12-07 11:31:34,829 ERROR Application run failed2)集群方式启动多个实例### 进入三台机器的nacos解压目录 ./bin/start.sh访问以下链接,默认用户名/密码是 nacos/nacos :http://47.116.142.177:8847/nacos/http://47.116.142.177:8848/nacos/http://47.116.142.177:8849/nacos/
一、前言分析完SpringBoot启动加载Nacos配置的流程,我们注意到它会优先使用LocalConfigInfoProcessor.getFailover()加载Nacos的本地配置文件,文件路径如下:user.home/nacos/config/serverName_nacos/data/config-data-tenant/tenant/group/dataId其中serverName的构成如下:fixed-IP_Port-namespaceId_nacos上述文件路径构成中,类似user.home的都是变量;tenant为nameSpaceId。二、验证1、LocalConfigInfoProcessor类开始验证之前,我们先了解一下LocalConfigInfoProcessor类的几个关键方法:1、getFailover()方法首先会通过getFailoverFile()获取本地配置文件,然后通过readFile()读取文件内容;2、getSnapshot()方法首先通过getSnapshotFile()获取远程配置的snapshot文件,然后通过readFile读取;3、saveSnapshot()方法会存储从Nacos服务端获取到的新的config;4、cleanAllSnapshot方法会清除snapshot目录下所有缓存文件。2、验证正文LocalConfigInfoProcessor.saveSnapshot保存的Nacos远端配置快照数据路径在我本机的文件路径如下:其中:1)/Users/zhouxin是user.home;2)fixed-127.0.0.1_8848-2c38da96-f654-4105-bbc0-63befaa449f0_nacos是serverName;3)2c38da96-f654-4105-bbc0-63befaa449f0是namespaceId;4)DEFAULT_GROUP是group;5)ls的结果(config1.yaml config2.yaml config3.yaml config5.yaml)是dataId集合;开始搞事情!在/Users/zhouxin/nacos/config/fixed-127.0.0.1_8848-2c38da96-f654-4105-bbc0-63befaa449f0_nacos/目录下创建如下文件目录:/data/config-data-tenant/2c38da96-f654-4105-bbc0-63befaa449f0/DEFAULT_GROUP并新建一个config1.yaml文件,内容如下:student: name: saint-local-config-data-tenantNacos远端相同nameSpace、Group下的config1.yaml,内容为:student: name: saint--name spring: cloud: config: # 本地配置允许覆盖远程配置 override-none: true allow-override: false override-system-properties: false启动SpringBoot项目,我们可以看到他在本地目录找到了config1.yaml文件:1)结论1、/data/config-data-tenant/2c38da96-f654-4105-bbc0-63befaa449f0/DEFAULT_GROUP目录下的config1.yaml文件覆盖了Nacos服务端的config1.yaml文件;2、Nacos服务端的config1.yaml中所有配置全部失效。因为/data/config-data-tenant/2c38da96-f654-4105-bbc0-63befaa449f0/DEFAULT_GROUP目录下的config1.yaml文件是全量覆盖。2)再做个实验/data/config-data-tenant/2c38da96-f654-4105-bbc0-63befaa449f0/DEFAULT_GROUP这个目录下会不会自动生成一些文件,比如我第一次访问Nacos服务端配置之后。进入到/Users/zhouxin/nacos/config/fixed-127.0.0.1_8848-2c38da96-f654-4105-bbc0-63befaa449f0_nacos目录,先把我们手动添加的文件目录全部删除;接着运行SpringBoot项目再ls看一下/Users/zhouxin/nacos/config/fixed-127.0.0.1_8848-2c38da96-f654-4105-bbc0-63befaa449f0_nacos目录;嘿,它不会自动添加./data一系列目录文件。最后,我们一起思考下这个机制的作用是什么?欢迎评论留言
一、前言项目上使用Nacos做为配置中心,以前使用的是Concul;现在准备详细梳理一下Nacos做为配置中心的配置文件加载优先级问题,因为以前在使用Consul的时候,由于配置文件优先级问题踩过大坑。二、实战1、集成Nacos配置中心1) POM中引入SpringCloud、Spring Cloud Alibaba依赖,记得做好maven版本管理、否则存在版本问题一地鸡毛的场景。<properties> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> </properties> <dependencies> <!--整合nacos config--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</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> <!--整合spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--整合spring cloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>2) 在bootstrap.yml中配置Nacos-server的地址server: port: 9001 spring: application: name: config1 # 配置名,即Nacos dashboard中的Data id cloud: nacos: config: server-addr: 127.0.0.1:8848 # 配置中心地址,集群的话多个节点使用,分隔 file-extension: yaml # 配置的后缀名 namespace: 2c38da96-f654-4105-bbc0-63befaa449f0 # 命名空间ID group: DEFAULT_GROUP # 指定要获取配置的组3) 随便写个Controller,用@Value等注解或ConfigurableApplicationContext获取配置信息。import javax.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author zhouxin * @RefreshScope 动态加载配置 @RefreshScope @RestController public class NacosServerController { @Resource private ConfigurableApplicationContext configurableApplicationContext; * 可用于验证本服务配置文件和extension-configs之间的优先级 @Value("${student.name:null}") private String name; * 如果只是通过@Value获得的配置信息,不会随着Nacos的修改操作而获得最新配置信息。那么,实时获取最新配置信息有两种方式: * 方式1:添加@RefreshScope注解。 * 方式2:通过getProperty的方式,获得最新的配置信息。 * @return @GetMapping("/name") public String getName() { String name1 = configurableApplicationContext.getEnvironment().getProperty("student.name"); return String.format("name=%s <br> name1=%s", name, name1); }Nacos服务端config1.yaml配置如下:启动服务,通过HTTP访问(http://localhost:9001/name)到的结果如下:这说明:我们可以通过很多中方式注入配置,包括:@Value、@ConfigurationProperties、ConfigurableApplicationContext等。2、应用配置共享Nacos提供两种应用间共享配置的方式:扩展DataId(extension-configs)、共享DataId(shared-configs);另外需共享的DataId,yaml后缀不能少,且目前只支持yaml/properties。0)Nacos服务端配置准备(1)config1.yaml(2)config2.yaml(3)config3.yaml(4)config4.yaml(5)config5.yaml1)扩展Data Id配置Spring Cloud Alibaba Nacos Config 从 0.2.1 版本后,可支持自定义扩展Data Id 配置,特性如下:1、支持多个 Data Id 的配置;通过 spring.cloud.nacos.config.extension-configs[n].data-id 的配置方式2、可以自定义Data Id 所在的组,不明确配置的话,默认是 DEFAULT_GROUP;通过 spring.cloud.nacos.config.extension-configs[n].group 的配置方式3、当某个Data Id 的配置变更时,自定义应用中是否动态刷新配置值,默认为否;通过 spring.cloud.nacos.config.extension-configs[n].refresh 的配置方式源码中的体现:(1)在bootstrap.yml文件中按住command 鼠标左键点extension-configs,进入如下源码:从上面我们可以看出扩展DataID的主体是Config类,我们看一下它:从这里我们便能看出上面提到的extension-configs的特性和一些默认值。使用:(1)bootstrap.yml文件中增加如下配置:spring: cloud: nacos: config: # 使用extension-configs[n],配置加载多个dataId extension-configs: - data-id: config4.yaml group: CONFIG4_GROUP refresh: true # 动态刷新配置 - data-id: config5.yaml group: DEFAULT_GROUP refresh: true # 动态刷新配置(2)controller中引入只存在于config4.yaml和config5.yaml中的配置:@Value("${config4.name:null}") private String config4Name; @Value("${config5.name:null}") private String config5Name; @GetMapping("/allname") public String getAllName() { return String.format("config4Name=%s <br> config5Name=%s", config4Name, config5Name); }通过HTTP访问(http://localhost:9001/allname)到的结果如下:2)共享Data Id配置为了更加清晰的在多个应用间配置共享的 Data Id ,也可以通过以下的方式来配置:1、支持多个 Data Id 的配置;通过 spring.cloud.nacos.config.shared-configs[n].data-id 的配置方式2、可以自定义Data Id 所在的组,不明确配置的话,默认是 DEFAULT_GROUP;通过 spring.cloud.nacos.config.shared-configs[n].group 的配置方式3、当某个Data Id 的配置变更时,自定义应用中是否动态刷新配置值,默认为否;通过 spring.cloud.nacos.config.shared-configs[n].refresh 的配置方式使用:(1)bootstrap.yml文件中增加如下配置:spring: cloud: nacos: config: # 使用shared-config[n],配置加载多个dataId shared-configs: - data-id: config2.yaml group: DEFAULT_GROUP refresh: true # 动态刷新配置 - data-id: config3.yaml group: DEFAULT_GROUP refresh: true # 动态刷新配置(2)controller中引入只存在于config2.yaml和config3.yaml中的配置:@Value("${config2.name:null}") private String config2Name; @Value("${config3.name:null}") private String config3Name; @GetMapping("/allname") public String getAllName() { return String.format("config2Name=%s <br> config3Name=%s", config2Name, config3Name); }通过HTTP访问(http://localhost:9001/allname)到的结果如下:聪明的大家肯定发现这和extension-configs扩展DataId的方式一毛一样啊,是的,它就一毛一样。但是注意,在老版本中它俩的扩展方式还真不一样,下面我们来看一下新老版本扩展/共享DataId的差异?3、扩展/共享DataId新老版本差异1)老版本的pom<properties> <java.version>1.8</java.version> <spring.boot>2.1.3.RELEASE</spring.boot> <spring.cloud>Greenwich.RELEASE</spring.cloud> <spring.cloud.alibaba>2.1.0.RELEASE</spring.cloud.alibaba> </properties> <dependencyManagement> <dependencies> <!-- 引入Spring Cloud Alibaba依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 引入Spring Cloud依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 引入Spring Boot依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>2)扩展DataId方式的差异新版本yaml中使用extension-configs,老版本yaml使用ext-config;除此之外,功能上并没有区别。3)分享DataId方式的差异(1)使用上:1、新版本yaml中使用shared-configs;2、老版本yaml使用shared-dataids根据DataID加载配置信息、使用refreshable-dataids实现指定DataId配置信息的动态刷新;2)功能上:新版本yaml中支持指定group,老版本中不支持指定Group,只会取默认的group--DEFAULT_GROUP;为什么老版本不支持指定group呢?我们来看一下:3)源码分析(1) 在bootstrap.yml文件中按住command 鼠标左键点shared-dataids,进入如下源码:public static final String SEPARATOR = "[,]";这里我们可以看到它会将传入的sharedDataids以,分隔为多个DataId,并将DataId作为入参构造Config:其中只会指定dataId,并不会牵扯到group。(2) 我们再看shared-dataids:它也只是将拆分出的所有dataId对应的Config的refresh属性设置为true,也不牵扯到group。(3)会不会有group-dataids呢?进入NacosConfigProperties类中全局搜索一把,并没有。好了,新老版本的差异我们也聊完了,下面看一下配置文件加载的优先级吧。4、扩展的配置文件加载优先级Spring Cloud Alibaba Nacos Config 目前提供了三种配置能力从 Nacos 拉取相关的配置;A: 通过 spring.cloud.nacos.config.shared-configs[n].data-id 支持多个共享 Data Id 的配置;B: 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的方式支持多个扩展 Data Id 的配置;C: 通过内部相关规则(应用名、应用名+ Profile )自动生成相关的 Data Id 配置;当三种方式共同使用时,总的来说 他们的一个优先级关系是:A < B < C。(1)以老版本来说:1、shared-dataids中越靠后,优先级越高;2、ext-config中n越大、或越靠后,优先级越高;3、Shared-dataids < ext-config < 内部;(2)以新版本来说:1、shared-configs中n越大、或越靠后,优先级越高;2、extension-configs中n越大、或越靠后,优先级越高;3、shared-configs < extension-configs < 内部;1)逻辑图相信大家看了这个图,也不需要做过多解释了。2)代码(1)controller:import javax.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; * @author zhouxin * @RefreshScope 动态加载配置 @RefreshScope @RestController public class NacosServerController { @Resource private ConfigurableApplicationContext configurableApplicationContext; * 可用于验证本服务配置文件和extension-configs之间的优先级 @Value("${student.name:null}") private String name; @Value("${config2.name:null}") private String config2Name; @Value("${config3.name:null}") private String config3Name; @Value("${config4.name:null}") private String config4Name; @Value("${config5.name:null}") private String config5Name; * 用于验证shared-configs之间的优先级 @Value("${config.name:null}") private String configName; * extension-configs和shared-configs的优先级 @Value("${ext.name:null}") private String extName; * 用于验证extension-configs之间的优先级 @Value("${ext.gender:null}") private String extGender; * 如果只是通过@Value获得的配置信息,不会随着Nacos的修改操作而获得最新配置信息。那么,实时获取最新配置信息有两种方式: * 方式1:添加@RefreshScope注解。 * 方式2:通过getProperty的方式,获得最新的配置信息。 * @return @GetMapping("/name") public String getName() { String name1 = configurableApplicationContext.getEnvironment().getProperty("student.name"); return String.format("name=%s <br> name1=%s", name, name1); @GetMapping("/allname") public String getAllName() { return String.format("config1Name=%s<br> config2Name=%s<br> config3Name=%s<br> config4Name=%s <br> config5Name=%s <br> configName=%s <br> extName=%s <br> extGender=%s", name, config2Name, config3Name, config4Name, config5Name, configName, extName, extGender); }(2)新版本bootstrap.yml配置:server: port: 9001 spring: application: name: config1 cloud: nacos: config: # 此处的nacos用于配置管理 server-addr: 127.0.0.1:8848 file-extension: yaml namespace: 2c38da96-f654-4105-bbc0-63befaa449f0 # 命名空间ID group: DEFAULT_GROUP # 指定要获取配置的组 shared-configs: - data-id: config2.yaml refresh: true - data-id: config3.yaml refresh: true extension-configs: - data-id: config4.yaml group: CONFIG4_GROUP refresh: true # 动态刷新配置 - data-id: config5.yaml group: DEFAULT_GROUP refresh: true # 动态刷新配置(3)老版本bootstrap.yml配置:server: port: 9001 spring: application: name: config1 # 配置名 cloud: nacos: config: server-addr: 127.0.0.1:8848 # 配置中心地址——单机 file-extension: yaml # 后缀名 namespace: 2c38da96-f654-4105-bbc0-63befaa449f0 # 命名空间ID group: DEFAULT_GROUP # 指定要获取配置的组 # 使用shared-dataids,配置加载多个dataId shared-dataids: config2.yaml,config3.yaml refreshable-dataids: config2.yaml # 使用ext-config[n],配置加载多个dataId ext-config: - data-id: config4.yaml group: CONFIG4_GROUP refresh: true # 动态刷新配置 - data-id: config5.yaml group: DEFAULT_GROUP refresh: true # 动态刷新配置5、本地和远程配置优先级SpringCloud中,nacos是借助SpringCloud的Config来加载属性源的,所以是否覆盖系统属性和配置文件属性的设置也是通过SpringCloud的配置进行触发。默认情况下nacos属性源配置优先级最高,会覆盖系统属性源和配置属性源(本地文件)。可以通过在远程配置中心(Nacos服务中)中做如下配置,设置本地配置覆盖远程配置:spring: cloud: config: # Nacos远程配置是否不覆盖其他属性源(文件、系统),默认为false,即覆盖其他源(文件、系统),当allow-override:为true时才会生效 override-none: true # 是否允许Nacos远程配置被本地文件覆盖,默认为true allow-override: true # Nacos远程配置是否可以覆盖系统属性源(系统环境变量或系统属性),默认为true,即允许 override-system-properties: false注意:1、将其放在bootstrap.yml或application.yml配置文件中是无效的。2、如果allow-override值为false,即使将override-none设置为true,也是无效的。6、其他通过设置 spring.cloud.nacos.config.enabled = false 来完全关闭 Spring Cloud Nacos Config;更多Nacos-config的内容请参考Nacos的GitHub Wiki:https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config三、最佳实践仁者见仁智者见智,仅供参考;1、能放本地、不放远程;避免滥用远程服务器;2、尽量规避优先级;3、定规则,例如所有配置属性都要加上注释;4、配置管理人员尽量少;最后,虽然我用的Nacos1.4.1版本,但是不建议大家在生产环境使用这个版本,因为该版本存在一个致命的bug:客户端与 Nacos Server 如果发生短暂的域名解析问题,会导致心跳永久丢失,进而引发服务全量下线,即使网络恢复,也不会自动恢复心跳。域名解析失败常见于网络抖动或者 K8s 环境下的 coreDNS 访问超时等场景,并且该问题仅存在于 1.4.1 版本,低于此版本不受此问题的影响,使用 1.4.1 的用户建议升级至 1.4.2 以避免此问题(源自阿里内部博文)。
一、Nacos服务注册流程图服务注册表serviceMap/** * Map(namespace, Map(group::serviceName, Service)). private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();二、Nacos服务注册入口1、SpringBoot自动装配使用Nacos做为注册中心,首先要在pom中引入:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>嗯,然后呢?SpringBoot项目大家都知道有一个很重要的东西:spring.factories,这个是针对引入的jar包做自动装配用的;在SpringBoot项目运行时,SpringFactoriesLoader 类会去寻找META-INF/spring.factories文件,spring.factories用键值对的方式记录了所有需要加入容器的类,key为:org.springframework.boot.autoconfigure.EnableAutoConfiguration,value为各种xxxAutoConfiguration;或key为:org.springframework.cloud.bootstrap.BootstrapConfiguration,value为各种xxxBootstrapConfiguration。大家对EnableAutoConfiguration都比较熟悉了,自动装配的撒;而BootstrapConfiguration可能会相对陌生一点,它呢是做自定义启动配置的,也就是说:所有的Bean都会在SpingApplicatin启动前加入到它的上下文中。2、Nacos中的spring.factories查看引入的nacos-discovery包的层级结构,找到spring.factories文件;打开这文件瞅一瞅:上面我有聊到BootstrapConfiguration是在SpringBoot项目启动时自动加载配置到SpingApplicatin的上下文中。我们进去点进去看一下这个类NacosDiscoveryClientConfigServiceBootstrapConfiguration:emmm,它好像啥也没干,就引入了两个自动配置类:NacosDiscoveryClientAutoConfiguration,NacosDiscoveryAutoConfiguration;咱也不知道哪个是和服务注册相关的,先都点进去看看撒。(1)NacosDiscoveryClientAutoConfiguration:额额额,从命名来看,好像没有和注册相关的,先看看下一个类吧。(2)NacosDiscoveryAutoConfiguration:卧槽,这个好。这个类里能看到用注册register命名的方法;感觉就它了。3、客户端自动注册入口类NacosDiscoveryClientAutoConfigurationNacosDiscoveryAutoConfiguration类内部内部管理了三个类:NacosServiceRegistry(完成服务注册功能,实现ServiceRegistry接口),NacosRegistration(注册时用来存储nacos服务端的相关信息),NacosAutoServiceRegistration(自动注册功能)。这个自动注册功能是怎么实现的呢?从类的命名上来看,感觉是在NacosAutoServiceRegistration中的,我们跟进去看一下:NacosAutoServiceRegistration类继承自AbstractAutoServiceRegistration类;再看AbstractAutoServiceRegistration类实现了ApplicationListener接口。ApplicationListener接口实际是一个事件监听器,其监听WebServerInitializedEvent事件。在AbstractAutoServiceRegistration类的bind()方法中会绑定一个事件,启动调用start()方法进行事件的绑定操作。start()方法中再调用register()方法实现注册功能,具体的register()内部逻辑由子类实现。这里和AQS一样,是典型模板方法设计模式。go on,go on;我们接着看ServiceRegistry接口的实现类,点一下发现直接蹦到了NacosServiceRegistry类上;感觉对味了。虽然我们找到了服务注册的入口,但是呢,其实spring-cloud-commons包中定义了一套服务注册规范,集成Spring Cloud实现服务注册的组件都会实现ServiceRegistry接口。4、Nacos服务注册类NacosServiceRegistry现在我们继续接着NacosServiceRegistry#register()方法来看:先是组装服务实例信息,然后走入到了NamingService接口#registerInstance()方法,我跟,我继续往里跟;点到NamingService接口的实现类NacosNamingService;NacosNamingService类在初始化的时候会实例几个比较关键的类:// 1)发送心跳的 private BeatReactor beatReactor; // 2)事件分发器:订阅的服务发生改变时,Nacos服务端就会通知到当前Nacos Client端; // Client端收到这个通知后,将通知的事件给到观察者,也就是我们自己实现的listener private EventDispatcher eventDispatcher; // 3)与Nacos服务端通信的 private NamingProxy serverProxy;NacosNamingService#registerInstance()方法套娃如下:@Override public void registerInstance(String serviceName, Instance instance) throws NacosException { registerInstance(serviceName, Constants.DEFAULT_GROUP, instance); @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { if (instance.isEphemeral()) { // 组装心跳信息 BeatInfo beatInfo = new BeatInfo(); beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName)); beatInfo.setIp(instance.getIp()); beatInfo.setPort(instance.getPort()); beatInfo.setCluster(instance.getClusterName()); beatInfo.setWeight(instance.getWeight()); beatInfo.setMetadata(instance.getMetadata()); beatInfo.setScheduled(false); long instanceInterval = instance.getInstanceHeartBeatInterval(); beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval); // 1)发送心跳 beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo); // 2)注册服务 serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance); }这里有两个重要的点:1、如果实例节点是临时节点的话(默认是临时的),会组装一个心跳信息,然后通过BeatReactor组件发送心跳到服务端,也就是服务续约;2、调用ServerProxy组件注册服务到服务端,即服务上线。在聊服务续约和服务注册之前,我们先看一下serviceName的组成,其实就是就是上面传递下来的groupName和serviceName用@@拼接起来作为新的serviceName,以达到Group层的数据隔离。NamingUtils.getGroupedName(serviceName, groupName) // NamingUtils public static String getGroupedName(String serviceName, String groupName) { return groupName + Constants.SERVICE_INFO_SPLITER + serviceName; }1)Client服务续约即Nacos Client端定时发送心跳到服务端。在BeatReactor#addBeatInfo()方法中,会启动一个定时任务,立即执行BeatTask这个任务。我们接着来看BeatTask这个Runnable接口的实现类,是如何运行的?我们可以看到它主要展现两个能力:(1)调用NamingProxy向Nacos服务端发送心跳;(2)再开启一个定时任务,延时5S再持续发送心跳到Nacos服务端,即默认每5s向Nacos服务端发送一次心跳。发送心跳就很简单了,直接采用HTTP接口调用的方式,调用服务端的/nacos/v1/ns/instance/beat接口。2)Client服务注册在NamingProxy#registerService()方法中直接做HTTP请求调用Nacos Server的接口/nacos/v1/ns/instance做服务注册:public static String WEB_CONTEXT = "/nacos"; public static String NACOS_URL_BASE = WEB_CONTEXT + "/v1/ns"; public static String NACOS_URL_INSTANCE = NACOS_URL_BASE + "/instance";5、Nacos Server端如何接收心跳、服务注册请求上面我们聊到了Nacos Client端分别调用服务端的/nacos/v1/ns/instance/beat、/nacos/v1/ns/instance接口进行服务续约和服务注册,下面我们聊一下服务端对这个两个请求是如何处理的?0)先找入口Nacos Client通过NamingProxy类调用Nacos Server,以开源框架的命名规范来看,Nacos Server的源码中应该有个和naming相关命名的模块;接着往下展开,找到controllers包下的InstanceController:1)Server端接收服务注册请求InstanceController#register()方法是Nacos Server接收服务注册请求的入口:@CanDistro @PostMapping @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public String register(HttpServletRequest request) throws Exception { final String namespaceId = WebUtils .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); final Instance instance = HttpRequestInstanceBuilder.newBuilder() .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build(); // 真正注册服务的地方,serviceName为注册的服务名,namespaceId默认为public getInstanceOperator().registerInstance(namespaceId, serviceName, instance); return "ok"; }其中namespaceId可以做一层数据隔离;我们接着往里看getInstanceOperator().registerInstance():InstanceOperator接口有两个实现类,分别是:InstanceOperatorClientImpl,InstanceOperatorServiceImpl,由于我这里是作为服务端,我们关注InstanceOperatorServiceImpl;1、组装服务的实例信息,包含:IP、Port2、调用ServiceManager的registerInstanc()方法,完成真正的服务注册操作。ServiceManager#registerInstanc()方法中主要做四件事:1、如果服务不存在,则创建一个服务;2、从本地缓存《Map(namespace, Map(group::serviceName, Service))》中获取服务信息;3、校验服务不能为null;4、将服务的实例信息添加到DataStore的dataMap缓存中(1)创建服务这里的操作也很简单,套娃套娃套娃,不对是封装、抽象。最终将服务信息保存到本地缓存serviceMap中。public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException { createServiceIfAbsent(namespaceId, serviceName, local, null); public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException { Service service = getService(namespaceId, serviceName); if (service == null) { Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName); service = new Service(); service.setName(serviceName); service.setNamespaceId(namespaceId); service.setGroupName(NamingUtils.getGroupName(serviceName)); // now validate the service. if failed, exception will be thrown service.setLastModifiedMillis(System.currentTimeMillis()); service.recalculateChecksum(); if (cluster != null) { cluster.setService(service); service.getClusterMap().put(cluster.getName(), cluster); service.validate(); // 存储服务信息和初始化,点进去 putServiceAndInit(service); if (!local) { addOrReplaceService(service); private void putServiceAndInit(Service service) throws NacosException { putService(service); service = getService(service.getNamespaceId(), service.getName()); // 服务初始化,会做健康检查 service.init(); // 添加两个监听器,使用Raft协议和 Distro协议维护数据一致性的,,包括:Nacos Client感知服务提供者实例变更 consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service); consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service); Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson()); // ”服务注册“操作,也就是将服务信息保存到本地缓存serviceMap中。 public void putService(Service service) { if (!serviceMap.containsKey(service.getNamespaceId())) { serviceMap.putIfAbsent(service.getNamespaceId(), new ConcurrentSkipListMap<>()); serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service); }注意:在service.init()初始化服务时,会启动一个定时任务做不健康服务的服务剔除:/** * Service#init() public void init() { // 开始一个定时任务,对不健康的服务实例做服务下线/剔除,点进去 HealthCheckReactor.scheduleCheck(clientBeatCheckTask); for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) { entry.getValue().setService(this); entry.getValue().init(); @JsonIgnore private ClientBeatCheckTask clientBeatCheckTask = new ClientBeatCheckTask(this);走到HealthCheckReactor.scheduleCheck()方法中,会延时5s开启一个固定延时5s的FixedDelay类型的定时任务执行服务剔除操作;另外:由于ClientBeatCheckTask不是NacosHealthCheckTask的实现类,所以定时任务中执行的方法为ClientBeatCheckTask中的run()方法;看一下ClientBeatCheckTask的run()方法:通过判断当前时间和实例最后一次心跳时间的间隔是否大于阈值(默认15s),决定是否进行服务剔除/下线;(2)从本地缓存Map中获取服务信息从本地缓存serviceMap中获取服务信息,没别的啥操作public Service getService(String namespaceId, String serviceName) { // 如果namespaceId没有的话 if (serviceMap.get(namespaceId) == null) { return null; // 点进去 return chooseServiceMap(namespaceId).get(serviceName); public Map<String, Service> chooseServiceMap(String namespaceId) { return serviceMap.get(namespaceId); }(3)校验服务不能为null一个单纯的判空处理;public void checkServiceIsNull(Service service, String namespaceId, String serviceName) throws NacosException { if (service == null) { throw new NacosException(NacosException.INVALID_PARAM, "service not found, namespace: " + namespaceId + ", serviceName: " + serviceName); }(4)**服务实例信息"持久化"将服务的相应实例信息保存到DataStore的serviceMap中;由于存在多个服务实例同时注册的场景,所以要加一个synchronized锁。public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException { String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral); Service service = getService(namespaceId, serviceName); synchronized (service) { List<Instance> instanceList = addIpAddresses(service, ephemeral, ips); Instances instances = new Instances(); instances.setInstanceList(instanceList); // 保存服务信息,key为namespaceId和serviceName的结合体,点进去 consistencyService.put(key, instances); }ConsistencyService是一个接口,由于我们默认是采用ephemeral方式(在聊Nacos Client时我们也有提到,服务端见Instance#isEphemeral() 或 SwitchDomain#isDefaultInstanceEphemeral()),所以以临时Client为例,我们看一下DistroConsistencyServiceImpl;如果是持久client,则关注RaftConsistencyServiceImpl。@Override public void put(String key, Record value) throws NacosException { // 服务实例信息持久化,点进去 onPut(key, value); // If upgrade to 2.0.X, do not sync for v1. if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) { return; // Nacos集群间数据同步 distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE, DistroConfig.getInstance().getSyncDelayMillis()); }进入到DistroConsistencyServiceImpl类的onPut()方法:public void onPut(String key, Record value) { if (KeyBuilder.matchEphemeralInstanceListKey(key)) { Datum<Instances> datum = new Datum<>(); datum.value = (Instances) value; datum.key = key; datum.timestamp.incrementAndGet(); // 往DataStore的dataMap中添加数据,点进去 dataStore.put(key, datum); // 如果listener中没有这个key的话直接返回,key是在创建Service时添加进去的,见ServiceManager#putServiceAndInit()方法 if (!listeners.containsKey(key)) { return; // 通知Nacos client服务端服务实例信息发生变更,这里是先添加任务;点进去 notifier.addTask(key, DataOperation.CHANGE); }1. 往DataStore的dataMap中添加数据@Component public class DataStore { private Map<String, Datum> dataMap = new ConcurrentHashMap<>(1024); public void put(String key, Datum value) { // 单纯的往一个Map缓存放一下数据 dataMap.put(key, value); }2. 进入到Notifier类内部,通知服务实例信息变更进入到DistroConsistencyServiceImpl的内部类Notifier,先看其addTask()方法:最重要的一步将服务实例信息添加到tasks任务队列中,按常理来说到这也就结束了;但是添加到任务队列之后呢,怎么处理嘞?我们注意到DistroConsistencyServiceImpl中有一个@PostConstruct修饰的init()方法,也就是说在DistroConsistencyServiceImpl类构造器执行之后会执行这个方法启动Notifier通知器:我们走入Notifer#run()方法:首先从tasks任务丢列中取出任务,调用自己的handle()方法处理任务:handle()方法中调用监听器listener的onChange()/onDelete()方法执行相应通知数据更新、删除的逻辑。下面我着重看一下通知数据变更的onChange()方法:RecordListener是一个接口,它有三个实现,我们进入到它的实现类Service中;着重看一下updateIPs()方法,其他代码不涉及到主链路;遍历要注册的instance集合,采用一个临时的Map记录clusterName和Instance实例的对应关系,然后更新clusterMap<clusterName, Cluster>:维护Cluster中实例的代码大家感兴趣可以点进去看看,主要思想还是比对新老数据,找出新的instance,与挂掉的instance,最后更新 cluster对象里面的存放临时节点的集合 或 存放永久节点的集合。最后调用PushService通知单客户端服务信息发生变更。OK,fine!Nacos服务注册,服务端主要是将服务信息和服务的实例信息保存到两个Map(serviceMap、dataMap)中;启动一个延时5s执行的定时任务做服务剔除/下线;Notifier做普通服务集群实例信息维护,调用PushService通知客户端服务信息发生变更。2)Server端接收心跳请求InstanceController#beat()方法是Nacos Server接收心跳请求的入口:其内部调用InstanceOperator接口的handleBeat()方法做心跳判断;InstanceOperator有两个实现类,我们这里讨论的是服务端如何处理客户端的心跳请求,因此我们看InstanceOperatorServiceImpl类;InstanceOperatorServiceImpl#handleBeat():1、如果服务还没有注册,就先注册;2、接着获取服务信息,校验服务不能为null;3、最后,调用Service#processClientBeat()处理客户端心跳;Service#processClientBeat()中启动一个只执行一次的任务:任务的逻辑体现在ClientBeatProcessor#run()方法中:1、更新实例的最后一次心跳时间,进而决定服务是否需要下线/剔除;2、如果服务之前是不可用的,则设置服务为可用的,并调用PushService通知客户端;服务端对于心跳的处理,主要两件事:维护心跳的最后一次时间,如果服务变为可用状态则通知客户端;另外在服务注册创建Service服务时会启动一个固定延时5S执行的定时任务做服务的剔除,其中以心跳时间为根本逻辑。
一、前言在前面我们分析完了Nacos服务注册、服务发现的原理;当研究完Nacos作为服务配置中心时,是通过定时任务 + 长轮询的方式实现配置信息的准实时动态刷新;突发奇想那么服务实例信息变更,作为服务的消费者如何实时感知?所以有了今天这篇文章(以服务实例新增为例)。因为本篇文章和服务注册、服务发现源码紧密结合,大家可以先参考一下下面这两篇文章:1、图文详述Nacos服务注册源码分析:https://blog.csdn.net/Saintmm/article/details/1219811842、图文详述Nacos服务发现源码分析:https://blog.csdn.net/Saintmm/article/details/122019300PS : Nacos Client版本:1.4.2。二、服务注册、发现、UDP通知交互流程图三、源码分析针对服务注册、服务发现的源码此篇文章一带而过,细节大家可以参考上面提到的两篇文章。1、服务注册在进行服务注册的时候,会做三件事:增加对服务临时实例和永久实例监听的监听器、添加服务实例时添加CHANGE事件到通知器Notifier、由ApplicationContext事件机制处理CHANGE事件并采用UDP的方式通知Nacos Client(服务消费者)。>1 增加服务监听器当一个服务需要向Nacos Server注册自己时,并且之前没有注册过;此时会Nacos Server会创建一个Service,并且添加两个监听器,监听服务临时和永久实例的变更;逻辑体现在ServiceManager#putServiceAndInit()方法中:本篇文章我们以临时服务实例为例: 添加服务监听器即以namespaceId、serviceName和ephemeral为key,Service对象为value将键值对添加到DistroConsistencyServiceImpl类的listeners字段中。>2 给通知器添加CHANGE事件如果服务在Nacos Server中已经存在了,服务注册操作需要给服务添加实例信息,并且将服务实例信息变更事件放入到通知者Notifier的阻塞队列tasks中,以供相应服务的服务消费者(Nacos client)开启UDP推送服务后 将服务实例信息变更事件推送到服务消费者(Nacos client)。逻辑体现在DistroConsistencyServiceImpl#onPut()方法中:1)添加任务实际是将CHANGE类型任务添加到DistroConsistencyServiceImpl的内部类Notifier的阻塞队列tasks中;2)dataStore.put(key, datum)操作会把服务的所有实例信息维护在DataStore的dataMap中,以供服务变更事件监听器使用;>3 采用UDP通知Nacos ClientDistroConsistencyServiceImpl初始化时会初始化notifier,并且在其后置构造方法(@PostConstruct)中会启动notifier任务。在通知者Notifier的run()方法中会死循环从阻塞队列tasks中取任务,做相应的事件处理;处理变更事件时,新的value值从增加服务实例时维护在DataStore中的dataMap中取;最后进入到Service#onChange()方法中;updateIPs()方法主要做两件事:1)会维护服务所在集群的实例信息clusterMap,这里的集群也可以理解为区域Region上划分的几个机房,比如:南京机房、北京机房;2)采用UDP的方式通知服务消费者(Nacos Client),消费的服务提供者实例信息发生变更;调用的UdpPushService#serviceChanged()方法中大有乾坤,开源框架的魅力呀;这其中是我们熟悉的ApplicationContext事件机制,这里进行一个发布事件的操作;其实吧,看到这里的时候我有点懵逼,于是就复习了一下ApplicationContext的事件机制;见博客:Spring ApplicationContext的事件机制是什么?在Nacos中如何应用?我们这里再简要说一下ApplicationContext事件机制在Nacos中的应用。(1)ApplicationContext的事件机制在Nacos中的应用ServiceChangeEvent为要发布的事件;UdpPushService#serviceChanged()方法中会调用applicationContext.publishEvent()发布事件;UdpPushService实现ApplicationListener接口的onApplicationEvent()方法处理事件。总的来说UdpPushService类将ServiceChangeEvent事件通过applicationContext.publishEvent()发布给自己实现的onApplicationEvent()方法进行处理,这里是一个典型的观察者设计模式。(2)处理ServiceChangeEvent1> 最后会通过DatagramSocket发送UDP请求:@Component @SuppressWarnings("PMD.ThreadPoolCreationRule") public class UdpPushService implements ApplicationContextAware, ApplicationListener<ServiceChangeEvent> { @Override public void onApplicationEvent(ServiceChangeEvent event) { // todo 处理业务逻辑 // If upgrade to 2.0.X, do not push for v1. if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) { return; ....... // 发送UDP请求 udpPush(ackEntry); ....... }2> 在处理ServiceChangeEvent事件时,有一个问题?从上面看到现在都没有提到Nacos Client(服务消费者),那么serviceChangeEvent应该发到哪里?对于这个问题我们在下面继续聊。2、服务发现在当前流程中,服务发现主要相当于一个导火索,只有做了服务发现,Nacos Server 才知道要推送服务实例变更信息给哪个Nacos Client。用编程逻辑来说,也就是Nacos Client做服务发现时,需要将自己的IP、Port等信息存储到Nacos Server中,当发生相应服务实例信息变更时,Nacos Server遍历它进行UDP通知。>1 Nacos Client数据存储在服务注册,我们最后聊到会在UdpPushService#onApplicationEvent()方法中处理ServiceChangeEvent,它在判断应该往哪些Nacos Client发送服务实例变更通知时,是通过遍历NamingSubscriberServiceV1Impl的字段clientMap。我们接着看一下做服务发现时是如何填充的clientMap?>2 开启UDP推送服务、增加PushClient在..... 一系列操作之后,具体操作见最开始提到的文章;1)进入到InstanceOperatorServiceImpl#listInstance()方法中:如果UDP推送服务可以开启进行Push操作,则会调用subscriberServiceV1.addClient()增加一个PushClient。2)subscriberServiceV1.addClient()中会先初始化一个PushClient,然后将其放入到clientMap中;对于clientMap而言,整体的交互流程如下图:3、Nacos Client接收UDP通知Nacos Server是通过udpSocket.send()方法发送通知的,感觉上Nacos Client应该是通过udpSocket.receive(方法接收通知,搜一下看看;我们进入到PushReceiver类中,PushReceiver是一个线程类,在其run()方法死循环接收UDP通知,当通知类型是service时,处理本地缓存的服务信息serviceInfoMap;ServiceInfoHolder#processServiceInfo()方法中处理修改本地缓存相对就很简单了,直接调用Map.put()方法更新相应key的value值即可:此处的整体流程如下图:四、总结做服务发现时会做两件事:*1. 会对服务的永久实例和临时实例各注册一个监听器;*2. 服务实例增加时触发一个服务实例信息CHANGE事件,由Notifier通知者通过UDP的方式通知Nacos Client(服务消费者);服务注册时则会告诉Notifer通知者应该通知哪些Nacos Client;Nacos Client接收到UDP通知之后,更新本地缓存serviceInfoMap。
一、Nacos服务发现流程图建议大家自己梳理一下流程,也可以参考:Nacos服务注册源码分析流程图二、找源码入口spring-cloud-commons包中定义了一套服务发现的规范,核心逻辑在DiscoveryClient接口中;集成Spring Cloud实现服务发现的组件都会实现DiscoveryClient接口;nacos-discovery包下的NacosDiscoveryClient类实现DiscoveryClient接口。三、客户端服务发现1、当nacos客户端运⾏起来之后,它只是去做服务注册、配置获取等操作;并不会立即去请求服务信息;2、当第一次请求时候,才会去获取服务,即懒加载机制;1)先从本地缓存serviceInfoMap中获取服务实例信息,获取不到则通过NamingProxy调用Nacos 服务端获取服务实例信息;最后开启定时任务每秒请求服务端 获取实例信息列表进而更新本地缓存serviceInfoMap;// NacosDiscoveryClient#getInstances() public List<ServiceInstance> getInstances(String serviceId) { try { // 通过NacosNamingService获取服务对应的实例信息;点进去 List<Instance> instances = discoveryProperties.namingServiceInstance() .selectInstances(serviceId, true); return hostToServiceInstanceList(instances, serviceId); } catch (Exception e) { throw new RuntimeException( "Can not get hosts from nacos server. serviceId: " + serviceId, e); // NacosNamingService#selectInstances() public List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException { return selectInstances(serviceName, new ArrayList<String>(), healthy); public List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy) throws NacosException { // 默认走订阅模式 return selectInstances(serviceName, clusters, healthy, true); public List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException { // 默认查询DEFAULT_GROUP下的服务实例信息 return selectInstances(serviceName, Constants.DEFAULT_GROUP, clusters, healthy, subscribe); public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException { ServiceInfo serviceInfo; // 默认走订阅模式,即subscribe为TRUE if (subscribe) { serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ",")); } else { serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ",")); return selectInstances(serviceInfo, healthy); }HostReactor#getServiceInfo()方法是真正获取服务实例信息的地方:public ServiceInfo getServiceInfo(final String serviceName, final String clusters) { NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch()); String key = ServiceInfo.getKey(serviceName, clusters); if (failoverReactor.isFailoverSwitch()) { return failoverReactor.getService(key); // 1、从本地缓存serviceInfoMap中获取实例信息 ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters); // 2、如果本地缓存中没有,则走HTTP调用从Nacos服务端获取 if (null == serviceObj) { serviceObj = new ServiceInfo(serviceName, clusters); serviceInfoMap.put(serviceObj.getKey(), serviceObj); updatingMap.put(serviceName, new Object()); updateServiceNow(serviceName, clusters); updatingMap.remove(serviceName); } else if (updatingMap.containsKey(serviceName)) { if (UPDATE_HOLD_INTERVAL > 0) { // hold a moment waiting for update finish synchronized (serviceObj) { try { serviceObj.wait(UPDATE_HOLD_INTERVAL); } catch (InterruptedException e) { NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e); // 3、开启一个定时任务,每隔一秒从Nacos服务端获取最新的服务实例信息,更新到本地缓存seriveInfoMap中 scheduleUpdateIfAbsent(serviceName, clusters); // 4、 从本地缓存serviceInfoMap中获取服务实例信息 return serviceInfoMap.get(serviceObj.getKey()); }1、从本地缓存中获取服务实例信息:private ServiceInfo getServiceInfo0(String serviceName, String clusters) { String key = ServiceInfo.getKey(serviceName, clusters); return serviceInfoMap.get(key); }2、则走HTTP调用从Nacos服务端获取服务实例信息:public void updateServiceNow(String serviceName, String clusters) { ServiceInfo oldService = getServiceInfo0(serviceName, clusters); try { // 通过NamingProxy走HTTP接口调用,获取服务实例信息 String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false); if (StringUtils.isNotEmpty(result)) { // 更新本地缓存serviceInfoMap processServiceJSON(result); } catch (Exception e) { NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e); } finally { if (oldService != null) { synchronized (oldService) { oldService.notifyAll(); }3、开启一个定时任务,每隔一秒从Nacos服务端获取最新的服务实例信息,更新到本地缓存seriveInfoMap中:public void scheduleUpdateIfAbsent(String serviceName, String clusters) { if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) { return; synchronized (futureMap) { if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) { return; // 启动定时任务 ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters)); futureMap.put(ServiceInfo.getKey(serviceName, clusters), future); // 定时任务执行逻辑,UpdateTask#run() public void run() { try { ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters)); if (serviceObj == null) { updateServiceNow(serviceName, clusters); executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS); return; if (serviceObj.getLastRefTime() <= lastRefTime) { updateServiceNow(serviceName, clusters); serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters)); } else { // if serviceName already updated by push, we should not override it // since the push data may be different from pull through force push refreshOnly(serviceName, clusters); // 开启一个定时任务,1s之后执行 executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS); lastRefTime = serviceObj.getLastRefTime(); } catch (Throwable e) { NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e); }查询服务实例列表:public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly) throws NacosException { final Map<String, String> params = new HashMap<String, String>(8); params.put(CommonParams.NAMESPACE_ID, namespaceId); params.put(CommonParams.SERVICE_NAME, serviceName); params.put("clusters", clusters); params.put("udpPort", String.valueOf(udpPort)); params.put("clientIP", NetUtils.localIP()); params.put("healthyOnly", String.valueOf(healthyOnly)); return reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/list", params, HttpMethod.GET); }2)在HostReactor实例化的时候会实例化PushReceiver,进而开启一个线程死循环通过DatagramSocket#receive()监听Nacos服务端中服务实例信息发生变更后的UDP通知。public class PushReceiver implements Runnable { private DatagramSocket udpSocket; public PushReceiver(HostReactor hostReactor) { try { this.hostReactor = hostReactor; udpSocket = new DatagramSocket(); // 启动一个线程 executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName("com.alibaba.nacos.naming.push.receiver"); return thread; executorService.execute(this); } catch (Exception e) { NAMING_LOGGER.error("[NA] init udp socket failed", e); public void run() { while (true) { try { // byte[] is initialized with 0 full filled by default byte[] buffer = new byte[UDP_MSS]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // 监听Nacos服务端服务实例信息变更后的通知 udpSocket.receive(packet); String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim(); NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString()); PushPacket pushPacket = JSON.parseObject(json, PushPacket.class); String ack; if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) { hostReactor.processServiceJSON(pushPacket.data); // send ack to server ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":" + "\"\"}"; } else if ("dump".equals(pushPacket.type)) { // dump data to server ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":" + "\"" + StringUtils.escapeJavaScript(JSON.toJSONString(hostReactor.getServiceInfoMap())) + "\"}"; } else { // do nothing send ack only ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":" + "\"\"}"; udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")), ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress())); } catch (Exception e) { NAMING_LOGGER.error("[NA] error while receiving push data", e); }四、服务端服务发现Nacos服务端的服务发现主要做两件事:1、查询服务实例列表;先从缓存serviceMap中找到service对应的Cluster,再从Cluster的两个Set:persistentInstances、ephemeralInstances获取全量的实例信息;2、将客户端传来的ip、udp端口号加添加到clientMap,进而做服务推送;clientMap属于NamingSubscriberService的实现类NamingSubscriberServiceV1Impl,其key是service name,value是订阅了该服务的客户端列表(ip+端口号)。见naming项目下的 InstanceController类的list()方法:1)获取服务实例列表@GetMapping("/list") @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ) public Object list(HttpServletRequest request) throws Exception { String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); String agent = WebUtils.getUserAgent(request); String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY); String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY); int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0")); boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false")); boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false")); String app = WebUtils.optional(request, "app", StringUtils.EMPTY); String env = WebUtils.optional(request, "env", StringUtils.EMPTY); String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY); Subscriber subscriber = new Subscriber(clientIP + ":" + udpPort, agent, app, clientIP, namespaceId, serviceName, udpPort, clusters); // 进去InstanceOperatorServiceImpl#listInstance()方法获取服务实例列表 return getInstanceOperator().listInstance(namespaceId, serviceName, subscriber, clusters, healthyOnly); //InstanceOperatorServiceImpl#listInstance() public ServiceInfo listInstance(String namespaceId, String serviceName, Subscriber subscriber, String cluster, boolean healthOnly) throws Exception { ClientInfo clientInfo = new ClientInfo(subscriber.getAgent()); String clientIP = subscriber.getIp(); ServiceInfo result = new ServiceInfo(serviceName, cluster); Service service = serviceManager.getService(namespaceId, serviceName); long cacheMillis = switchDomain.getDefaultCacheMillis(); // now try to enable the push try { // 尝试启用推送服务UdpPushService,即服务实例信息发生变更时通过UDP的方式通知Nacos Client if (subscriber.getPort() > 0 && pushService.canEnablePush(subscriber.getAgent())) { subscriberServiceV1.addClient(namespaceId, serviceName, cluster, subscriber.getAgent(), new InetSocketAddress(clientIP, subscriber.getPort()), pushDataSource, StringUtils.EMPTY, StringUtils.EMPTY); cacheMillis = switchDomain.getPushCacheMillis(serviceName); } catch (Exception e) { Loggers.SRV_LOG.error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, subscriber.getPort(), e); cacheMillis = switchDomain.getDefaultCacheMillis(); if (service == null) { if (Loggers.SRV_LOG.isDebugEnabled()) { Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName); result.setCacheMillis(cacheMillis); return result; // 检查服务是否禁用 checkIfDisabled(service); // 这里是获取服务注册信息的关键代码,获取所有永久和临时服务实例 List<com.alibaba.nacos.naming.core.Instance> srvedIps = service .srvIPs(Arrays.asList(StringUtils.split(cluster, StringUtils.COMMA))); // filter ips using selector,选择器过滤服务 if (service.getSelector() != null && StringUtils.isNotBlank(clientIP)) { srvedIps = selectorManager.select(service.getSelector(), clientIP, srvedIps); // 如果找不到服务则返回当前服务 if (CollectionUtils.isEmpty(srvedIps)) { ....... return result; // Service#srvIPs() public List<Instance> srvIPs(List<String> clusters) { if (CollectionUtils.isEmpty(clusters)) { clusters = new ArrayList<>(); clusters.addAll(clusterMap.keySet()); return allIPs(clusters); // Service#allIPs() public List<Instance> allIPs(List<String> clusters) { List<Instance> result = new ArrayList<>(); for (String cluster : clusters) { // 服务注册的时候,会将实例信息写到clusterMap中,现在从其中取 Cluster clusterObj = clusterMap.get(cluster); if (clusterObj == null) { continue; result.addAll(clusterObj.allIPs()); return result; // Cluster#allIPs() public List<Instance> allIPs() { List<Instance> allInstances = new ArrayList<>(); // 获取服务下所有的持久化实例 allInstances.addAll(persistentInstances); // 获取服务下所有的临时实例 allInstances.addAll(ephemeralInstances); return allInstances; }2)采用UDP方式做服务实例推送NamingSubscriberServiceV1Impl#addClient():public void addClient(String namespaceId, String serviceName, String clusters, String agent, InetSocketAddress socketAddr, DataSource dataSource, String tenant, String app) { // 初始化推送客户端实例PushClient PushClient client = new PushClient(namespaceId, serviceName, clusters, agent, socketAddr, dataSource, tenant, app); // 添加推送目标客户端 addClient(client); // 重载方法addClient() public void addClient(PushClient client) { // client is stored by key 'serviceName' because notify event is driven by serviceName change // 客户端由键“ serviceName”存储,因为通知事件由serviceName更改驱动 String serviceKey = UtilsAndCommons.assembleFullServiceName(client.getNamespaceId(), client.getServiceName()); ConcurrentMap<String, PushClient> clients = clientMap.get(serviceKey); // 如果获取不到客户端想调用的ServiceName对应的推送客户端,则新建推送客户端,并缓存 if (clients == null) { clientMap.putIfAbsent(serviceKey, new ConcurrentHashMap<>(1024)); clients = clientMap.get(serviceKey); PushClient oldClient = clients.get(client.toString()); // 存在老的PushClient,则刷新 if (oldClient != null) { oldClient.refresh(); } else { // 否则缓存PushClient PushClient res = clients.putIfAbsent(client.toString(), client); if (res != null) { Loggers.PUSH.warn("client: {} already associated with key {}", res.getAddrStr(), res); Loggers.PUSH.debug("client: {} added for serviceName: {}", client.getAddrStr(), client.getServiceName()); }五、总结客户端:1、优先从本地缓存中获取服务实例信息;2、维护定时任务定时从Nacos服务端获取服务实例信息;服务端:1、返回指定命名空间下内存注册表中所有的永久实例和临时实例给客户端;2、开启一个UDP服务实例信息变更推送服务;
一、问题复现和详细异常今天一个同事在开发时给一个类(AServiceImpl)加了@AllArgsConstructor,希望通过private final 的方式将XxClass 和 XxxClass注入到当前类:@Service @AllArgsConstructor public class AServiceImpl implements AService{ private final XxClass xxClass; private final XxxClass xxxClass; @Value("${my.value:myValue}") private String value; ..... }运行时报错如下:org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1777) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1333) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1287) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1206) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.2.jar:5.3.2] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:923) ~[spring-context-5.3.2.jar:5.3.2] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:588) ~[spring-context-5.3.2.jar:5.3.2] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767) [spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) [spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426) [spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) [spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) [spring-boot-2.4.1.jar:2.4.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) [spring-boot-2.4.1.jar:2.4.1] at com.saint.DemoTestApplication.main(DemoTestApplication.java:10) [classes/:na] Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1777) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1333) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1287) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ... 28 common frames omitted二、原因分析从报错提示可以看出是没有找到NoSuchBeanDefinition;根本原因是因为springioc容器加载bean默认使用无参构造进行初始化,就示例而言,需要用到只有一个入参的构造函数(AClassImpl(XsClass));由于此处使用的是@AllArgsConstructor注解,所以对AClassImpl而言,只有一个构造函数(AClassImpl(XxClass, XxxClass, String)),而构造函数的第三个参数是用@Value注入导入的一个属性值,其并不会存在于Spring容器,所以报错。源码分析从报错信息找到出错的入口:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)这里可以看到获取到的AServiceImpl的构造函数中有三个入参,最后一个入参是String类型;然后顺着报错日志的执行链路,找到这里ConstructorResolver#resolveAutowiredArgument()方法会判断 用于解析指定参数的模板方法 是否支持被自动注入(autowired);非Class类型均不行。抛出异常的源码位置如下:因为采用构造函数注入属性的方式,属性是必须要存在于Spring容器的,所以这里检查参数必须性时会校验不通过(即不会提前return),进而报错;三、解决方案一般可以给AServiceImpl提供一个默认的构造方法来解决,但此处需要注入XxClass 和 XxxClass,所以仅需生成一个只包含XxClass 和 XxxClass两个参数的构造方法即可。因此,我们选择使用@RequiredArgsConstructor注解来替换@AllArgsConstructor注解;@RequiredArgsConstructor 和 @AllArgsConstructor的区别?@RequiredArgsConstructor会将类的每一个final字段或者non-null字段生成一个构造方法@AllArgsConstructor 生成一个包含所有字段的构造方法;@NoArgsConstructor 生成无参的构造方法;使用@RequiredArgsConstructor可以代替@Autowrited注解;而@RequiredArgsConstructor是根据构造器注入的,所以会有循坏依赖的问题。@RequiredArgsConstructor循环依赖问题解决措施有三种方式可以解决:改为使用@Autowired注解做属性方法注入Bean;不过,Spring 从 4.0 开始, 就不推荐使用属性注入模式了,原因在于它可以让我们忽略掉一些代码可能变坏的隐患。使用@RequiredArgsConstructor(onConstructor =@_(@Autowired) ),这样默认都是通过@Autowired注入Bean的;使用@RequiredArgsConstructor(onConstructor_={@Lazy} ),做懒加载处理。
前言以前负责的一个项目,从单体架构往微服务架构迁移时,引入了Consul作为服务配置中心,然后导致所有的异步定时任务(@schedule+@Async)都不执行了;跟源码发现Consul作为服务配置中心时会在client端起一个定时任务线程池(核心线程数和最大线程数均为1)其伦村Consul Server中的服务配置;由于@Async默认使用SpringBoot自带的线程池,而这个线程池已经被Consul创建,并且核心线程数和最大线程数都为1,就导致@Async一直拿不到可用的线程,进而所有的定时任务都没有执行;当时的解决方案:自定义线程池,@Async使用时指定线程池名称,执行异步任务。自定义线程池在@Configuration类中通过@Bean的方式注入一个ThreadPoolTaskExecutor到Spring容器;@Bean("myExecutor") public ThreadPoolTaskExecutor mqInvokeAysncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 线程池中的线程名前缀 executor.setThreadNamePrefix("log-message-"); // 设置线程池关闭的时候需要等待所有任务都完成 才能销毁其他的Bean executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住 executor.setAwaitTerminationSeconds(120); executor.setCorePoolSize(4); executor.setMaxPoolSize(16); // 任务阻塞队列的大小 executor.setQueueCapacity(20000); return executor; }使用自定义线程池1)@Async中使用在使用的方法上面添加@Async(“自定义线程池类beanName”)注解;public class AsyncTest { // 指定使用哪个线程池,不指定则使用spring默认的线程池 @Async("myExecutor") public void executeAsync() { System.out.println("executeAsync"); }2)CompletableFuture中使用当使用CompletableFuture.runAsync()方法异步执行一系列任务时,可以在方法的第二个参数中指定用哪个线程池执行任务;public class ImportOrderLogMsgProducer { @Resource(name = "myExecutor") private ThreadPoolTaskExecutor threadPoolTaskExecutor; public void testAsync(LogMessage logMessage) { CompletableFuture.runAsync(() -> { try { // do something } catch (Exception e) { log.error("... catch:{}", e.getMessage(), e); }, threadPoolTaskExecutor);
一、前言最近在研究OpenFeign源码时,@EnableFeignClients注解中会通过@Import注解导入一个ImportBeanDefinitionRegistrar接口的实现类FeignClientsRegistrar,出于好奇又回头研究了一下SpringBoot启动流程中处理@Import的逻辑,在此记录一下。我们在SpringBoot启动流程系列聊了以下内容:1> 《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息;2> 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段;3> 《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机);4> 《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段。5> 《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析);6> SpringBoot自动装配时做条件装配的原理;7> 源码分析SpringBoot如何内嵌并启动Tomcat服务器的;在《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析);一文中我们聊了在哪里会解析@Import注解,并处理其中自动装配相关的内容(即DeferredImportSelector接口的实现类),本文我们接着讨论如何处理@Import注解导入的ImportBeanDefinitionRegistrar接口实现类。二、ImportBeanDefinitionRegistrar介绍ImportBeanDefinitionRegistrar是spring对外提供的动态注册beanDefinition的接口,并且spring内部大多也用该接口动态注册beanDefinition。ImportBeanDefinitionRegistrar接口的Bean不会直接注册到IOC容器,它的执行时机比较早,发生在SpringBoot启动流程的注册BeanDefinition时;ImportBeanDefinitionRegistrar接口提供了registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)供子类重写;开发者可以直接调用BeanDefinitionRegistry#registerBeanDefinition()方法传入BeanDefinitionName和对应的BeanDefinition对象,直接往Spring临时容器(beanDefinitionMap)中注册。所谓的Spring临时容器是指:在Spring解析类时会将所有符合注册要求的类放到一个临时容器中,后续执行完BeanPostProcessor、initMessageSource、initApplicationEventMulticaster、onRefresh等操作之后,才会从临时容器中取出所有类,真正注入到Spring容器(singletonObjects)中。此外,ImportBeanDefinitionRegistrar 通常和@Import注解配合使用,@Import注解将ImportBeanDefinitionRegistrar的实现类注入到@Import所属根类的ConfigurationClass属性中,在注册跟类的BeanDefinition时,会遍历调用其@Import的所有ImportBeanDefinitionRegistrar接口的 registerBeanDefinitions()方法。三、ImportBeanDefinitionRegistrar在SpringBoot启动流程中的体现?建议大家先把SpringBoot启动流程系列看完,然后再看这里,不然有可能会存在一些困惑。1、处理ImportBeanDefinitionRegistrar的入口在SpringBoot启动流程中,通过ConfigurationClassParser#doProcessConfigurationClass()解析启动类的注解时,会做@Import注解的process操作,即进入到ConfigurationClassParser#processImports()方法;具体代码执行流程参考博文:《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)。另外debug的项目是我们在OpenFeign系列中的案例(SpringCloud之Feign实现声明式客户端负载均衡详细案例)。2、将ImportBeanDefinitionRegistrar添加到所属ConfigClass的一个属性上进入到ConfigurationClassParser#processImports()方法中,就启动类FeignConsumerApplication而言,它直接或间接一共@Import了三个类,分别如下:其中FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,FeignClientsRegistrar类图如下:下面接着看processImports()方法针对ImportBeanDefinitionRegistrar接口的具体处理逻辑;processImports()方法中会判断如果一个类A实现了ImportBeanDefinitionRegistrar接口,会对类A做实例化,并将其存储到当前ConfigClass的一个属性(Map类型的变量importBeanDefinitionRegistrars)中,以在注册ConfigurationClass到Spring容器时,拿出importBeanDefinitionRegistrars中的所有内容,执行每个ImportBeanDefinitionRegistrar的registerBeanDefinitions()方法。就FeignClientsRegistrar而言,会将其添加到启动类FeignConsumerApplication属性中,具体添加过程如下:到这里,@Import的内容解析也就结束了,后面会根据@Import解析出的内容再做处理(即:执行ImportBeanDefinitionRegistrar接口方法)。3、执行ImportBeanDefinitionRegistrar接口方法的时机解析完所有的candidates候选类之后,会进入到ConfigurationClassBeanDefinitionReader#loadBeanDefinitions(Set<ConfigurationClass>)方法对所有的ConfigurationClass做条件装配、属性处理,然后将相应BeanDefinition注册到Spring中。代码执行流程如下:代码逻辑解释:在加载并注册启动类的BeanDefinition时,会将启动类ConfigurationClass下的一些属性进行解析处理,其中就包括对@Import的所有ImportBeanDefinitionRegistrar接口方法进行执行。就FeignClientsRegistrar而言,其实现的ImportBeanDefinitionRegistrar接口方法执行流程如下:这里逻辑就很简单,ImportBeanDefinitionRegistrar接口提供了一个“模板方法”供子类实现。最后走到FeignClientsRegistrar中,也就进入到了开启OpenFeign的入口。四、后续文章本篇文章我们讨论了在SpringBoot启动流程中开启OpenFeign的入口,下一篇文章,我们接着讨论OpenFeign如何扫描所有的FeignClient?如何生成FeignClient的代理类?
@[toc]一、前言在前面的文章我们聊了Spring Boot的整体启动流程、自动装配、条件装配等内容:1> 《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息;2> 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段;3> 《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机);4> 《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段。5> 《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)6> 《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理;在使用springboot搭建一个web应用程序的时候,我们发现不需要自己搭建一个tomcat服务器,只需要引入spring-boot-starter-web,在应用启动时会自动启动嵌入式的tomcat作为服务器。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>注意:SpringBoot版本:2.3.7.RELEASE(博主写博客时最新Spring-boot版本 – 2.6.X代码逻辑几乎一样)本文我们接着讨论Spring Boot 如何内嵌 并 启动 Tomcat的?二、整体执行流程图三、内嵌Tomcat入口 --> onRefresh()从SpringApplication#run()开始往下追,追到AbstractApplicationContext#refresh()方法中,其内部会调用onRefresh()方法,这里负责开始内嵌Tomcat服务器。在开始讨论onRefresh()方法之前,我们先找到当前Web应用的ApplicationContext具体是哪个(即AbstractApplicationContext的子类)?下面结合整个Spring Boot的启动流程,有两点是有迹可循的:1、推断Web应用类型在博文 <《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段> 中我们讨论过SpringApplication的构建过程中会推断Web应用的类型;WebApplicationType.deduceFromClasspath();因为Web应用类型可能在SpringApplication构造后及run方法之前,再通过setWebApplicatioinType(WebApplicationType)方法调整;又在推断Web应用类型的过程中,由于当前Spring应用上下文尚未准备,所以采用检查当前ClassLoader下基准Class的存在性来推断Web应用类型。public enum WebApplicationType { NONE, SERVLET, REACTIVE; private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }; private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet"; private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler"; private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer"; static WebApplicationType deduceFromClasspath() { // 1. 如果`DispatcherHandler`存在,并且`DispatcherServlet`和`ServletContainer`不存在时,Web应用类型为REACTIVE; if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; // 2. 如果`Servlet`和`ConfigurableWebApplicationContext`不存在,则当前应用为非Web引应用,即NONE。 for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; // 3.当Spring WebFlux和Spring Web MVC同时存在时,Web应用依旧是SERVLET。 return WebApplicationType.SERVLET; }WEB 应用类型,一共有三种:NONE,SERVLET,REACTIVE。deduceFromClasspath()方法利用ClassUtils.isPresent(String, ClassLoader)方法依次判断reactive.DispatcherHandler、ConfigurableWebApplicationContext、Servlet、servlet.DispatcherServlet的存在性组合情况,从而判断Web 引用类型,具体逻辑如下:如果DispatcherHandler存在,并且DispatcherServlet和ServletContainer不存在时,即:Spring Boot仅依赖WebFlux时,Web应用类型为REACTIVE;如果Servlet和ConfigurableWebApplicationContext不存在,则当前应用为非Web应用,即NONE。因为这两个API是Spring Web MVC必须的依赖。当Spring WebFlux和Spring Web MVC同时存在时,Web应用类型依旧是SERVLET。2、创建应用上下文在博文 <《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段 > 中 我们讨论过的SpringAppliation准备阶段的第八步会根据上面推断出的Web应用来创建相应的 ApplicationContext应用上下文对象。根据应用类型利用反射创建Spring应用上下文,可以理解为创建一个容器;就SERVLET而言:实例化AnnotationConfigServletWebServerApplicationContext。3、AnnotationConfigServletWebServerApplicationContext的类图AnnotationConfigServletWebServerApplicationContext 继承自 ServletWebServerApplicationContext,ServletWebServerApplicationContext 又间接继承自AbstractApplicationContext,这样再回到AbstractApplicationContext#onRefresh(),我们便知道这里的应用上下文是哪个实例了。4、AbstractApplicationContext#onRefresh()由于AnnotationConfigServletWebServerApplicationContext类中没有重写onRefresh方法,所以从类图的最下方往上找到ServletWebServerApplicationContext#onRefresh()方法。// ServletWebServerApplicationContext#onRefresh() @Override protected void onRefresh() { //创建主题对象,不用在意 super.onRefresh(); try { //开始创建web服务 createWebServer(); catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); }方法逻辑:首先调用父类AbstractApplicationContext的onRefresh()方法,创建一个主题对象(无需特意关注)。接着调用自己的createWebServer()方法创建WebServer。下面接着看createWebServer()方法做了什么?1> createWebServer() --> 创建WebServerprivate void createWebServer() { // 第一次进来,默认webServer 是 null WebServer webServer = this.webServer; // 第一次进行,默认servletContext 是 null ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { // 从BeanFactory中获取ServletWebServerFactory的实现类 ServletWebServerFactory factory = getWebServerFactory(); // 获取servletContextInitializer(getSelfInitializer()方法会初始化Tomcat对象),获取webServer(完成内嵌Tomcat的API调用) // todo 注意getSelfInitializer()返回一个lambdab表达式,其中的内容不会执行,而是在启动TomcatEmbeddedContext时才会执行lambda this.webServer = factory.getWebServer(getSelfInitializer()); getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer)); getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer)); else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); // 根据上下文的配置属性 替换servlet相关的属性资源 initPropertySources(); }方法逻辑:进入到方法的时候,webServer和servletContext均为null。首先从BeanFactory中获取ServletWebServerFactory的实现类;然后根据获取到的ServletWebServerFactory,进而获取Servlet上下文初始化器servletContextInitializer、获取WebServer。在获取servletContextInitializer时,返回的是一个lambda表达式,lambda表达式中的内容(即:初始化Tomcat对象)在启动TomcatEmbeddedContext时才会执行。根据上下文的配置属性 替换servlet相关的属性资源;(1 从BeanFactory中获取ServletWebServerFactory:protected ServletWebServerFactory getWebServerFactory() { // Use bean names so that we don't consider the hierarchy String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing " + "ServletWebServerFactory bean."); if (beanNames.length > 1) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple " + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames)); return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); }getWebServerFactory()方法会从BeanFactory中获取所有ServletWebServerFactory接口的实现类,如果存在多个,则抛异常。ServletWebServerFactory接口有四个主要的实现类:其中默认的 Web 环境就是 TomcatServletWebServerFactory,而UndertowServletWebServerFactory用于响应式编程。本文debug应用时用的正是默认的Web环境 --> TomcatServletWebServerFactory。(2 获取Servlet上下文初始化器servletContextInitializer:返回一个lambda表达式,在后面启动TomcatEmbeddedContext时才会执行lambda。(3 获取WebServer:接着进入到TomcatServletWebServerFactory # getWebServer() 方法:@Override public WebServer getWebServer(ServletContextInitializer... initializers) { // 走进这里时,initializers还没有执行 if (this.disableMBeanRegistry) { Registry.disableRegistry(); // 完成Tomcat的API调用,把需要的对象创建好、参数设置好 Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); // 准备tomcatEmbeddedContext并将其设置到tomcat中,其中会把上面获取到的servletContextInitializer绑定到tomcatEmbeddedContext。 prepareContext(tomcat.getHost(), initializers); // 构建tomcatWebServer return getTomcatWebServer(tomcat); }方法逻辑:首先完成Tomcat的API调用,把需要的对象创建好、参数设置好;而Tomcat有两个核心功能:<处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化>、<加载和管理 Servlet,以及具体处理 Request 请求>。而针对这两个功能,Tomcat 设计了两个核心组件来分别完成这两件事,即:连接器Connector和容器Container(包括:Engine、Host、Context、Wrapper)。所以,其中最重要的两件事是:1> 把连接器 Connector 对象添加到 Tomcat 中;2> 配置容器引擎,configureEngine(tomcat.getEngine());准备tomcatEmbeddedContext并将其设置到tomcat中,其中会把上面获取到的servletContextInitializer绑定到tomcatEmbeddedContext。构建tomcatWebServer。下面我们着重讨论如何构建Tomcat服务的?2> getTomcatWebServer(tomcat) --> 构建Tomcat服务整个构建Tomcat服务的代码执行流程如下:其中牵扯到Tomcat其他组件(StandardServer、StandardService、StandardEngine、MapperListener、Connector)的初始化,整个生命周期流转如下:3> TomcatServletWebServerFactory#initialize()初始化Tomcat服务详细代码如下:private void initialize() throws WebServerException { logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); synchronized (this.monitor) { try { // 将engineName和instanceId用-拼接到一起 addInstanceIdToEngineName(); Context context = findContext(); context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { // Remove service connectors so that protocol binding doesn't // happen when the service is started. // 删除Connectors,以便再启动服务时不发生协议绑定,点进去看一下 removeServiceConnectors(); // 启动服务触发初始化监听器 this.tomcat.start(); // 在主线程中重新抛出失败异常 rethrowDeferredStartupExceptions(); try { ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); catch (NamingException ex) { // Naming is not enabled. Continue // Unlike Jetty, all Tomcat threads are daemon threads. We create a // blocking non-daemon to stop immediate shutdown // 所有的tomcat线程都是守护线程,所以创建一个阻塞非守护线程来避免立即关闭 startDaemonAwaitThread(); catch (Exception ex) { // 出现异常时,停止Tomcat stopSilently(); destroySilently(); throw new WebServerException("Unable to start embedded Tomcat", ex); }4> 执行完createWebServer()方法之后的日志输出从日志输出来看,createWebServer() 方法看似是用来启动web服务的,并没有真正启动 Tomcat,只是通过ServletWebServerFactory 创建了一个 WebServer,初始化了一堆设置(包括:Port、Service、Engine、embeddedWebApplicationContext)。真正的启动发生在AbstractApplicationContext#finishRefresh()中。四、真正启动Tomcat --> finishRefresh()代码整理执行流程如下:WebServerStartStopLifecycle类负责处理WebServer(Tomcat)的启动和关闭;1、启动TomcatWebServerStartStopLifecycle#start()代码执行流程如下:TomcatWebServer#start()详细代码如下:@Override public void start() throws WebServerException { synchronized (this.monitor) { if (this.started) { return; try { // 添加之前移除的connector,绑定service和Connector addPreviouslyRemovedConnectors(); // 获取当前Tomcat绑定的Connector Connector connector = this.tomcat.getConnector(); // 默认会走进去 if (connector != null && this.autoStart) { // 启动时执行延迟加载 performDeferredLoadOnStartup(); // 检查connector启动状态是否为失败,失败抛出异常 checkThatConnectorsHaveStarted(); this.started = true; // Tomcat启动成功之后打印日志 logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '" + getContextPath() + "'"); catch (ConnectorStartFailedException ex) { stopSilently(); throw ex; catch (Exception ex) { PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort()); throw new WebServerException("Unable to start embedded Tomcat server", ex); finally { Context context = findContext(); ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); }方法逻辑:首先添加之前移除的connector,绑定service和Connector;获取到当前Tomcat绑定的Connector,接着进行执行延迟加载启动;然后 检查connector启动状态是否为失败,失败抛出异常,否则打印Tomcat启动成功日志。1)addPreviouslyRemovedConnectors()private void addPreviouslyRemovedConnectors() { Service[] services = this.tomcat.getServer().findServices(); for (Service service : services) { // 从上面移除connector添加的缓存中取出connector Connector[] connectors = this.serviceConnectors.get(service); if (connectors != null) { for (Connector connector : connectors) { // connector添加到tomcat service中 service.addConnector(connector); if (!this.autoStart) { // 如果不是自动启动,则暂停connector stopProtocolHandler(connector); // 添加完成后移除connector this.serviceConnectors.remove(service); }2)performDeferredLoadOnStartup()private void performDeferredLoadOnStartup() { try { for (Container child : this.tomcat.getHost().findChildren()) { if (child instanceof TomcatEmbeddedContext) { // 延迟加载启动 ((TomcatEmbeddedContext) child).deferredLoadOnStartup(); catch (Exception ex) { if (ex instanceof WebServerException) { throw (WebServerException) ex; throw new WebServerException("Unable to start embedded Tomcat connectors", ex); }3)checkThatConnectorsHaveStarted()private void checkThatConnectorsHaveStarted() { checkConnectorHasStarted(this.tomcat.getConnector()); for (Connector connector : this.tomcat.getService().findConnectors()) { checkConnectorHasStarted(connector); } TomcatWebServer#start()方法执行完之后的日志输出: 2、关闭Tomcat在refreshContext()方法中会通过AbstractApplicationContext#registerShutdownHook()方法注册一个shutdownhook线程,当JVM退出时,确保后续Spring应用上下文所管理的Bean能够在标准的Spring生命周期中回调,从而合理的销毁Bean所依赖的资源(即:注册一个关闭webServer的钩子函数,而钩子函数可以完成关闭的功能)。我们知道应用的上下文实例是ServletWebServerApplicationContext,而它重写了其父类AbstractApplicationContext中的doClose()方法,所以进入到ServletWebServerApplicationContext#doClose()方法;发布一个事件之后,调用其父类AbstractApplicationContext#doClose()方法;整体代码执行流程如下:最后进入到WebServerStartStopLifecycle#stop():五、总结在SpringApplication的运行阶段会通过refreshContext()方法进行上下文的刷新操作,其会进入到AbstractApplicationContext#refresh()方法中,进而调用onRefresh()方法内嵌Tomcat,进行Tomcat的初始化,在finishRefresh()方法中进行Tomcat的启动。1> 创建WebServer:从BeanFactory中获取ServletWebServerFactory的实现类TomcatServletWebServerFactory ,然后通过其获取到WebServer;在获取WebServer的同时,初始化相关的Tomcat对象,包括:Connector、Container。2> 启动WebServer:通过生命周期回调的方式将Tomcat和Connector绑定、延时加载启动Connector、启动成功后打印Tomcat启动成功日志。
一、前言针对条件装配我们讨论了如下内容:《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配《SpringBoot系列十二》:如何自定义条件装配(由@ConditionalOnClass推导)《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(万字图文源码分析)(含@ConditionalOnClass原理)《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理《SpringBoot系列十四》:@ConditionalOnBean、@ConditionalOnMissingBean注解居然失效了!在《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理一文中,我们知道了条件装配时是分两阶段(配置类解析、Bean注册)进行的。二、ConfigurationConditionConfigurationCondition接口是Spring4.0提供的注解,位于org.springframework.context.annotation包内,继承自Condition接口;public interface ConfigurationCondition extends Condition { * 返回当前Condition可以被评估的配置阶段 * Return the {@link ConfigurationPhase} in which the condition should be evaluated. ConfigurationPhase getConfigurationPhase(); * Condition应该被评估的各个配置阶段 * The various configuration phases where the condition could be evaluated. enum ConfigurationPhase { * 配置类解析阶段 * The {@link Condition} should be evaluated as a {@code @Configuration} * class is being parsed. * <p>If the condition does not match at this point, the {@code @Configuration} * class will not be added. PARSE_CONFIGURATION, * Bean注册阶段 * The {@link Condition} should be evaluated when adding a regular * (non {@code @Configuration}) bean. The condition will not prevent * {@code @Configuration} classes from being added. * <p>At the time that the condition is evaluated, all {@code @Configuration} * classes will have been parsed. REGISTER_BEAN }ConfigurationCondition中的getConfigurationPhase()方法,用于返回ConfigurationPhase配置阶段(ConfigurationPhase 的枚举);PARSE_CONFIGURATION:表示在配置类解析阶段进行Condition的评估;REGISTER_BEAN:表示在注册Bean阶段进行Condition的评估;1、ConfigurationCondition和Condition的区别?1> Condition评估的时机:ConfigurationCondition中提供配置阶段的概念,其包含两个阶段:PARSE_CONFIGURATION 和 REGISTER_BEAN,使用ConfigurationCondition接口实现类做Condition条件装配时,会判断传入的配置阶段和ConfigurationCondition#getConfigurationPhase()返回的配置阶段是否一致,如果不一致则不进行Condition评估;以此实现更细粒度的条件装配控制。Condition中没有配置阶段的概念,在任何时候都可以使用其进行Condition评估;2> 使用对比:OnBeanCondition实现ConfigurationCondition接口,getConfigurationPhase()返回ConfigurationPhase.REGISTER_BEAN,表示只在注册Bean阶段进行Condition评估,其他阶段ConditionEvaluator#shouldSkip()方法均直接返回false。OnClassCondition实现Condition接口,配置类加载的任何阶段都可以进行条件评估。1)OnBeanCondition1> OnBeanCondition类图:2> OnBeanCondition实现的方法:2)OnClassCondition1> OnBeanCondition类图:2、什么时候用ConfigurationCondition?ConfigurationPhase的作用是控制条件评估(过滤)的时机:是在解析配置类的时候 还是在创建Bean的时候。一般而言ConfigurationCondition多用于 只在注册Bean阶段才进行条件评估的Condition中使用,比如OnBeanCondition。以OnBeanCondition为例,其中的很多条件评估的依据只有在注册Bean阶段才会相对更加完整。
@[TOC]一、前言在上一篇博文(《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理)中我们讨论了@Conditional条件装配的原理。其中会牵扯到各个bean加载到Spring临时容器beanDefinitionNames和manualSingletonNames的顺序,如果对顺序的控制不当会导致@ConditionalOnBean、@ConditionalOnMissingBean注解失效。条件装配的其他文章如下:《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配《SpringBoot系列十二》:如何自定义条件装配(由@ConditionalOnClass推导)《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理(万字图文源码分析)(含@ConditionalOnClass原理)二、@ConditionalOnMissingBean失效场景复现1> 一个被@Component注解和@ConditionalOnMissingBean(ClassC.class)注解标注的ClassA:package com.saint; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.stereotype.Component; * @author Saint @Component @ConditionalOnMissingBean(ClassC.class) public class ClassA { public ClassA() { System.out.println("初始化了ClassA!"); }2> 一个被@Configuration注解标注,其中有一个@Bean方法的ClassB:package com.saint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; * @author Saint @Configuration public class ClassB { public ClassB() { System.out.println("初始化了ClassB!!!"); @Bean public ClassC getClassC() { return new ClassC(); }3> 普通的ClassC对象:package com.saint; * @author Saint public class ClassC { public ClassC() { System.out.println("初始化了ClassC!!"); }期望的效果:如果没有人注册了类型为ClassC的bean对象,就往Spring容器里注册一个类型为ClassA的bean对象;而ClassB对象中通过@Bean方法向Spring容器中注册了类型为ClassC的bean对象;按照正常的逻辑,并不会向Spring容器中注册ClassA对象。实际效果:从控制台的输出来看,实际上ClassA对象被注册到了Spring容器中;三、@ConditionalOnMissingBean失效原因分析结合我们对@ConditionalOnMissingBean的理解,很容易得到表面结论:在解析ClassA中的@ConditionalOnMissingBean(ClassC.class)注解时,ClassC还没有注册到Spring 容器中。为什么会这样呢?我们结合Spring Boot对ConfigurationClass配置类的处理过程来看一下,在博文(《SpringBoot系列十三》:图文精讲@Conditional条件装配实现原理)中我们聊过:SpringBoot对配置类的处理分为两个阶段:配置类解析 和 Bean注册。在配置类解析阶段会将所有被@Component衍生注解标注的类全部添加到Spring临时容器beanDefinitionNames和manualSingletonNames中,而其他的配置类(@Import、@Bean等导入的类)在此阶段完成后会临时存放在ConfigurationClassParser对象的configurationClasses映射(Map<ConfigurationClass, ConfigurationClass>)中,在Bean注册阶段才会把映射里所有的ConfigurationClass转为BeanDefinition注册到BeanFactory的String集合类型的beanDefinitionNames成员中。如下代码块所示:变量configClasses 的数据结构是 LinkedHashSet,所以其排序规则就是先来的放前面。所以在配置类解析阶段,ClassA已经放入到了Spring的临时容器beanDefinitionNames和configurationClasses映射中,ClassB放入到了configurationClasses映射中,ClassC还没有被解析出来。在配置类注册阶段,根据类所在包中的顺序,重新先将ClassA再次根据条件装配结果注入到Spring的临时容器beanDefinitionNames,然后才将ClassB注入到Spring的临时容器,再解析ClassB中的ClassC,并将其注入到Spring容器。从上图可以看出,用户自定义的类 排在 EnableAutoConfiguration自动配置加载的类 的前面;用户自定义的类之间的顺序是按照文件的目录结构从上到下排序,并且无法干预, @Order,Order接口,@AutoConfigureBefore,@AutoConfigureAfter,AutoConfigureOrder 在这里都是无效的;自动装配的类之间 可以使用 @Order,Order接口,@AutoConfigureBefore,@AutoConfigureAfter,AutoConfigureOrder 五种方式去改变加载的顺序。解决方案:结合上述最后注册Bean阶段时,类的排序规则,我们可以将ClassA当做自动装配类,这样在注册bean阶段,ClassA的处理也就处于 在ClassB中处理@Bean方法将ClassC注册到Spring容器中 的处理之后,即ClassA中处理@ConditionalOnMissingBean(ClassC.class)注解时,ClassC对象已经处理完了。1> 修改ClassA类:package com.saint; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Configuration; * @author Saint @Configuration @ConditionalOnMissingBean(ClassC.class) public class ClassA { public ClassA() { System.out.println("初始化了ClassA!"); }2> 增加自动装配配置:3> 注册Bean阶段类的顺序如下:4> 程序运行结果:ClassA没有被注册到Spring容器中。四、总结SpringBoot官网的JavaDoc强烈建议开发人员仅在自动装配中使用Bean Conditions条件注解。因为开发人员需要特别小心BeanDefinition的添加顺序,因为这些条件是依赖与迄今为止哪些bean已经被处理来评估的!下一篇博文:我们详细讨论一下@Conditional条件装配时Condition的处理顺序。
一、@Conditional简介和使用@Conditional注解是从spring4.0版本才有的,其是一个条件装配注解,可以用在任何类型或者方法上面,以指定的条件形式限制bean的创建;即当所有条件都满足的时候,被@Conditional标注的目标才会被spring容器处理。@Conditional本身也是一个父注解,从SpringBoot1.0版本开始派生出了大量的子注解;用于Bean的按需加载。@Conditional注解和其所有子注解必须依托于被@Component衍生注解标注的类,即Spring要能扫描到@Conditional衍生注解所在的类,才能做进一步判断。@Conditional衍生注解可以加在类 或 类的方法上;加在类上表示类的所有方法都做条件装配、加在方法上则表示只有当前方法做条件装配。1、@Conditional源码@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { * All {@link Condition} classes that must {@linkplain Condition#matches match} * in order for the component to be registered. Class<? extends Condition>[] value(); }@Conditional注解只有一个value参数,类型是:Condition类型的数组;而Condition是一个接口,其表示一个条件判断,内部的matches()方法返回true或false;当所有Condition都成立时,@Conditional的条件判断才成立。1)Condition接口@FunctionalInterface public interface Condition { * 判断条件是否匹配 * @param context 条件判断上下文 * @param metadata 注解元数据 * @return 如果条件匹配返回TRUE,否者返回FALSE boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }Condition是一个函数式接口,其内部只有一个matches()方法,用来判断条件是否成立的,方法有2个入参:context:条件上下文,ConditionContext接口类型,用来获取容器中的信息metadata:用来获取被@Conditional标注的对象上的所有注解信息2)ConditionContext接口public interface ConditionContext { * 返回bean定义注册器BeanDefinitionRegistry,通过注册器获取bean定义的各种配置信息 BeanDefinitionRegistry getRegistry(); * 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象 @Nullable ConfigurableListableBeanFactory getBeanFactory(); * 返回当前spring容器的环境配置信息对象 Environment getEnvironment(); * 返回正在使用的资源加载器 ResourceLoader getResourceLoader(); * 返回类加载器 @Nullable ClassLoader getClassLoader(); }ConditionContext接口中提供了一些常用的方法,用于获取spring容器中的各种信息。2、@Conditional衍生注解使用案例上面提到,从SpringBoot1.0版本开始@Conditional派生出了大量的子注解;用于Bean的按需加载。主要包括六大类:Class Conditions、Bean Conditions、Property Conditions、Resource Conditions、Web Application Conditions、SpEL Expression Conditions。见Spring Boot官网:它们的作用:下面分开来看它们是怎么使用的:1)Class Conditions包含两个注解:@ConditionalOnClass 和 @ConditionalOnMissingClass。1> @ConditionalOnClass@ConditionalOnClass注解用于判断其value值中的Class类是否都可以使用类加载器加载到,如果都能,则符合条件装配。@ConditionalOnClass注解中有两个属性:value() 和 name()。@ConditionalOnClass#value() 属性方法提供“类型安全”的保障,避免在开发过程中出现全类名拼写的低级失误。@ConditionalOnClass#name() 多用于三方库或高低版本兼容的场景使用方式(参考源码中的WebClientAutoConfiguration类):@ConditionalOnClass(WebClient.class) @Configuration ClientHttpConnectorAutoConfiguration.class }) public class WebClientAutoConfiguration { }这里表示,只有当WebClient.Class类和ClientHttpConnectorAutoConfiguration.class类都存在时,才会加载WebClientAutoConfiguration到Spring容器。2> @ConditionalOnMissingClass@ConditionalOnMissingClass注解用于判断其value值中的Class类是否都不可以使用类加载器加载到,如果都不能,则符合条件装配。使用方式:@ConditionalOnMissingClass("org.aspectj.weaver.Advice") @Configuration static class ClassProxyingConfiguration { }这里表示,只有当org.aspectj.weaver.Advice类不存在时,才会加载ClassProxyingConfiguration到Spring容器。2)Bean Conditions包含两个注解:@ConditionalOnBean 和 @ConditionalOnMissingBean。SpringBoot官网的JavaDoc强烈建议开发人员仅在自动装配中使用Bean Conditions条件注解。因为开发人员需要特别小心BeanDefinition的添加顺序,因为这些条件是依赖与迄今为止哪些bean已经被处理来评估的!1> @ConditionalOnBean@ConditionalOnBean注解用于判断某些Bean是否都加载到了Spring容器BeanFactory中,如果是的,则符合条件装配。使用方式:@Configuration(proxyBeanMethods = false) @ConditionalOnBean(ConnectionFactory.class) public class JmsAutoConfiguration { }这里表示,只有当ConnectionFactory类已经被加载到Spring容器BeanFactory中时,才会加载JmsAutoConfiguration类到Spring容器中。2> @ConditionalOnMissingBean@ConditionalOnBean注解用于判断某些Bean是否都没有加载到Spring容器BeanFactory中,如果是的,则符合条件装配。使用方式:@Configuration public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public SomeService someService() { return new SomeService(); }这里表示,只有当SomeService类没有被加载到Spring容器BeanFactory中时,才会加载SomeService类到Spring容器中。使用方式2:@Configuration public class MyAutoConfiguration { @Bean(name = "mySomeService") @ConditionalOnMissingBean(name = "mySomeService") public SomeService someService() { return new SomeService(); }这里同样表示,只有当SomeService类没有被加载到Spring容器BeanFactory中时,才会加载SomeService类到Spring容器中。只是针对类注入到Spring容器时的beanName做了一个自定义。使用方式3:@Configuration public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean(type = "com.fasterxml.jackson.databind.ObjectMapper") public SomeService someService() { return new SomeService(); }这里同样表示,只有当Bean类型为com.fasterxml.jackson.databind.ObjectMapper的类没有被加载到Spring容器BeanFactory中时,才会加载SomeService类到Spring容器中。3)Property Conditions(@ConditionalOnProperty)@ConditionalOnProperty注解依赖于Spring环境参数(Spring Environment property)来做条件装配。其使用prefix 和 name属性表明哪个属性应该被检查。如果prefix()不为空,则属性名称为prefix()+name(),否则属性名称为name()默认情况下,匹配存在且不等于false的任何属性。此外可以使用havingValue 和 matchIfMissing 属性创建更高级的检查。havingValue() --> 表示期望的配置属性值,并且禁止使用falsematchIfMissing() --> 用于判断当属性值不存在时是否匹配使用方式1:@Configuration @ConditionalOnProperty(prefix = "formatter", name = "enabled", havingValue = "true") public class ForMatterAutoConfiguration { }这里表示,当Spring Environment的属性 formatter.enabled 为“true"时,ForMatterAutoConfiguration才会被加载到Spring容器。使用方式2:@Configuration @ConditionalOnProperty(prefix = "formatter", name = "enabled", havingValue = "true", matchIfMissing = true) public class ForMatterAutoConfiguration { }这里表示,当属性 formatter.enabled 配置不存在时,同样视作匹配。4)Resource Conditions(@ConditionalOnResource)@ConditionalOnResource通过判断某些资源是否存在来做条件装配。使用方式:@Configuration @ConditionalOnResource(resources = {"classpath:META-INF/build-info.properties"}) public class ForMatterAutoConfiguration { }这里表示,只有当classpath:META-INF/build-info.properties文件资源存在时,ForMatterAutoConfiguration才会被加载到Spring容器。5)Web Application Conditions包含两个注解:@ConditionalOnWebApplication 和 @ConditionalOnNotWebApplication。1> @ConditionalOnWebApplication@ConditionalOnWebApplication用于判断SpringBoot应用的类型是否为指定Web类型(ANY、SERVLET、REACTIVE)。使用方式:@Configuration @ConditionalOnWebApplication(type = Type.SERVLET) public class ForMatterAutoConfiguration { }这里表示,只有当SpringBoot应用类型为SERVLET应用类型时,ForMatterAutoConfiguration才会被加载到Spring容器。2> @ConditionalOnNotWebApplication@ConditionalOnNotWebApplication用于判断SpringBoot应用的类型是否为不为Web应用。使用方式:@Configuration @ConditionalOnNotWebApplication public class ForMatterAutoConfiguration { }这里表示,只有当SpringBoot应用类型不是Web应用类型时,ForMatterAutoConfiguration才会被加载到Spring容器。6)SpEL Expression Conditions(ConditionalOnExpression)@ConditionalOnExpression通过SpEL Expression来做条件装配。使用这种方式做条件装配有个坑:其会导致在上下文刷新处理中很早就初始化了被标注的bean,进而导致bean无法进行后置处理(比如配置属性绑定),其状态可能是不完整的。所以不建议使用。使用方式:@Configuration @ConditionalOnExpression("${formatter.enabled:true} && ${formatter.enabled.dup:true}") public class ForMatterAutoConfiguration { }这里表示,只有当属性formatter.enabled 和 formatter.enabled.dup同时为true时,ForMatterAutoConfiguration才会被加载到Spring容器。7)其他除上述官方提供的六类条件装配注解,还有三四个条件装配的注解,其中@ConditionalOnSingleCandidate需要着重看一下@Conditional扩展注解作用@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者是首选Bean@ConditionalOnJava系统的java版本是否符合要求@ConditionalOnProperty系统中指定的属性是否有指定的值@ConditionalOnJndiJNDI存在指定项
一、前言在博文《SpringBoot系列十一》:精讲如何使用@Conditional系列注解做条件装配中我们讨论了如何使用@Conditional系列注解做条件装配,假如我想自定义条件装配改怎么做呢? 本文就如何自定义条件装配展开讨论。二、@Conditional介绍@Conditional注解是从spring4.0版本才有的,其是一个条件装配注解,可以用在任何类型或者方法上面,以指定的条件形式限制bean的创建;即当所有条件都满足的时候,被@Conditional标注的目标才会被spring容器处理。@Conditional本身也是一个父注解,从SpringBoot1.0版本开始派生出了大量的子注解;用于Bean的按需加载。@Conditional注解和其所有子注解必须依托于被@Component衍生注解标注的类,即Spring要能扫描到@Conditional衍生注解所在的类,才能做进一步判断。@Conditional衍生注解可以加在类 或 类的方法上;加在类上表示类的所有方法都做条件装配、加在方法上则表示只有当前方法做条件装配。1、@Conditional源码@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { * All {@link Condition} classes that must {@linkplain Condition#matches match} * in order for the component to be registered. Class<? extends Condition>[] value(); }@Conditional注解只有一个value参数,类型是:Condition类型的数组;而Condition是一个接口,其表示一个条件判断,内部的matches()方法返回true或false;当所有Condition都成立时,@Conditional的条件判断才成立。1)Condition接口@FunctionalInterface public interface Condition { * 判断条件是否匹配 * @param context 条件判断上下文 * @param metadata 注解元数据 * @return 如果条件匹配返回TRUE,否者返回FALSE boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }Condition是一个函数式接口,其内部只有一个matches()方法,用来判断条件是否成立的,方法有2个入参:context:条件上下文,ConditionContext接口类型,用来获取容器中的信息metadata:用来获取被@Conditional标注的对象上的所有注解信息2)ConditionContext接口public interface ConditionContext { * 返回bean定义注册器BeanDefinitionRegistry,通过注册器获取bean定义的各种配置信息 BeanDefinitionRegistry getRegistry(); * 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象 @Nullable ConfigurableListableBeanFactory getBeanFactory(); * 返回当前spring容器的环境配置信息对象 Environment getEnvironment(); * 返回正在使用的资源加载器 ResourceLoader getResourceLoader(); * 返回类加载器 @Nullable ClassLoader getClassLoader(); }ConditionContext接口中提供了一些常用的方法,用于获取spring容器中的各种信息。三、自定义条件装配先看看@ConditionalOnClass注解是怎么做的?1、@ConditionalOnClass@ConditionalOnClass注解被@Conditional(OnClassCondition.class)注解标注,结合上面@Conditional注解的介绍来看。可以知道@ConditionalOnClass的条件装配判断逻辑依赖于Condition接口的实现类OnClassCondition实现。再看OnClassCondition的类图:理论上只需要OnClassCondition重写Condition接口的matches(ConditionContext, AnnotatedTypeMetadata)方法即可自定义条件装配的判定逻辑。然而OnClassCondition并没有,matches()方法的重写由OnClassCondition的父类SpringBootCondition来完成。下面模仿其自定义一个《系统属性名称与属性值匹配的条件注解@ConditionalOnSystemProperty》。2、自定义条件装配注解@ConditionalOnSystemProperty注解@ConditionalOnSystemProperty注解的作用是获取到 启动jar程序命令行中 的Program arguments属性的名称与属性值做匹配进而决定是否装配类。对于如何在IDEA中设置Program arguments,参考博文:https://blog.csdn.net/Saintmm/article/details/124603343。整体代码目录结构如下:1> 自定义Conditionpackage com.saint.autoconfigure.condition; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.MultiValueMap; import java.util.Objects; * 系统属性值与值匹配条件 * @author Saint public class OnSystemPropertyCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(ConditionalOnSystemProperty.class.getName()); String name = (String) attributes.getFirst("name"); String value = (String) attributes.getFirst("value"); String systemPropertyValue = System.getProperty(name); // 比较系统属性值和方法的值 if (Objects.equals(systemPropertyValue, value)) { System.out.printf("系统属性【名称: %s】找到匹配值:%s \n", name, value); return true; return false; }2> 自定义条件装配注解package com.saint.autoconfigure.condition; import org.springframework.context.annotation.Conditional; import java.lang.annotation.*; * 系统属性名称与属性值匹配条件注解 * @author Saint @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnSystemPropertyCondition.class) public @interface ConditionalOnSystemProperty { * 属性名 String name(); * 属性值 String value(); }3> 定义 使用条件装配注解的配置类package com.saint.config; import com.saint.autoconfigure.condition.ConditionalOnSystemProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; * 条件配置类 * @author Saint @Configuration public class MyConditionalConfiguration { * 当存在key为language,value为Chinese的property属性时,加载当前方法 * @return @ConditionalOnSystemProperty(name = "language", value = "Chinese") @Bean("message") public String chinese() { return "你好,世界!"; * 当存在key为language,value为English的property属性时,加载当前方法 * @return @ConditionalOnSystemProperty(name = "language", value = "English") @Bean("message") public String english() { return "Hello, World"; }4> 在启动类中测试package com.saint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class SaintSpringbootApplication { public static void main(String[] args) { // 设置language属性值,也可以通过--language=English在Program arguments中设置 System.setProperty("language", "English"); ConfigurableApplicationContext context = SpringApplication.run(SaintSpringbootApplication.class, args); // 从Spring上下文中获取beanName为message的类 String message = context.getBean("message", String.class); System.out.println("当前message类型为:" + message); }运行结果输出如下:从结果可以看出,@ConditionalOnSystemProperty注解条件装配成功生效。
@[TOC]一、前言我们前六篇博文,详细讨论了SpringBoot整个启动流程、自动装配。博文如下:1> 《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息;2> 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段;3> 《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机);4> 《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段。5> 《SpringBoot启动流程五》:你确定你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)。6> SpringBoot自动装配机制原理。在《SpringBoot启动流程五》:你确定你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)一文中聊了@EnableAutoConfiguration,@Import注解是在何时被扫描的?所有的自动自动装配类是何时被扫描/加载的?也埋了一个坑:为什么127个自动装配类经过AutoConfigurationImportFilter过滤后只剩23个了?本文我们就这个坑,聊一下Spring自动装配中自动装配类是如何实现条件装配的!!注:Spring Boot版本:2.3.7.RELEASE。二、入口接着上篇博文《SpringBoot启动流程五》:你确定你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)中自动装配入口的代码执行流程图来看;核心在于AutoConfigurationImportSelector的getAutoConfigurationEntry(AnnotationMetadata)方法中。其负责拿到所有自动配置的节点,大致分为六步;第一步,在getCandidateConfigurations()方法中利用Spring Framework工厂机制的加载器SpringFactoriesLoader,通过SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)方法读取所有META-INF/spring.factories资源中@EnableAutoConfiguration所关联的自动装配Class集合。第二步,利用Set不可重复性对自动装配Class集合进行去重,因为自动装配组件存在重复定义的情况;第三步,读取当前配置类所标注的@EnableAutoConfiguration注解的属性exclude和excludeName,并与spring.autoconfigure.exclude配置属性的值 合并为自动装配class排除集合。第四步,校验自动装配Class排除集合的合法性、并排除掉自动装配Class排除集合中的所有Class(不需要自动装配的Class)。第五步,再次过滤后候选自动装配Class集合中不符合条件装配的Class成员;最后一步,触发自动装配的导入事件。第五步> getConfigurationClassFilter().filter(configurations) --> 过滤候选自动装配Class集合中不符合条件装配的Class成员:这里分两步:1)获取所有的Filter首先通过getConfigurationClassFilter()方法从所有META-INF/spring.factories文件中获取所有的自动装配过滤器AutoConfigurationImportFilter的实现类(一共有三个),然后实例化AutoConfigurationImportSelector类的内部类ConfigurationClassFilter,并将获取多的所有AutoConfigurationImportFilter的实现类集合赋值到ConfigurationClassFilter的filters属性中(后面会用到它做条件装配)。2)执行条件装配然后对获取到的所有自动装配类(最干净、最简单的SpringBoot程序有127个)执行过滤操作(条件装配)后,还剩23个自动装配类。那么是如何做的条件装配呢?三、条件装配原理首先遍历在getConfigurationClassFilter()方法中获取到的三个过滤器:OnClassCondition、OnWebApplicationCondition、OnBeanCondition;分别执行他们的match()方法做条件装配,最后将符合条件的结果放入到一个List集合中返回。下面以实际样例来看一下,这三个过滤器是如何对自动装配类做条件装配的?无论是OnClassCondition、OnWebApplicationCondition 还是OnBeanCondition他们都继承自FilteringSpringBootCondition类,并且它们三个之中都没有重写FilteringSpringBootCondition#match(String[], AutoConfigurationMetadata)方法,所以条件装配判定逻辑的入口统一为FilteringSpringBootCondition#match(String[], AutoConfigurationMetadata):其中调用的getOutcomes(String[],AutoConfigurationMetadata)方法里针对不同的FilteringSpringBootCondition子类而实现逻辑不同,下面分开看一下。1、OnClassConditionOnClassCondition#getOutComs()方法代码执行流程如下:resolveOutcomesThreaded()方法源码解释如下:private ConditionOutcome[] resolveOutcomesThreaded(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { // 将自动装配类的条件装配一分为2进行处理, int split = autoConfigurationClasses.length / 2; // 这里会开启一个线程处理前半部分的自动装配类 OutcomesResolver firstHalfResolver = createOutcomesResolver(autoConfigurationClasses, 0, split, autoConfigurationMetadata); OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(autoConfigurationClasses, split, autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader()); // 当前线程处理后半部分的自动装配类 ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes(); // 这里调用thread.join()方法等待上面开启的线程run()执行完毕(即:处理完前半部分自动装配类) ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes(); // 最后将处理结果合并 并 返回。 ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length]; System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length); System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length); return outcomes; }这里挺有意思的,它首先将所有的自动装配类一分为二,前半部分开启一个线程进行处理、后半部分当前线程处理,后半部分处理完之后,调用thread.join()等待thread.join()方法等待上面开启的线程run()执行完毕(即:处理完前半部分自动装配类)。最后合并处理结果并返回。1> 创建线程处理前半部分自动装配类:2> 处理后半部分自动装配类:从这里我们来看条件装配的细节;1)条件装配逻辑判断代码逻辑在OnClassCondition静态内部类StandardOutcomesResolver#resolveOutcomes()方法中:在getOutcomes(String[],int,int,AutoConfigurationMetadata)方法中:遍历传入的所有自动装配类名称;首先获取自动装配类autoConfigurationClass上@ConditionalOnClass注解中的值;如果获取不到,继续下一个循环。获取到相应的类,则通过类加载机制判断其是否存在,如果能加载到则返回。。。。 否则返回null。这里使用类加载机制判断Class是否存在时,不会对Class执行初始化操作。以org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration类为例,细看一下其@ConditionalOnClass条件装配逻辑;JmxAutoConfiguration类上被@ConditionalOnClass({ MBeanExporter.class })注解标注,所以获取到的candidates局部变量值为:MBeanExporter。接着来看如何判断MBeanExporter.class类是否存在,代码执行流程如下:代码流程解析:根据当前ClassLoader使用反射Class.forNam()加载类:如果能加载到,则给最上层的getOutComes(String)方法返回FALSE;如果加载不到返回一个ConditionOutcome对象,内容为:@ConditionalOnClass did not find required class 'org.springframework.jms.core.JmsTemplate'。(JmsTemplate为某自动装配类,是一个变量)如果@ConditionalOnClass中存在多个Class,则for循环判断,只要有一个Class不存在,则直接返回返回一个ConditionOutcome对象,内容为:@ConditionalOnClass did not find required class 'org.springframework.jms.core.JmsTemplate'。(JmsTemplate为某自动装配类,是一个变量)就JmxAutoConfiguration而言,其可以被自动装配。最后返回到条件装配判定逻辑的入口统一FilteringSpringBootCondition#match(String[], AutoConfigurationMetadata)的体现为:就JmxAutoConfiguration而言,其在String[] autoConfigurationClasses数组中的下标位置为63,而返回的boolean[] match数组下标位置63处为TRUE,表示其可以被自动装配。==此外:SpringBoot提供了两个基于Class的条件注解:一个是@ConditionalOnClass(类加载器中存在指定的类),另一个是@ConditionalOnMissingClass(类加载器中不存在指定的类),@ConditionalOnClass条件装配的原理我们知道了,@ConditionalOnMissingClass我们同样也就知道了。区别在于@ConditionalOnMissingClass加载到类之后给最上层的getOutComes(String)方法返回TRUE。==OnClassCondition过滤完,候选的自动装配类还剩26个:继续进入第二个过滤器OnWebApplicationCondition开始第二轮过滤;2、OnWebApplicationConditionOnWebApplicationCondition的执行逻辑和OnClassCondition一样,区别在于:OnClassCondition判断的是@ConditionalOnClass注解,OnWebApplicationCondition判断的是@ConditionalOnWebApplication注解。OnClassCondition依靠类加载机制判断@ConditionalOnClass注解value属性中的Class是否存在;OnWebApplicationCondition同样也是依靠类加载机制,但是判断的内容要少很多,其仅仅判断<ANY、SERVLET、REACTIVE>三种应用类型对应的类是否存在。就org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration而言:其没被@ConditionalOnWebApplication注解标注,所以在getOutcome(String type)方法中直接返回null,否者需要走应用类型的判断逻辑。上图的两个常量如下:// SERVLET应用特有类 private static final String SERVLET_WEB_APPLICATION_CLASS = "org.springframework.web.context.support.GenericWebApplicationContext"; // Reactive应用特有类 private static final String REACTIVE_WEB_APPLICATION_CLASS = "org.springframework.web.reactive.HandlerResult";如果这两个类同时存在,优先走SERVLET应用类型,这个在之前的博文(SpringBoot应用分类?)聊过。最后SpringApplicationAdminJmxAutoConfiguration类可以被自动装配;OnWebApplicationCondition过滤完,候选的自动装配类还剩24个:继续进入第三个过滤器OnBeanCondition开始第三轮过滤;3、OnBeanConditionOnBeanCondition#getOutcomes(String[],AutoConfigurationMetadata)方法执行逻辑如下:遍历所有的(24个)候选自动装配类;首先获取自动装配类autoConfigurationClass上@OnBeanCondition注解中的值;如果获取不到,继续下一个循环。获取到相应的类,则通过类加载机制判断其是否存在,如果能加载到则返回。。。。 否则返回null。这里使用类加载机制判断Class是否存在时,不会对Class执行初始化操作。以org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration类为例,细看一下其@OnBeanCondition条件装配逻辑;CacheAutoConfiguration类上被@ConditionalOnBean(CacheAspectSupport.class)注解标注,所以获取到的onBeanTypes局部变量值为:CacheAspectSupport。接着来看如何判断CacheAspectSupport类的Bean实例是否存在,代码执行流程如下:看到最后我人傻了,卧槽,这和我学的@ConditionalOnBean注解的含义不一样啊,它不是要判断指定的Class是否已经被实例化存在于Spring容器中吗??==但是从代码debug流程上来看它居然也是判断当前类加载器是否可以加载到Class类。==就CacheAutoConfiguration而言,其可以被自动装配。最后返回到条件装配判定逻辑的入口统一FilteringSpringBootCondition#match(String[], AutoConfigurationMetadata)的体现为:就CacheAutoConfiguration而言,其在String[] autoConfigurationClasses数组中的下标位置为4,而返回的boolean[] match数组下标位置4处为TRUE,表示其可以被自动装配。1、验证加载自动装配类时@ConditionalOnBean注解是针对Class而不是Bean!!==但是从代码debug流程上来看@ConditionalOnBean居然也是判断当前类加载器是否可以加载到Class类。 ==虽然代码debug出来了,但我还是不敢相信!所以我要写个demo验证一下!!首先自定义一个自动装配类:TestSaintAutoConfiguration@Configuration @ConditionalOnBean(MyConditionBean.class) public class TestSaintAutoConfiguration { static { System.out.println("TestSaintAutoConfiguration类自动装配了"); }MyConditionBean类上不加任何注解,不让其注册到Spring容器中,仅使其可以在Class.forName时加载到。public class MyConditionBean { public MyConditionBean() { System.out.println("MyConditionBean"); }整体项目目录如下:如何自定义自动装配类,参考博文:SprinBoot自定义自动装配类与xxx-spring-boot-starter。经过上述过滤器过滤之后,我们的自动装配类TestSaintAutoConfiguration通过了过滤,”可以被自动装配“!!!呀,真的可以自动装配了耶!!执行运行程序并将自动装配的信息打出来试试看呢!首先要在application.yml文件中增加配置debug: true,打印自动装配信息。控制台输出如下:卧槽,卧槽,卧槽,不对呀!这里没自动装配TestSaintAutoConfiguration类啊。推测肯定后面又做了某些处理!!!代码一直往上返回,追到最上层获取所有自动装配类的地方 --> ConfigurationClassParser的内部类的processGroupImports()方法中:又进到了ConfigurationClassParser#processConfigurationClass()方法中,你会发现这里ConfigurationPhase是PARSE_CONFIGURATION而不是REGISTER_BEAN也就意味着,这里依旧会跳过条件装配,继续解析流程,啊啊啊啊,到底在哪呢?(此时博主已即将崩溃,但是博主没放弃,又找了好久好久,终于找到了)。回到处理自动装配的核心逻辑ConfigurationClassPostProcessor#processConfigBeanDefinitions(BeanDefinitionRegistry)方法:关于@ConditionalOnBean的处理逻辑,下篇文章<《SpringBoot启动流程七》:@Conditional条件装配实现原理(待补充博文链接)>中再做讨论。下图为我们自定义的自动装配类TestSaintAutoConfiguratioin装配失败的表现:
@[TOC]一、前言我们前四篇博文,详细讨论了SpringBoot整个启动流程。博文如下:1> 《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息;2> 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段;3> 《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机);4> 《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段。在启动流程中涉及SpringBoot的自动装配,虽然在之前我们聊过<SpringBoot自动装配机制原理>,但其中没有聊到@EnableAutoConfiguration,@Import注解是在何时被扫描的,本文就这一部分展开讨论。注:Spring Boot版本:2.3.7.RELEASE。二、入口在Spring应用上下文准备阶段prepareContext()方法将应用的启动类加到Context中。在Spring应用上下文启动阶段,会进入到refreshContext()方法,具体代码执行流程如下:看ServletWebServerApplicationContext的类图:ServletWebServerApplicationContext间接继承自AbstractApplicationContext,所以最终会进入到AbstractApplicationContext#refresh()方法。走到AbstractApplicationContext#refresh()方法便意味着Spring应用上下文进入Spring生命周期,Spring Boot核心特性随之启动,比如:自动装配。三、处理自动装配1、处理自动装配的入口最终进入到PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory,List<BeanFactoryPostProcessor>)方法中,自动装配在其中实现;invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory,List<BeanFactoryPostProcessor>)方法中,传入接收到的入参beanFactory类型为DefaultListableBeanFactory:再看DefaultListableBeanFactory的类图:其实现了BeanDefinitionRegistry接口,所以会进入到if代码块:在进入if代码块之后,会做两个操作:1> 首先,遍历传入的三个BeanFactoryPostProcessor对其做分类;分为常规后置处理器集合regularPostProcessors 和 注册处理器集合registryProcessors;分类之后,regularPostProcessors有一个成员,registryProcessors中有两个成员。final class PostProcessorRegistrationDelegate { public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) { // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the bean factory post-processors apply to them! // Separate between BeanDefinitionRegistryPostProcessors that implement // PriorityOrdered, Ordered, and the rest. List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>(); // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. // 这里只有一个值:org.springframework.context.annotation.internalConfigurationAnnotationProcessor String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { // internalConfigurationAnnotationProcessor实现了PriorityOrdered接口 if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { // 将ConfigurationClassPostProcessor添加到currentRegistryProcessors中 currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); // 对currentRegistryProcessors做一个排序 sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); // 走到这里registryProcessors中有三个对象了 // todo 核心所在 invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); }接着从BeanFactory中获取到BeanDefinitionRegistryPostProcessor的实现类ConfigurationClassPostProcessor,并将其添加到registryProcessors中;此时registryProcessors中有三个成员:SharedMetadataReaderFactoryContextInitializer的静态内部类CachingMetadataReaderFactoryPostProcessor;ConfigurationWarningsApplicationContextInitializer的静态内部类ConfigurationWarningsPostProcessor;ConfigurationClassPostProcessor;2> 其次,执行当前注册处理器ConfigurationClassPostProcessor;代码执行流程如下:由于postProcessors中只有一个成员ConfigurationClassPostProcessor,进入到ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry(BeanDefinitionRegistry)方法。从ConfigurationClassPostProcessor#processConfigBeanDefinitions(BeanDefinitionRegistry)方法开始真正进入到处理自动装配的核心逻辑。2、处理自动装配内容1)找启动类,构建ConfigurationClassParser解析器准备解析启动类首先从DefaultLisableBeanFactory中获取所有已经注册的BeanDefinition名称;candidateNames中包含了我们的启动类,此外还有6个internalXxx类;然后遍历找到启动类,将其加到configCandidates集合中。找到启动类(saintSpringBootApplicatioin)之后,构建一个配置类解析器ConfigurationClassParser,其中包括ComponentScanAnnotationParser、ConditionEvaluator,分别用于包扫描和条件装配;接着调用ConfigurationClassParser#parse()方法开始解析启动类进行应用程序的启动。2)解析启动类以ConfigurationClassParser#parse()方法为入口,部分代码执行流程如下:其中在做条件装配时,有个点需要注意一下:ConfigurationCondition接口内部的枚举类ConfigurationPhase中有两个值PARSE_CONFIGURATION、REGISTER_BEAN,分别表示:在类解析阶段做条件装配、在类注册阶段做条件装配。if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; }1> 继续看获取SourceClass的代码逻辑:其中会校验启动类上@SpringBootApplication注解的合法性,然后将启动类和其注解元数据AnnotationMetadata封装到SourceClass中返回。如果获取不到SourceClass,则不会执行配置类(启动类)的处理。2> 获取到SourceClass之后,处理配置类和SourceClass:ConfigurationClassParser#doProcessConfigurationClass(ConfigurationClass,SourceClassPredicate<String>)方法中会处理如下内容:如果启动类configClass被@Component的衍生注解(递归注解的父注解可以找到@Component)标注,则首先递归处理所有成员(嵌套)类:即 configClass类内部如果找到成员类,会递归调用doProcessConfigurationClass()方法处理所有成员类。解析启动类中所有的@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean注解。具体代码如下:class ConfigurationClassParser { @Nullable protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { // 启动类configClass被@Component的衍生注解(递归注解的父注解可以找到@Component)标注 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // 首先递归处理所有成员(嵌套)类:configClass类内部如果找到成员类,会递归调用doProcessConfigurationClass()方法处理所有成员类。 processMemberClasses(configClass, sourceClass, filter); // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); // Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { // componentScan中包含11个成员,对应于@ComponentScan中11个属性 for (AnnotationAttributes componentScan : componentScans) { // 处理@ComponentScan 中的属性,返回所有派生的@Component注解标注的类,然后立即进行扫描 // 此处会找到basePackages,其默认为启动类所在的目录 Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed // TODO 实际业务中这里定义了多个派生@Component注解标注的类,这里就会循环多少次 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); // Process any @Import annotations // getImports()方法从启动类中获取所有的@Import注解的内容 processImports(configClass, sourceClass, getImports(sourceClass), filter, true); // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); // Process default methods on interfaces processInterfaces(configClass, sourceClass); // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); // No superclass -> processing is complete return null; }就一个最简单、最干净的SpringBoot程序来看,其中没有@PropertySource、@ImportResource注解,平时工程中也很少使用。所以本文我们着重看@ComponentScan、@Import两个注解的处理流程(也就是我们自动装配的核心所在)。3)解析启动类中的@ComponentScan注解// Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { // componentScan中包含11个成员,对应于@ComponentScan中11个属性 for (AnnotationAttributes componentScan : componentScans) { // 处理@ComponentScan 中的属性,返回所有派生的@Component注解标注的类,然后立即进行扫描 // 此处会找到basePackages,其默认为启动类所在的目录 Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed // TODO 实际业务中这里定义了多个派生@Component注解标注的类,这里就会循环多少次 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); }首先获取启动类@SpringBootApplication注解中的11个属性,然后调用ComponentScanAnnotationParser#parse()方法处理@SpringBootApplication注解中的注解 并 设置到类路径BeanDefinition扫描器ClassPathBeanDefinitionScanner的相应属性中。class ComponentScanAnnotationParser { public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator"); boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass); scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator : BeanUtils.instantiateClass(generatorClass)); ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy"); if (scopedProxyMode != ScopedProxyMode.DEFAULT) { scanner.setScopedProxyMode(scopedProxyMode); else { Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver"); scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); scanner.setResourcePattern(componentScan.getString("resourcePattern")); for (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray("includeFilters")) { List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, this.environment, this.resourceLoader, this.registry); for (TypeFilter typeFilter : typeFilters) { scanner.addIncludeFilter(typeFilter); for (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray("excludeFilters")) { List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, this.environment, this.resourceLoader, this.registry); for (TypeFilter typeFilter : typeFilters) { scanner.addExcludeFilter(typeFilter); boolean lazyInit = componentScan.getBoolean("lazyInit"); if (lazyInit) { scanner.getBeanDefinitionDefaults().setLazyInit(true); Set<String> basePackages = new LinkedHashSet<>(); String[] basePackagesArray = componentScan.getStringArray("basePackages"); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); // 默认会走到这里 if (basePackages.isEmpty()) { // 默认,basePackages为启动类所在的目录。eg:启动类为com.saint.SaintSpringBootApplication,basePackages为:com.saint basePackages.add(ClassUtils.getPackageName(declaringClass)); scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { @Override protected boolean matchClassName(String className) { return declaringClass.equals(className); return scanner.doScan(StringUtils.toStringArray(basePackages)); }basePackages属性为@ComponentScan注解的默认扫描包路径,如果没指定该属性,则会将启动类所在的包作为默认值 赋值basePackages属性上(以启动类SaintSpringBootApplication为例,其默认扫包路径为:com.saint)。给ClassPathBeanDefinitionScanner制定完所有属性之后,会调用其doScan(String...)方法扫描basePackages目录下的所有标注了@Component衍生注解(比如:@Controller、@Service、@Repository)的类。具体代码执行流程如下:获取并注册完所有的@Component衍生类之后,在递归对这些类做解析。4)解析启动类中的@Import注解在此之前,我聊SpringBoot自动装配都是说,@EnableAutoConfiguration注解中通过@Import注解导入了AutoConfigurationImportSelector.class,@EnableAutoConfiguration注解中的@AutoConfigurationPackage中通过@Import注解导入了AutoConfigurationPackages.Registrar.class类,但我并不知道这里的@Import是在何时处理的!!这里我们就看一下针对@Import注解是怎么处理的。processImports(configClass, sourceClass, getImports(sourceClass), filter, true);这里分两步,首先通过getImports()方法获取启动类中的@Import注解,然后再通过processImports()方法处理所有的@Import注解。1> getImports()方法获取启动类中所有的@Import注解:具体递归流程如下:最终获取的@import注解有两个:2> processImports()方法处理获取到的启动类中所有的@Import注解:先处理AutoConfigurationPackages.Registrar.class类,再处理AutoConfigurationImportSelector类;对AutoConfigurationPackages.Registrar.class类的处理比较简单,利用反射将其实例化之后,添加到启动类的importBeanDefinitionRegistrars属性中。由于AutoConfigurationImportSelector实现了DeferredImportSelector接口,所以会对AutoConfigurationImportSelector进行一个处理:将AutoConfigurationImportSelector封装为DeferredImportSelectorHolder对象,然后添加到ConfigurationClassParser类的deferredImportSelectors属性中(供后面处理@Import内容)。5)处理所有的自动装配类最后回到ConfigurationClassParser#parse()方法中:代码执行流程如下:最终进入到AutoConfigurationImportSelector#getAutoConfigurationEntry(AnnotationMetadata)方法,这里的代码逻辑相信大家嘎嘎眼熟,在SpringBoot自动装配机制原理一文中我们聊过。AutoConfigurationImportSelector#getAutoConfigurationEntry(AnnotationMetadata)方法源代码如下:protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; // 1. 获取@EnableAutoConfiguration标注类的元信息 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 2. 返回自动装配类的候选类名集合 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 3. 移除重复对象,因为 自动装配组件存在重复定义的情况 configurations = removeDuplicates(configurations); // 4. 自动装配组件的排除名单 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 5.1. 检查自动装配Class排除集合的合法性 checkExcludedClasses(configurations, exclusions); // 5.2 排除掉不需要自动装配的Class configurations.removeAll(exclusions); // 6. 进一步过滤 configurations = getConfigurationClassFilter().filter(configurations); // 7. 触发自动装配的导入事件,事件包括候选的装配组件类名单和排除名单。 fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }其负责拿到所有自动配置的节点,大致分为六步;第一步,在getCandidateConfigurations()方法中利用Spring Framework工厂机制的加载器SpringFactoriesLoader,通过SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)方法读取所有META-INF/spring.factories资源中@EnableAutoConfiguration所关联的自动装配Class集合。第二步,利用Set不可重复性对自动装配Class集合进行去重,因为自动装配组件存在重复定义的情况;第三步,读取当前配置类所标注的@EnableAutoConfiguration注解的属性exclude和excludeName,并与spring.autoconfigure.exclude配置属性的值 合并为自动装配class排除集合。第四步,校验自动装配Class排除集合的合法性、并排除掉自动装配Class排除集合中的所有Class(不需要自动装配的Class)。第五步,再次过滤后候选自动装配Class集合中不符合条件装配的Class成员;最后一步,触发自动装配的导入事件。phase1> getCandidateConfigurations() --> 获取自动装配类:代码执行流程如下:getSpringFactoriesLoaderFactoryClass()方法返回我们熟悉的EnableAutoConfiguration注解类;紧接着,SpringFactoriesLoader.loadFactoryNames(Class<?>, ClassLoader)方法会获取所有META-INF/Spring.factories的配置文件,进而获取到所有的自动装配类;loadFactoryNames()原理如下:搜索指定ClassLoader下所有的META-INF/spring.fatories资源内容;将搜索到的资源内容作为Properties文件读取,合并为一个Key为接口的全类名、Value为实现类全类名 列表的Map,作为方法的返回值;最后从上一步返回的Map中查找并返回方法指定类型 对应的实现类全类名列表。loadFactoryNames()方法源码解释如下:private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 先从缓存中获取 MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { * 查找所有我们依赖的jar包,并找到对应有META-INF/spring.factories⽂件,然后获取⽂件中的内容 * 第一次循环:file:/.../org/springframework/spring-beans/5.2.12.RELEASE/spring-beans-5.2.12.RELEASE.jar!/META-INF/spring.factories * 第二次循环:file:/.../org/springframework/boot/spring-boot/2.3.7.RELEASE/spring-boot-2.3.7.RELEASE.jar!/META-INF/spring.factories * 第三次循环:file:/../org/springframework/boot/spring-boot-autoconfigure/2.3.7.RELEASE/spring-boot-autoconfigure-2.3.7.RELEASE.jar!/META-INF/spring.factories URL url = urls.nextElement(); // 获取资源 UrlResource resource = new UrlResource(url); // 获取资源的内容 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); cache.put(classLoader, result); return result; catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }第一次扫描:第二次扫描:第三次扫描:phase5> getConfigurationClassFilter().filter(configurations) --> 过滤候选自动装配Class集合中不符合条件装配的Class成员:这里分两步:第一步:获取所有的Filter首先通过getConfigurationClassFilter()方法从所有META-INF/spring.factories文件中获取所有的自动装配过滤器AutoConfigurationImportFilter的实现类(一共有三个),然后实例化AutoConfigurationImportSelector类的内部类ConfigurationClassFilter,并将获取多的所有AutoConfigurationImportFilter的实现类集合赋值到ConfigurationClassFilter的filters属性中(后面会用到它做条件装配)。第二步:执行条件装配然后对获取到的所有自动装配类(最干净、最简单的SpringBoot程序有127个)执行过滤操作(条件装配)后,还剩23个自动装配类。关于此处为什么127个自动装配类经过AutoConfigurationImportFilter过滤后只剩23个了,且听下回分解《SpringBoot自动装配中的条件装配》。后面就一路返回返回返回!!!!
一、前言上一篇博文(《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段)我们讨论了Spring应用上下文(ConfigurableApplicationContext)运行前的准备工作。本篇博文,我们把Spring应用上下文启动阶段、和启动后阶段做一个讨论。注:Spring Boot版本:2.3.7.RELEASE。二、Spring应用上下文启动阶段本阶段的执行有refreshContext(ConfigurableApplicationContext)方法实现,具体代码执行流程如下:看ServletWebServerApplicationContext的类图:ServletWebServerApplicationContext间接继承自AbstractApplicationContext,所以最终会进入到AbstractApplicationContext#refresh()方法。1、AbstractApplicationContext#refresh()方法走到这里,意味着Spring应用上下文进入Spring生命周期,Spring Boot核心特性随之启动,如:自动装配、嵌入式容器启动Production-Ready特性。其中的invokeBeanFactoryPostProcessors()会执行三个BeanFactoryPostProcessor,分别为:SharedMetadataReaderFactoryContextInitializer、ConfigurationWarningsPostProcessor、ConfigFileApplicationListener。这其中具体的执行逻辑,博主放在下一篇博文“SpringBoot自动装配原理”中讨论。三、Spring应用上下文启动后阶段当Spring应用上下文刷新操作之后,接着执行afterRefresh(ConfigurableApplicationContext,ApplicationArguments)方法,进入ApplicationContext启动后阶段。然而,实际上,SpringApplication#afterRefresh(ConfigurableApplicationContext,ApplicationArguments)方法并未给Spring应用上下文启动后阶段提供实现,而是交给开发人员自行扩展,所以这里没有什么讨论的意义。在执行完完afterRefresh()方法之后,还会执行五步操作:计时监听器停止并统计任务执行信息、输出日志记录执行主类名和时间信息、发布应用上下文已刷新但未运行程序事件ApplicationStartedEvent、执行所有的Runner运行器、发布应用上下文就绪事件ApplicationReadyEvent。1、计时监听器停止并统计任务执行信息stopWatch.stop();SpringBoot应用的启动时间为:StopWatch.stop()的当前时间 - StopWatch.start()的当前时间,默认统计的任务执行数量为1。2、输出日志记录执行主类名和时间信息这里通过getStartedMessage()方法获取程序启动后需要打印的信息,包括:启动类名称、程序启动需要的时间。private CharSequence getStartedMessage(StopWatch stopWatch) { StringBuilder message = new StringBuilder(); message.append("Started "); appendApplicationName(message); message.append(" in "); message.append(stopWatch.getTotalTimeMillis() / 1000.0); message.append(" seconds"); try { double uptime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0; message.append(" (JVM running for ").append(uptime).append(")"); catch (Throwable ex) { // No JVM time available return message; }Console日志输出如下:3、发布Spring应用上下文已刷新但未启动事件listeners.started(context);当Spring应用上下文刷新操作完成之后,事件ApplicationStartedEvent被EventPublishingRunListener广播。SpringBoot事件监听器:BackgroundPreinitializer、DelegatingApplicationListener监听到事件,但它们什么都不会做。除此之后,还会发布一个AvailabilityChangeEvent事件,状态:ReadinessState.CORRECT,表示应用已处于活动状态。4、执行所有的Runner运行器执行所有的ApplicationRunner和CommandLineRunner 两种运行器,默认情况下是没有运行器的,这里也是留给开发人员自己扩展的。5、发布应用上下文就绪事件ApplicationReadyEventlisteners.running(context);运行完所有的ApplicationRunner和CommandLineRunner之后,事件ApplicationReadyEvent被EventPublishingRunListener广播。SpringBoot事件监听器:SpringApplicationAdminMXBeanRegister、BackgroundPreinitializer、DelegatingApplicationListener监听到事件。SpringApplicationAdminMXBeanRegister将自己的状态设置为ready,可以对外提供服务。BackgroundPreinitializer中调用CountDownLatch.await()等待后台功能的初始化完成。DelegatingApplicationListener则什么都不做。除此之后,还会发布一个AvailabilityChangeEvent事件,状态:ReadinessState.ACCEPTING_TRAFFIC,表示应用可以开始准备接收请求了。四、Spring启动流程完结至此,Spring Boot的启动流程全部总结完毕,下一篇博文以汇总篇的方式,将SpringBoot启动流程文章整合到一起。后续会出一些博文,比如:SpringBoot如何集成远程配置中心、SpringBoot中配置文件是何时加载的、SpringBoot事件和事件监听器在整个SpringBoot启动流程中具体是如何运作的?(具体到每个事件对应的每个事件监听器都做了什么)、SpringBoot自动装配在启动流程中如何体现、SpringBoot如何内嵌Tomcat容器 等等......敬请期待!!!
@[toc]一、前言上文聊了 SpringBoot中SpringApplication是如何构建的(《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段)?从这篇文章开始,进入到SpringApplication的运行阶段(核心过程),我们分三个部分来讨论,分别为:SpringApplication准备阶段、ApplicationContext启动阶段、ApplicationContext启动后阶段。其中SpringApplication的准备阶段是从run(String...)方法调用开始 到 refreshContext(ConfigurableApplicationContext调用前)。注:Spring Boot版本:2.3.7二、SpringApplication准备阶段除初始化StopWatch等少数无足轻重的对象外,该过程会依次准备核心对象:SpringApplicationRunListeners、ApplicationArguments、ConfigurableEnvironment、Banner、ConfigurableApplicationContext 和 SpringBootExceptionReporter集合。其中ConfigurableApplicationContext 可能我们最熟悉,为了加深对它们的理解,下面逐一讨论。我在看源码的时候喜欢将各个逻辑按步骤,一步步拆分再汇总。针对Spring Boot运行阶段我大致分为十五步,其中SpringApplication准备阶段占了十步。整体流程如下:1、准备一些无伤大雅的对象在整体流程图的第一步、第二步、第六步准备了一些无伤大雅的对象,比如:开启计时器StopWatch、设置一些系统属性。1)第一步:开启计时器StopWatch第一步只是开启计时器,并设置程序启动的开始时间StopWatch stopWatch = new StopWatch(); stopWatch.start();代码的整体调用如下,其中taskName默认为空字符串"",记录程序启动的时间为运行stopWatch.start()方法的当前时间。就整个程序的启动时间而言,其实并不精确,因为构建SpringApplication的时间并没有算进去,其中也包含了一次从spring.factories文件中读取信息的IO操作。2)第二步:设置系统属性java.awt.headless第二步只是设置一个系统属性java.awt.headless,默认为true;用于运行headless服务器,进行简单的图像处理;此外,其多用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole,需要将该值设置为true。configureHeadlessProperty();整体代码执行流程:如果设置了java.awt.headless参数,则将其赋值给java.awt.headless系统属性,否者将true赋值给java.awt.headless系统属性。private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";3)第六步:设置系统属性spring.beaninfo.ignore第六步也只是设置一个系统属性spring.beaninfo.ignore,保证某些bean不会添加到准备的环境中。configureIgnoreBeanInfo(environment);整体代码执行流程:默认设置spring.beaninfo.ignore系统属性为true(一般不会该这个,无需关注)下面,我们进入一些核心对象、和核心执行流程的讨论。2、第三步:加载运行时监听器SpringApplicationRunListeners加载运行时监听器SpringApplicationRunListeners主要做两个操作:从所有依赖jar包的META-INF/spring.factories文件中获取SpringApplicationRunListener接口所有的实现类。将获取到的实现类通过构造函数赋值到SpringApplicationRunListeners类的List类型的成员变量listeners上。SpringApplicationRunListeners listeners = getRunListeners(args);从代码逻辑上来看,SpringApplicationRunListeners是由getRunListeners(String[])方法创建的。其中SpringApplicationRunListener 属于 组合模式的实现,其内部关联了SpringApplicationRunListner集合。而getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)方法只会获取到一个EventPublishingRunListeners对象,此对象会贯穿整个应用程序启动的过程,用于发布各种事件给到各个Spring事件监听器。针对getSpringFactoriesInstance()方法是如何从spring.factories文件中获取到执行SpringApplicationRunListener接口的所有实现类的,我们在博文:《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息中详细聊过;1)浅谈SpringApplicationRunListner上面说道,我们会把所有SpringApplicationRunListner的实现类赋值到SpringApplicationRunListeners类的List类型的成员变量listeners上。那么SpringApplicationRunListner有什么用?SpringApplicationRunListner是Spring Boot应用运行时监听器,而不是Spring Boot事件监听器;其监听方法被SpringApplicationRunListeners阶段性的执行,在SpringApplication的运行阶段涉及的方法如下:具体每个方法对应哪些Spring Boot事件、哪些Spring Boot事件监听器会执行,放在<SpringBoot事件和事件监听器在整个SpringBoot启动流程中具体是如何运作的?(具体到每个事件对应的每个事件监听器都做了什么)>一文中详细讨论,敬请期待。后期写完自动补充到这里(todo)。2)第三.2步:发布应用启动事件ApplicationStartingEvent在加载完运行时监听器SpringApplicationRunListeners之后,紧接着会通过其发布一个starting事件;按F7 debug往里跟:这里可以注意到,starting()方法中直接遍历this.listeners,而listeners是在初始化运行时监听器SpringApplicationRunListeners赋值的,并且其中只有一个对象:EventPublishingRunListeners。所以我们接下来进入EventPublishingRunListeners#starting()方法。从multicastEvent的命名推测,EventPublishingRunListeners通过其内部的initialMulticaster成员广播事件。1> 那么initialMulticaster成员是什么初始化的?在初始化EventPublishingRunListener(即在调用EventPublishingRunListener构造函数)的同时会初始化initialMulticaster,并将SpringApplication中的11个Spring事件监听器添加到initialMulticaster中。再看SimpleApplicationEventMulticaster的类图:其继承自AbstractApplicationEventMulticaster,而其自身并没有addApplicationListener(ApplicationListener)方法,但其父类AbstractApplicationEventMulticaster有,所以进入到AbstractApplicationEventMulticaster#addApplicationListener(ApplicationListener)方法:方法中先对defaultRetriever成员变量加锁,保证可以线程安全的操作defaultRetriever变量,其次将传入的Spring事件监听器listener添加到defaultRetriever对象的List类型的applicationListeners成员中。public abstract class AbstractApplicationEventMulticaster implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware { private final DefaultListenerRetriever defaultRetriever = new DefaultListenerRetriever(); private class DefaultListenerRetriever { public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>(); }2> 回到this.initialMulticaster.multicastEvent()继续看是如何发布事件的?EventPublishingRunListener将Spring事件的发布委托给它SimpleApplicationEventMulticaster类型的成员initialMulticaster。即:SimpleApplicationEventMulticaster出自 Spring Framework,其中关联了ApplicationListener,并负责广播ApplicationEvent。3> ApplicationEvent对应哪些ApplicationListener?以ApplicationStartingEvent事件为例,发布这个事件之后,应该通知哪些Spring事件监听器ApplicationListener?AbstractApplicationEventMulticaster#getApplicationListeners(ApplicationEvent,ResolvableType)方法给了我们答案。进入到retrieveApplicationListeners()方法,主要逻辑如下:对AbstractApplicationEventMulticaster类的defaultRetriever对象加锁,防止期间别的线程对defaultRetriever对象修改。加锁成功后,将defaultRetriever.applicationListeners赋值到listeners变量上。而defaultRetriever.applicationListeners我们在上文中提到过:其是在EventPublishingRunListener初始化的时候赋值的,里面包含11个监听器。遍历listeners,使用supportsEvent()方法判断每个监听器是否可以监听当前事件,将可以监听当前事件的监听器添加到allListeners List集合中,排序后返回。我们再看一下supportsEvent()方法是如何判断某个Spring事件监听器是否可以处理某个Spring事件的?以这里的ApplicationStartingEvent事件和CloudFoundryVcapEnvironmentPostProcessor事件监听器为例;<1> 首先将Spring事件监听器封装为GenericApplicationListenerAdapter对象,然后调用GenericApplicationListenerAdapter对象的supportsSourceType(Class)方法判断是否支持监听传入的事件类型。<2> 如果当前事件监听器的类型是SmartApplicationListener,则直接调用当前事件监听器的supportsEventType()方法,否则进一步调用declaredEventType.isAssignableFrom()方法判断。看到这里也就够了,大家不要再深入了。里面太细了,博主会放在<SpringBoot事件和事件监听器在整个SpringBoot启动流程中具体是如何运作的?(具体到每个事件对应的每个事件监听器都做了什么)>一文中详细讨论,敬请期待。后期写完自动补充到这里(todo)。后续文章中牵扯到的Spring事件细节都放在另一博文中详细讨论,次博文只给出结论。==说了这么多,发布应用启动事件ApplicationStartingEvent之后,其实只有LoggingApplicationListener 和 BackgroundPreinitializer做了事情;== 其中:LoggingApplicationListener 中获取了日志系统loggingSystem,并做了日志系统的beforeInitialize()操作。BackgroundPreinitializer 中做了一些后台功能的初始化,比如:转换服务、校验服务、Jackson、Charset字符集初始化。3、第四步:加载并解析命令行的参数到ApplicationArguments对象中当执行完SpringApplicationRunListeners#starting()方法后,SpringApplication进入到装配ApplicationArguments逻辑:ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);ApplicationArguments的实现类DefaultApplicationArguments的底层实现是基于Spring Framework中的命令行配置源SimpleCommandLinePropertySource。代码整体执行流程如下:SimpleCommandLinePropertySource将命令行参数分为两组,分别为:“选项参数”,选项参数必须以“--”为前缀。“非选项参数”,非选项参数是未包含“--”前缀的命令行参数。而命令行参数的解析由SimpleCommandLineArgsParser来完成,具体体现在其parse()方法中:class SimpleCommandLineArgsParser { * Parse the given {@code String} array based on the rules described {@linkplain * SimpleCommandLineArgsParser above}, returning a fully-populated * {@link CommandLineArgs} object. * @param args command line arguments, typically from a {@code main()} method public CommandLineArgs parse(String... args) { CommandLineArgs commandLineArgs = new CommandLineArgs(); // 1. 选项参数 for (String arg : args) { if (arg.startsWith("--")) { String optionText = arg.substring(2); String optionName; String optionValue = null; int indexOfEqualsSign = optionText.indexOf('='); if (indexOfEqualsSign > -1) { // 1.1 从字符串中截取出key optionName = optionText.substring(0, indexOfEqualsSign); // 1.2 从字符串中截取出value optionValue = optionText.substring(indexOfEqualsSign + 1); else { optionName = optionText; if (optionName.isEmpty()) { throw new IllegalArgumentException("Invalid argument syntax: " + arg); // 1.3 以key-value对的形式将参数 和 其对应的值添加到CommandLineArgs对象的Map类型成员optionArgs中。 commandLineArgs.addOptionArg(optionName, optionValue); // 2. 非选项参数 else { commandLineArgs.addNonOptionArg(arg); return commandLineArgs; }根据以上规则,命令行参数--server.port=8088被SimpleCommandLineArgsParser解析为“server.port : 8088”键值属性,加入到CommandLineArgs中。==这里看完,给人的感觉只是将参数解析为key:value键值形式添加到DefaultApplicationArguments对象中,具体什么时候会用呢?==4、第五步:准备当前应用程序的环境ConfigurableEnvironment当ApplicationArguments实例准备完毕后,SpringApplication进入到准备应用程序环境ConfigurableEnvironment的阶段。ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);prepareEnvironment()方法中的内容比较多,大致可以分为6步:获取当前环境,如果不存在,则根据应用类型来创建对应的环境。比如:SERVLET应用类型创建StandardServletEnvironment、REACTIVE应用类型创建StandardReactiveWebEnvironment,否者创建StandardEnvironment。另外:StandardReactiveWebEnvironment 继承自 StandardEnvironment,两者内容完全一样,只是StandardReactiveWebEnvironment又实现了ConfigurableReactiveWebEnvironment接口,虽然其中没做方法的重写。配置当前环境将类型转换器和格式化器添加到环境中、将命令行参数内容(SimpleCommandLinePropertySource {name='commandLineArgs'})添加到环境的propertySources成员变量中、给环境设置activeProfiles。如果propertySources中没有configurationProperties则将ConfigurationPropertySourcesPropertySource {name='configurationProperties'}加入到propertySources中,有的话先移除,然后再加。,广播ApplicationEnvironmentPreparedEvent事件,通知监听器,当前引用环境准备好了。在这里ConfigFileApplicationListener会解析我们的外部配置文件xx.properties、xxx.yml绑定应用环境到spring.main,将应用环境ConfigurableEnvironment转换为相应应用类型的环境。防止对环境转换时有问题,这里再重新配置当前环境。private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // 1、获取或者创建应用环境(创建时会根据应用类型判断) ConfigurableEnvironment environment = getOrCreateEnvironment(); // 2、配置应用环境,配置propertySource和activeProfiles configureEnvironment(environment, applicationArguments.getSourceArgs()); // 如果propertySources中没有`configurationProperties`则将`ConfigurationPropertySourcesPropertySource {name='configurationProperties'}`加入到propertySources中。 // 有的话先移除,然后再加。 ConfigurationPropertySources.attach(environment); // 3、监听器环境准备,广播ApplicationEnvironmentPreparedEvent事件 // 这里会处理我们自定义的配置文件内容--见ConfigFileApplicationListener listeners.environmentPrepared(environment); // 4、绑定应用环境,不用往里深跟 bindToSpringApplication(environment); if (!this.isCustomEnvironment) { // 将应用环境转换为相应应用类型的环境,比如:StandardServletEnvironment、StandardReactiveWebEnvironment、StandardEnvironment environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); // 5、防止对环境转换时有问题,这里再重新配置propertySource和activeProfiles ConfigurationPropertySources.attach(environment); return environment; }下面,我们将这6步拆开细看一下。1)获取或者创建应用环境getOrCreateEnvironment()private ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; switch (this.webApplicationType) { case SERVLET: return new StandardServletEnvironment(); case REACTIVE: return new StandardReactiveWebEnvironment(); default: return new StandardEnvironment(); }从方法的命名来看,这里的意思是获取或创建环境。首先尝试获取应用环境,如果环境不存在,则根据应用类型来创建对应的环境。SERVLET应用类型创建StandardServletEnvironment、REACTIVE应用类型创建StandardReactiveWebEnvironment,否者创建StandardEnvironment。另外:StandardReactiveWebEnvironment 继承自 StandardEnvironment,两者内容完全一样,只是StandardReactiveWebEnvironment又实现了ConfigurableReactiveWebEnvironment接口,虽然其中没做方法的重写。2)配置应用环境configureEnvironment()configureEnvironment(environment, applicationArguments.getSourceArgs());protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { // 加载转换器和格式化器 if (this.addConversionService) { ConversionService conversionService = ApplicationConversionService.getSharedInstance(); environment.setConversionService((ConfigurableConversionService) conversionService); // 配置property sources, 将“SimpleCommandLinePropertySource {name='commandLineArgs'}”添加到ConfigurableEnvironment的propertySourceList。 configurePropertySources(environment, args); // 配置profiles configureProfiles(environment, args); }configureEnvironment()方法中会做三件事:加载类型转换器和初始化器、配置propertySources、配置profiles。1> 加载类型转换器和初始化器默认会将ConversionService赋值到environment中。2> 配置propertySources如果存在命令行参数,则将命令行参数封装为SimpleCommandLinePropertySource添加到环境的propertySources成员变量中。这里和第四步:加载并解析命令行的参数到ApplicationArguments对象中是一样的操作。3> 给环境设置activeProfiles这里就是单纯的获取所有的additionalProfiles和当前环境active的Profile,最后合并添加到environment的activeProfiles成员变量上。3)环境的propertySources中添加configurationProperties走到这里时,从sources中获取到的configurationProperties为null,所以会初始化一个ConfigurationPropertySourcesPropertySource并添加到environment的propertySourceList中。此时,可以看到==environment的propertySources中有6个对象==,即比最初多个两个对象(commandLineArgs、configurationProperties):4)发布事件listeners.environmentPrepared()当应用程序的环境准备之后,EventPublishingRunListener发布一个ApplicationEnvironmentPreparedEvent事件。EventPublishingRunListener将Spring事件的发布委托给它SimpleApplicationEventMulticaster类型的成员initialMulticaster。具体细节和上文聊的==第三.2步:发布应用启动事件ApplicationStartingEvent==是一样的,参考其往里追即可。在这个阶段,ConfigFileApplicationListener事件监听器会进行yaml/properties配置文件的加载;LoggingApplicationListener事件监听器会进行日志系统的初始化;细节另出博文总结。此时,再看==environment的propertySources中有8个对象==,即比上一阶段6个对象多个两个对象(random、applicationConfig:[classpath:/application.yml]):5)绑定应用环境到spring.main绑定应用环境到spring.main;将应用环境ConfigurableEnvironment转换为相应应用类型的环境;6)再次向环境的propertySources中添加configurationProperties先将configurationProperties从environment的propertySources中移除,然后再将其添加到propertySources的头部。至此,ConfigurationEnvironment准备完毕,后面日志中开始输出banner信息。5、第七步:打印bannerBanner printedBanner = printBanner(environment);private Banner printBanner(ConfigurableEnvironment environment) { // bannerMode默认为CONSOLE if (this.bannerMode == Banner.Mode.OFF) { return null; // 获取resourceLoader,默认为:DefaultResourceLoader ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader : new DefaultResourceLoader(getClassLoader()); SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner); if (this.bannerMode == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); // 打印Banner日志,然后返回 return bannerPrinter.print(environment, this.mainApplicationClass, System.out); }具体流程如下:获取banner的逻辑如下:走完banner.printBanner(environment, sourceClass, out);逻辑之后,Console日志输出如下:这里的Banner是默认的,博主没有添加任何自定义的banner;自定义Banner可以参考博文:趣味篇:SpringBoot自定义Banner。至此,banner打印完毕,进入创建Spring应用上下文阶段。6、第八步:创建Spring应用上下文根据应用类型利用反射创建Spring应用上下文,可以理解为创建一个容器;就SERVLET而言:实例化AnnotationConfigServletWebServerApplicationContext。7、第九步:加载异常报告器SpringBootExceptionReporter加载异常报告器SpringBootExceptionReporter,用来支持报告关于启动的错误exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);具体逻辑:从"META-INF/spring.factories"文件中读取SpringBootExceptionReporter类的实例名称集合,然后进行Set去重、利用反射实例化对象,最后按照Order排序。排序后的Set集合赋值到Collection类型的exceptionReporters对象上。getSpringFactoriesInstances(Class)详细过程,见博文:《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息。8、第十步:Spring应用上下文运行前准备Spring应用上下文运行前的准备工作由SpringApplication#prepareContext方法完成,根据SpringApplicationRunListener的生命周期回调又分为“Spring应用上下文准备阶段” 和 “Spring应用上下文装载阶段”。prepareContext(context, environment, listeners, applicationArguments, printedBanner);private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { // 设置应用上下文的environment context.setEnvironment(environment); // 应用上下文后置处理,默认只会将ConversionService添加到context的beanFactory中 postProcessApplicationContext(context); // 应用一些初始化器,执行ApplicationContextInitializer,将所有的初始化对象放到context对象中。 applyInitializers(context); // 发布 应用上下文初始化完成事件ApplicationContextInitializedEvent listeners.contextPrepared(context); if (this.logStartupInfo) { // 打印启动信息 logStartupInfo(context.getParent() == null); // 打印active profile的信息 logStartupProfileInfo(context); // Add boot specific singleton beans // 注册启动参数bean,将容器指定的参数封装成bean,注入容器 ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); // 设置banner if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); // Load the sources // 加载所有资源(指的是启动器指定的参数) Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); // 核心所在!!将我们自己的启动类bean、自动装配的内容 加载到上下文中 load(context, sources.toArray(new Object[0])); // 发布 应用已准备好事件ApplicationPreparedEvent listeners.contextLoaded(context); }1)Spring应用上下文准备阶段本阶段的执行从prepareContext()方法开始,到SpringApplicationRunListener#contextPrepared()运行截止;该阶段,1> 首先context.setEnvironment(environment);将environment设置到Spring应用上下文中;2> 其次postProcessApplicationContext(context);会做一些上下文后置处理,默认只会将ConversionService添加到context的beanFactory中3> 接着applyInitializers(context);应用一些初始化器ApplicationContextInitializer,将所有的初始化器对象的相关内容放到Spring上下文context对象中。其中getInitializers()方法直接从SpringApplication的initializers成员中获取7个初始化器,而initializers的初始化发生在SpringApplication构建阶段,参考博文:《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段。每个初始化器做的具体工作如下:4> 接着listeners.contextPrepared(context);发布Spring上下文已经初始化完成事件ApplicationContextInitializedEvent此处只有BackgroundPreinitializer和DelegatingApplicationListener两个事件监听器会处理ApplicationContextInitializedEvent事件,然而它俩处理逻辑中什么都没做。至此,Spring应用上下文准备阶段内容全部结束,紧接着进入到Spring应用上下文装载阶段。2)Spring应用上下文装载阶段prepareContext()方法中的剩余逻辑全部为Spring应用上下文装载阶段。本阶段又可划分为四个过程,分别为:“注册Spring Boot Bean”、“合并Spring应用上下文配置源”、“加载Spring应用上下文配置源” 和 “发布应用已准备好但未刷新事件ApplicationPreparedEvent”。1> 注册Spring Boot BeanSpringApplication#preparedContext()方法会将之前创建的ApplicationArguments对象和可能存在的Banner实例注册为Spring 单例Bean。2> 合并Spring应用上下文配置源合并Spring应用上下文配置源的操作由getAllSources()方法实现。Set<Object> sources = getAllSources();public Set<Object> getAllSources() { Set<Object> allSources = new LinkedHashSet<>(); // primarySources来自于SpringApplication构造器参数 if (!CollectionUtils.isEmpty(this.primarySources)) { allSources.addAll(this.primarySources); // sources来自于setSource(Set<Object>)方法 if (!CollectionUtils.isEmpty(this.sources)) { allSources.addAll(this.sources); return Collections.unmodifiableSet(allSources); }getAllASources()方法返回值是一个只读Set,它由两个子集合组成:属性primarySources和sources;前者来自来自于SpringApplication构造器参数、后者来自setSource(Set)方法。3> 加载Spring应用上下文配置源load(ApplicationContext, Object[])方法将承担加载Spring应用上下文配置源的职责:load(context, sources.toArray(new Object[0]));protected void load(ApplicationContext context, Object[] sources) { if (logger.isDebugEnabled()) { logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources)); // 获取bean对象定义的加载器 BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources); if (this.beanNameGenerator != null) { loader.setBeanNameGenerator(this.beanNameGenerator); if (this.resourceLoader != null) { loader.setResourceLoader(this.resourceLoader); if (this.environment != null) { loader.setEnvironment(this.environment); // 实际执行load的是BeanDefinitionLoader中的load方法 loader.load(); }Spring应用上下文 Bean的装载任务SpringApplication委托给了BeanDefinitionLoader来完成。进入BeanDefinitionLoader#load()方法。AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner配合使用,形成AnnotationConfigApplicationContext扫描和注册配置类的基础,并将配置类解析为Bean定义BeanDefinition。我们接着跟AnnotatedBeanDefinitionReader#register()方法:最后进入到doRegisterBean(Class,String,Class,Supplier,BeanDefinitionCustomizer[])方法,里面有些东西我们会很熟悉。ConditionEvaluator.shouldSkip(AnnotatedTypeMetadata)方法会判断当前要注册的类有没有被@Conditional注解标注?是否应该跳过它的注册逻辑?BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry);方法中真正将当前启动类注册为BeanDefinition,并且其中会涉及到Alias别名的处理。4> 发布应用已准备好但未刷新事件ApplicationPreparedEventlisteners.contextLoaded(context);当应用上下文准备之后,EventPublishingRunListener发布一个ApplicationPreparedEvent事件。和前面发布事件的方式一样EventPublishingRunListener将Spring事件的发布委托给它SimpleApplicationEventMulticaster类型的成员initialMulticaster。具体细节和上文聊的==第三.2步:发布应用启动事件ApplicationStartingEvent==是一样的,参考其往里追即可。在这个阶段,各个监听器做的内容如下:三、next当SpringApplicationRunListener#contextLoaded()方法执行完成后,Spring应用上下文(ConfigurableApplicationContext)运行前准备的各个操作都执行完毕。写一篇博文,我们把Spring应用上下文启动阶段、和启动后阶段做一个讨论。
一、前言看Spring Boot源码的时候,发现在SpringApplication初始化阶段会加载Spring应用上下文初始化器(ApplicationContextInitializer)、加载Spring应用事件监听器(ApplicationListener);而ApplicationContextInitializer 和 ApplicationListener内建的实现类预置在spring-boot jar包的META-INF/spring.factories文件中;此外,在spring-boot-autoconfigure jar包的META-INF/spring.factories文件中也有一部分:所以,在Spring Boot中一共内建了11个ApplicationListener、7个ApplicationContextInitializer。那么SpringBoot是怎么将其加载到Spring容器中的呢?怎么加载到SpringApplication中的呢?我们就此展开研究。二、正文入口无论是在SpringApplication初始化阶段时加载Spring事件监听器ApplicationListener、Spring应用上下文初始化器ApplicationContextInitializer,还是在SpringApplication准备阶段时加载Spring运行时监听器SpringApplicationRunListener、异常报告器SpringBootExceptionReporter,都要从SpringApplication#getSpringFactoriesInstances()重载方法开始,并且进入到getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args);以Spring应用上下文初始化器为例,此处的type为ApplicationContextInitializer;整体的处理流程为:下面我们分开来看;1、找到type的所有实现类使用Spring工厂加载机制方法SpringFactoriesLoader.loadFactoryName(Class,ClassLoader)来做这个操作;SpringFactoriesLoader.loadFactoryName(Class,ClassLoader)中首先会根据类加载器加载出所有spring.factories中的所有内容。1)loadSpringFactories(ClassLoader)loadSpringFactories(ClassLoader)会解析所有加载的jar包中 META-INF/spring.factories配置文件的配置内容,并组装为Map<String, List>数据结构,方法返回。具体流程如下:首先,去缓冲中查询是否有入参classLoader对应的配置信息(仅第一次加载spring.factories文件时不走缓存),如果存在,则表明服务之前解析过配置文件 并 方法返回。如果不存在,则进行解析操作。其次,获得所有依赖jar包中,具有META-INF/spring.factories配置文件的jar文件URI,并依次进行遍历。接着,将spring.factories配置的内容转化成properties实例;遍历properties实例,将key和value维护到Map<String, List<String>> result数据结构中,如果多个spring.factories中的key相同,则value取合集。最后,将result维护到缓冲cache中——key=ClassLoader value=result;并将result作为返回值返回。<1> 缓存cache数据结构:static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();<2> 方法主体:private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 1、去缓存中,查询是否有入参classLoader对应的配置信息, // 如果存在,则表明服务之前解析过配置文件。如果不存在,则进行解析操作 MultiValueMap<String, String> result = cache.get(classLoader); // 缓存中存在则直接返回 if (result != null) { return result; try { // 2、获得所有依赖jar包中,具有META-INF/spring.factories配置文件的jar文件URI // todo 问自己一个问题,它是怎么找到的? Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); // 遍历所有的URI while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 通过url获得资源resource UrlResource resource = new UrlResource(url); // 3、将spring.factories配置的内容转化成properties实例 Properties properties = PropertiesLoaderUtils.loadProperties(resource); // 4、遍历properties实例,将key和value维护到Map<String, List<String>> result数据结构中 for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); // StringUtils.commaDelimitedListToStringArray只是单纯的将字符串转为String[]数组 for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); // 4、将result维护到缓冲cache中——key=ClassLoader value=result cache.put(classLoader, result); return result; catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }在遍历properties实例,将key和value维护到Map<String, List> result数据结构的过程中,可以发现一个问题:如果多个spring.factories文件中针对同一个key有相同的value值,那岂不就是重复添加了。假设,我依赖的某一个jar包的META-INF/factories中和spring-boot jar包的META-INF/factories中都有 `org.springframework.context.ApplicationContextInitializer=\org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\`。则ConfigurationWarningsApplicationContextInitializer会被添加两次到ApplicationContextInitializer对应的List<String>中。spring不会有这种bug吧?显然是不可能的,往上追,追到SpringApplication类中,在获取到type类所有实现类的类名时会用Set集合做一个去重。思考一下为什么不能在最底层就做去重呢?而需要每个调用方都自己去重!2)classLoader.getResources(FACTORIES_RESOURCE_LOCATION)我很好奇,classLoader.getResources(FACTORIES_RESOURCE_LOCATION)它是如何找到所有META-INF/spring.factories文件的?所以这里特意写一小节。此处的classLoader为AppClassLoader;比较有意思的是getResources(FACTORIES_RESOURCE_LOCATION)方法使用F7进不去,要进去到AppClassLoader里打断点。AppClassLoader是Launcher的静态内部类,其类图如下:即,AppClassLoader间接继承自ClassLoader,而getResources(String name)方法在其类结构中只出现在ClassLoader中,所以要去ClassLoader中打断点;加载Resource资源时也会用到父类加载器。递归由AppClassLoader 的父加载器 ExtClassLoader 负责加载Resource资源;最终体现为:Enumeration<URL>[]数组的0下标所表示其父类、祖父类加载器加载到的Resources资源,而1下标处表示自己加载到Resources资源,这也和双亲委派机制的不一样的点。而AppClassLoader 的父类、祖父类加载器并没有加载到任何资源(因为META-INF/Spring.factories文件也只存在于AppClassLoader的扫描的目录下)。最后看一下AppClassLoader是怎么找到所有的META-INF/spring.factories文件的?1> 因为ClassLoader#findResources(String)是一个抽象方法,具体逻辑由子类实现,结合AppClassLoader的类图,定位到URLClassLoader#findResources(String);在URLClassLoader内部会调用其组合的URLClassPath类的findResources(String, boolean)方法去做一个真正的资源扫描操作。最终效果如下,但是不建议追(太深了,并且很不好debug)。但是有一点我们可以记住:hasMoreElements() 和 nextElement()方法均出自sun.misc包下的CompoundEnumeration类。有兴趣的建议参考博主打断点的思路继续深追如下代码段:3)loadSpringFactories(ClassLoader)返回结果接着通过Map的getOrDefault()方法获取到result中key为ApplicationContextInitializer的value。最后回到SpringApplication#getSpringFactoriesInstances()方法中,使用Set集合来接返回值,以达到一个去重的效果。2、实例化type的所有实现类紧接着上面进入到createSpringFactoriesInstances()方法根据类的全路径名做实例化操作。List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);遍历所有的全路径类名,使用AppClassLoader将相应Class文件从磁盘装载到内存中,然后利用反射获取Class类的无参构造函数、实例化对象。注意:要实例化的类必须要有无参构造函数。private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) { List<T> instances = new ArrayList<>(names.size()); for (String name : names) { try { // 装载class文件到内存 Class<?> instanceClass = ClassUtils.forName(name, classLoader); Assert.isAssignable(type, instanceClass); Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes); // 利用反射实例化对象 T instance = (T) BeanUtils.instantiateClass(constructor, args); instances.add(instance); catch (Throwable ex) { throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex); return instances; }3、做Order排序将type类对应的所有实现类实例化完毕之后,要对他们做一个根据Order的排序。AnnotationAwareOrderComparator.sort(instances);sort()方法中直接使用List集合的sort()方法,但需要自定义Comparator为当前类实例AnnotationAwareOrderComparator。再看AnnotationAwareOrderComparator的类结构:AnnotationAwareOrderComparator继承自OrderComparator,自定义Comparator需要实现Comparator的抽象方法compare(T o1, T o2)。AnnotationAwareOrderComparator自身没有compare()方法,所以看其父类OrderComparator中的compare方法;因为OrderComparator#doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider)方法中的入参sourceProvider为null,所以进入到getOrder()方法时,后续直接调用子类的findOrder(Object obj)方法去查找相应类的顺序值。AnnotationAwareOrderComparator#findOrder()方法中先调用父类OrderComparator#findOrder()方法,如果找到顺序值直接返回,否者从类的@Order注解中取到顺序值。1> 先看OrderComparator#findOrder()方法:该方法判断obj有没有实现Ordered接口,实现Ordered接口之后,有没有重写其getOrder()方法,如果重写了,则直接从getOrder()中获取到序列值。@Nullable protected Integer findOrder(Object obj) { return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null); }以ContextIdApplicationContextInitializer为例,其getOrder()方法返回的序列值为2147483637;2> 再看AnnotationAwareOrderComparator#findOrder()方法:如果通过OrderComparator#findOrder()获取不到序列值,则通过findOrderFromAnnotation()方法从@Order注解中获取序列值。@Override @Nullable protected Integer findOrder(Object obj) { Integer order = super.findOrder(obj); if (order != null) { return order; return findOrderFromAnnotation(obj); }以ConfigurationWarningsApplicationContextInitializer为例:findOrderFromAnnotation()方法中通过OrderUtils.getOrderFromAnnotations(element, annotations);获取@Order注解中的值:OrderUtils.getOrderFromAnnotations(element, annotations);方法返回之后,如果order为null,则直接返回null。最后返回到OrderComparator#getOrder()方法,如果order为null,则将Order设置为Integer.MAX_VALUE;如果两个对象的Order序列值一样,则按原本在集合中的顺序先后排列。1)总述利用List集合自身的排序,通过传入自定义的Comparator 实现排序规则。规则具体如下:AnnotationAwareOrderComparator类继承自OrderComparator,排序规则体现在OrderComparator类中的compare()方法;每个排序对象都会通过OrderComparator#getOrder()方法获取排列的序列值。getOrder()方法中首先通过findOrder()方法查找序列值,而AnnotationAwareOrderComparator重写了findOrder()方法。所以调用findOrder()方法会先进入到AnnotationAwareOrderComparator#findOrder()方法。在AnnotationAwareOrderComparator#findOrder()方法中,会先调用其父类OrderComparator#findOrder()方法判断对象是否实现Ordered接口 并 重写了getOrder()方法,有则返回getOrder()的值。否则通过findOrderFromAnnotation()方法从对象的@Order注解中获取具体的值,如果对象没有被@Order注解标注,则返回null。最后到OrderComparator#getOrder()层面,如果findOrder()返回了具体的Integer值,则返回,否者返回Integer.MAX_VALUE。下篇接着聊SpringBoot启动流程之SpringApplication准备阶段。
一、自定义bannerSpring Boot支持图片和文字两种banner样式,而图片会被转化为ASCII字符画展示。1、自定义文字方式这里可以有两种方式:1)通过在配置文件中指定文件static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";1> 在yaml文件中做如下配置:spring: banner: location: banner_test.txt2> 在resources目录下新建banner_test.txt文件:3> 控制台输出如下:2)默认读取resources目录下的banner.txt文件static final String DEFAULT_BANNER_LOCATION = "banner.txt";1> 啥也不要干,只需要在resources目录下新建一个banner.txt文件:2> 控制台输出如下:2、自定义图片方式static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";支持的图片类型:static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };1> 在yaml文件中做如下配置:spring: banner: image: location: xiaoniao.jpg2> 在resources目录下添加xiaoniao.jpg图片:3> 控制台输出如下:WARN 7716 --- [ main] org.springframework.boot.ImageBanner : Image banner not printable: class path resource [xiaoniao.jpg] (class java.lang.IllegalStateException: 'Unable to read image banner source')可以看到已经是使用了图片打印banner,但是打印不出来。我再换个图片试试:输出如下:这个图片可以用,但是感觉像乱码呀!这就是我们上面提到的图片会被转化为ASCII字符画展示。所以不要用图片打印banner。二、banners?banner可以同时打印自定义文本banner、自定义图片:比如在resource目录下添加banner.txt的同时,通过spring.banner.image.location指定打印图片的路径。示例输出如下:
@[TOC]1、概述SpringBoot自动配置不是配置XML,自动配置的是一些Java Config类;也就是说Spring Boot自动装配的对象是Spring Bean。SpringBoot自动配置的类内容取决于我们在应用的Class Path下添加的JAR文件依赖。自动装配类能够打包到外部的JAR文件中,并且将被SpringBoot装载;自动装配也能被关联到“starter"中,这些”starter“提供自动装配的代码及关联的依赖;spring-boot-autoconfigure是Spring Boot核心模块(JAR),其中提供了大量的内建自动装配@Configuration类,它们统一存放在org.springframework.boot.autoconfigure包或子包下;自动装配的类均配置在META-INF/spring.factories(spring-boot-autoconfigure)资源中;META-INF/spring.factories属于Java Properties文件格式;其中激活自动装配注解@EnableAutoConfiguration充当该Properties的Key,而自动装配类为Value。# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\2、@SpringBootApplication1)注解语义SpringBoot的自动装配机制由@SpringBootApplication注解体现,@SpringBootApplication内部包含三个注解:@SpringBootConfiguration(就是一个@Configuration配置类)、@ComponentScan(basePackages="")(激活@Component的扫描,扫描时排除指定类)是一种综合性技术手段;它重新深度整合Spring注解编程模型,@Enable模块驱动及条件装配等Spring framework原生特性@ComponentScan中排除了AutoConfigurationExcludeFilter类(即:永远排除其他同时标注@Configuration和@EnableAutoConfiguration的类)@EnableAutoConfiguration(核心所在,负责激活Spring Boot自动装配机制)即:@SpringBootApplication注解等同于@Configuration + @EnableAutoConfiguration + @ComponScan注解。然而:我们可以选择不使用@SpringBootApplication,只要将注解@EnableAutoConfiguration 标注在一个@Configuration类上,即可实现自动装配。但是它能减少多注解所带来的配置成本;比如@ComponentScan的basePackages属性被@SpringBootApplication的scanBasePackages属性做别名。2)@SpringBootApplication属性别名@AliasFor注解用于桥接其他注解的属性,其能够将一个或多个注解的属性“别名”在某个注解中。public @interface SpringBootApplication { @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" Class<?>[] scanBasePackageClasses() default {}; }@SpringBootApplication(scanBasePackages = {"com.saint.spring.controller"})@SpringBootApplication的scanBasePackages属性将 @ComponentScan扫描的位置重定向到我们指定的位置;即@SpringBootApplication利用@AliasFor注解别名了@CompoentScan注解的basePackages()属性。3)@SpringBootApplication标注非引导类可以将@SpringBootApplication注解加载任一类ClassA上(注意scanBasePackages属性的值),然后在引导类的main()方法中run(ClassA.class, args)。@SpringBootApplication并非限定标注于引导类。@SpringBootApplication和@EnableAutoConfiguration均能激活自动装配的特性。但对于被标注类的Bean类型则存在差异。@SpringBootApplication“继承”了@Configuration 拥有CGLIB提升特性:正常@Bean的声明方式为“轻量模式”(Lite)在@Configuration下声明的@Bean则属于”完全模式“(Full),会执行CGLIB提升的操作。即:@Configuration类有CGLIB提升。关于CGLIb提升:在java配置类中加@Configuration,下面的声明@bean的方法,就只会被调一次,也就是初始化的时候,哪怕是下面的方法直接互相引用,返回的new的对象的构造方法也只会调一次;而如果不加@Configuration,那么下面的方法如果有相互调用,那么返回的new的对象的构造方法就会被调多次;3、@EnableAutoConfiguration(自动配置的过程)@EnableAutoConfiguration又由两部分组成,分别为:@AutoConfigurationPackage(负责加载默认包加载/扫描路径)、@Import(AutoConfigurationImportSelector.class)(自动装配后续实现);1)@AutoConfigurationPackage@AutoConfigurationPackage注解中通过@Import注解导入了AutoConfigurationPackages.Registrar类;@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { }AutoConfigurationPackages.Registrar类:AutoConfigurationPackages.Registrar类的registerBeanDefinitions()方法中:入参metadata为启动类全路径名;例如:com.saint.StartApplication;new PackageImports(metadata).getPackageNames()表示默认的包加载/扫描路径,即启动类所在的目录;例如:com.saintstatic class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); @Override public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new PackageImports(metadata)); }注意:如果我们依赖jar包中类所文件目录层级和当前程序扫描的包路径有交叉(比如:都已com.group.demo开头)是可以不用在spring.factories文件中写EnableAutoConfiguration = 某某类的。2)@Import(AutoConfigurationImportSelector.class)主要是SpringBoot自动配置的一些特征:通过@Import注入AutoConfigurationImportSelector类,AutoConfigurationImportSelector实现了ImportSelector接口。selectImports()方法会去扫描META-INF/Spring.factories的配置文件,所有组件自动装配均在其中实现;0> AutoConfigurationImportSelector读取自动装配class的流程在其selectImports(AnnotationMetadata)方法中调用自己的getAutoConfigurationEntry(AnnotationMetadata)方法拿到所有自动配置的节点;这里分为六步;第一步,利用Spring Framework工厂机制的加载器SpringFactoriesLoader,通过SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)方法读取所有META-INF/spring.factories资源中@EnableAutoConfiguration所关联的自动装配Class集合。第二步,利用Set不可重复性对自动装配Class集合进行去重,因为自动装配组件存在重复定义的情况;第三步,读取当前配置类所标注的@EnableAutoConfiguration注解的属性exclude和excludeName,并与spring.autoconfigure.exclude配置属性的值 合并为自动装配class排除集合。第四步,校验自动装配Class排除集合的合法性、并排除掉自动装配Class排除集合中的所有Class(不需要自动装配的Class)。第五步,再次过滤候选自动装配Class集合中不符合条件装配的Class成员;最后一步,触发自动装配的导入事件AutoConfigurationImportEvent,发布事件到到Listener(Listener中会绑定BeanFactory,将通过过滤的自动装配候选类、已经排除的自动装配类信息记录到条件评估报告器ConditionEvaluationReport中)。我们在application.yml文件中指定debug: true来查看自动装配情况时,就是ConditionEvaluationReport输出的。@Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; // 加载自动装配的元信息 AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; // 1. 获取@EnableAutoConfiguration标注类的元信息 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 2. 返回自动装配类的候选类名集合 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 3. 移除重复对象,因为 自动装配组件存在重复定义的情况 configurations = removeDuplicates(configurations); // 4. 自动装配组件的排除名单 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 5.1. 检查自动装配Class排除集合的合法性 checkExcludedClasses(configurations, exclusions); // 5.2 排除掉不需要自动装配的Class configurations.removeAll(exclusions); // 6. 进一步过滤 configurations = getConfigurationClassFilter().filter(configurations); // 7. 触发自动装配的导入事件,事件包括候选的装配组件类名单和排除名单。 fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }我们先着重看一下getCandidateConfigurations()是如何获取到所有的自动装配类的?phase1> getCandidateConfigurations() --> 获取自动装配类:获取所有META-INF/Spring.factories的配置文件,进而获取所有的自动配置类;利用SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)方法,SpringFactoriesLoader是Spring Framework工厂机制的加载器;loadFactoryNames()原理如下:搜索指定ClassLoader下所有的META-INF/spring.fatories资源内容;将搜索到的资源内容作为Properties文件读取,合并为一个Key为接口的全类名、Value为实现类全类名 列表的Map,作为方法的返回值;最后从上一步返回的Map中查找并返回方法指定类型 对应的实现类全类名列表。protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; * SpringFactoriesLoader#loadFactoryNames() public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 先从缓存中获取 MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { * 查找所有我们依赖的jar包,并找到对应有META-INF/spring.factories⽂件,然后获取⽂件中的内容 * 第一次循环:file:/.../org/springframework/spring-beans/5.2.12.RELEASE/spring-beans-5.2.12.RELEASE.jar!/META-INF/spring.factories * 第二次循环:file:/.../org/springframework/boot/spring-boot/2.3.7.RELEASE/spring-boot-2.3.7.RELEASE.jar!/META-INF/spring.factories * 第三次循环:file:/../org/springframework/boot/spring-boot-autoconfigure/2.3.7.RELEASE/spring-boot-autoconfigure-2.3.7.RELEASE.jar!/META-INF/spring.factories URL url = urls.nextElement(); // 获取资源 UrlResource resource = new UrlResource(url); // 获取资源的内容 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); cache.put(classLoader, result); return result; catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }第一次扫描:第二次扫描:第三次扫描:SpringBoot扫描到META-INF/spring.factories⽂件之后,META-INF/spring.factories文件中的内容很多,比如spring-boot-autoconfigure-2.3.7.RELEASE.jar!/META-INF/spring.factories中它的org.springframework.boot.autoconfigure.EnableAutoConfiguration key对应很多的Value,我们如何知道哪个需要自动加载、哪个不需要?1、自动配置按需加载的原理比如:org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration@Configuration(proxyBeanMethods = false) // 没有引入RabbitTemplate和Channel就不会初始化RabbitAutoConfiguration到IOC容器 @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) @EnableConfigurationProperties(RabbitProperties.class) @Import(RabbitAnnotationDrivenConfiguration.class) public class RabbitAutoConfiguration { }其中@ConditionalOnClass({ RabbitTemplate.class, Channel.class })便体现了SpringBoot自动配置的按需加载,即:只有RabbitTemplate.class和Channel.class都存在时,才会自动配置RabbitAutoConfiguration。相关的注解还有很多,比如:@ConditionalOnMissingClass、@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnProperty2、容错兼容比如用户创建了一个名称不正确(不符合SpringBoot要求)的类型的Bean,SpringBoot会对其进行自动容错兼容。比如自动装载Servlet时,org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration中上传文件的处理器MultipartResolver,如果用户创建了一个名称不为multipartResolver的MultipartResolver类,SpringBoot会自动将其名称转换为标准名称multipartResolver:@Bean // 作用在当前方法上,要求IOC中必须存在MultipartResolver类型的Bean @ConditionalOnBean(MultipartResolver.class) // 要求IOC容器中不存在名称为multipartResolver的bean @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) public MultipartResolver multipartResolver(MultipartResolver resolver) { // Detect if the user has created a MultipartResolver but named it incorrectly // 调⽤者为IOC,resolver就是从IOC中获取到的类型为MultipartResolver的bean(是MultipartResolver类型的但是名称不是“multipartResolver”) return resolver; // 保存到IOC中的bean的名称是“multipartResolver” }public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";3、 用户配置优先(也可以说是外部配置项修改组件行为)自定义配置优先级高于系统的默认配置。比如在WebMvcAutoConfiguration类针对View进行解析的InternalResourceViewResolver的prefix和suffix设置。@Bean // 如果容器中没有@ConditionalOnMissingBean名称为“defaultViewResolver”的InternalResourceViewResolver类型的Bean,则去创建(对于IOC容器而言,如果你没有创建,我帮你去创建;如果你创建了,那我就以你为主) public InternalResourceViewResolver defaultViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix(this.mvcProperties.getView().getPrefix()); resolver.setSuffix(this.mvcProperties.getView().getSuffix()); return resolver; }比如,自定义配置:spring.mvc.view.prefix = "aaa" spring.mvc.view.suffix="html"4、 查看自动配置情况yaml文件中配置如下内容:debug=true控制台输出:Negative matches: ----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition) AopAutoConfiguration.AspectJAutoProxyingConfiguration: Did not match: - @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice' (OnClassCondition)phase last > fireAutoConfigurationImportEvents() --> 自动装配事件:首先获取所有的自动装配事件监听器AutoConfigurationImportListener;AutoConfigurationImportListener有别于传统的Spring ApplicationListener的实现。ApplicationListener与Spring应用上下文ConfigurableApplicationContext紧密关联,监听Spring ApplicationContext。而AutoConfigurationImportListener则是自定义的java EventListener实现,仅监听AutoConfigurationImportEvent,不过其实例同样可以被SpringFactoriedLoader加载,也就是说SpringBoot框架层面为开发人员提供了扩展的途径;所以我们可以自定义AutoConfigurationImportListener的实现类;执行所有的自动装配事件。private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) { // 1. 获取所有的自动装配事件监听器AutoConfigurationImportListener List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners(); if (!listeners.isEmpty()) { AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions); for (AutoConfigurationImportListener listener : listeners) { invokeAwareMethods(listener); // 2. 执行自动装配事件 listener.onAutoConfigurationImportEvent(event); }自定义AutoConfigurationImportListener:/** * 自定义监听器(监听autoImport) public class CustomAutoConfigurationImportListener implements AutoConfigurationImportListener { @Override public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) { // 获取当前ClassLoader ClassLoader classLoader = event.getClass().getClassLoader(); // 候选的自动装配Class名单 List<String> candidates = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader); // 实际的自动装配Class名单 List<String> configurations = event.getCandidateConfigurations(); // 排除的自动装配Class名单 Set<String> exclusions = event.getExclusions(); // 输出各自数量 System.out.printf("自动装配Class名单 - 候选数量: %d, 实际数量: %d, 排除数量: %s \n", candidates.size(), configurations.size(), exclusions.size()); // 输出实际和排除的自动装配Class名单 System.out.println("实际的自动装配Class名单:"); event.getCandidateConfigurations().forEach(System.out::println); System.out.println("排除的自动装配Class名单:"); event.getExclusions().forEach(System.out::println); }META-INF/spring.factories中添加如下信息org.springframework.boot.autoconfigure.AutoConfigurationImportListener =\ com.saint.spring.autoconfigureImportlistener.CustomAutoConfigurationImportListener@EnableAutoConfiguration(exclude = SpringApplicationAdminJmxAutoConfiguration.class) public class EnableAutoConfigurationBootstrap { public static void main(String[] args) { new SpringApplicationBuilder(EnableAutoConfigurationBootstrap.class) .web(WebApplicationType.NONE) // 非Web应用 .run(args) // 运行 .close(); // 关闭当前上下文 }从数量上来看,META-INF/spring.factories资源配置的自动装配Class名单要远多于实际装载。这是因为部分类被AutoConfigurationImportSelector#filter方法过滤掉了。4、自动装配生命周期自动装配类的@Order很大,也就是优先级极低。要等所有@Configuration class加载完,它才会加载。SpringBoot官网提供了两种方式对自动装配组件组件进行排序。1)排序自动装配组件两种自动装配组件的排序手段:绝对自动装配顺序 --> @AutoConfigureOrder与Spring Framework @Order注解的语义相同;相对自动装配顺序 --> @AutoConfigureBefore 和 @AutoConfigureAfter(推荐使用)。一般不建议对自动装配组件的顺序进行排序,因为这要求开发人员要了解所有的自动装配组件的顺序,显然不太现实。我们可以了解大体的排序规则;如果自动装配Class集合中未包含@AutoConfigureOrder等顺序注解,则他们按照字母顺序依次加载。如果存在,当AutoConfigurationClasses与AutoConfigurationClass建立映射关系后,具体的@AutoConfigureOrder排序规则由AutoConfigurationClass#getOrder()方法决定:首先读取自动装配类的@AutoConfigureOrder的配置值,如果不存在,则使用默认值(很小);接着对@AutoConfigureBefore 和@AutoConfigureAfter进行排序。因为兼容性问题,建议使用@AutoConfigureBefore或@AutoConfigureAfter的name()属性方法。自动装配Class:META-INF/spring-autoconfigure-metadata.properties是自动装配Class预处理元信息配置的资源。当该资源文件存在自动装配Class的注解元信息配置时,自动装配Class无须ClassLoader加载,即可得到所需的元信息,减少了运行时的计算消耗。如果自动装配Class集合中未包含@AutoConfigureOrder等顺序注解,则他们是按照字母顺序依次加载的。5、自定义一个自动装配类(starter)1)命名规则自动装配Class类 --> XxxAutoConfiguration自动装配package命名模式 --> ${root-package}.autoconfigure.${module-package}org.springframework.boot.autoconfigure.aop.AopAutoConfigurationstarter命名模式 --> ${module}-spring-boot-starter2)demo见博文:SprinBoot自定义自动装配类与xxx-spring-boot-starter6、如何使自动装配失效?SpringBoot提供了两种方式:代码配置方式配置类型安全的属性方法:@EnableAutoConfiguration.exclude()配置排除类名的属性方法:@EnableAutoConfiguration.excludeName()外部化配置方式配置属性:spring.autoconfigure.exclude7、后续对于@EnableAutoConfiguration注解中@Import注解导入的AutoConfigurationImportSelector类何时加载,即何时加载自动装配的类?,见博文:《SpringBoot启动流程五》:你确定你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)自动装配入口的代码执行流程图:
一、@AliasFor概述和使用所有注解均实现Annotation接口。较底层注解能够覆盖其元注解的同名属性,并且AnnotationAttributes采用注解就近覆盖的设计原则。覆盖的分类:隐性覆盖:元注解的层次高低关系、Override显性覆盖:当A @AliasFor B时,属性A显性覆盖了属性B的内容。@AliasFor可建立在不同注解层次的属性之间。1. 同一注解内显式使用:@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { @AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {}; //... }value和path互为别名,互为别名的限制条件如下:互为别名的属性 其属性值类型、默认值,都是相同的。互为别名的属性必须定义默认值。互为别名的注解必须成对出现。2. 显示的覆盖元注解中的属性:先来看一段代码:@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = AopConfig.class) public class AopUtilsTest {定义一个标签:@Retention(RetentionPolicy.RUNTIME) @ContextConfiguration public @interface STC { @AliasFor(value = "classes", annotation = ContextConfiguration.class) Class<?>[] cs() default {}; }因为@ContextConfiguration注解被定义为@Inherited的,所以@STC注解可理解为继承了@ContextConfiguration注解;@STC注解使用cs属性替换@ContextConfiguration注解中的classes属性。使用@AliasFor标签,分别设置了value(即作为哪个属性的别名)和annotation(即作为哪个注解);使用@STC注解替换@ContextConfiguration:@RunWith(SpringJUnit4ClassRunner.class) @STC(cs = AopConfig.class) public class AopUtilsTest { @Autowired private IEmployeeService service; }3. 注解中隐式声明别名@ContextConfiguration public @interface MyTestConfig { @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] value() default {}; @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] groovyScripts() default {}; @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] xmlFiles() default {}; }因为value,groovyScripts,xmlFiles都定义了别名@AliasFor(annotation = ContextConfiguration.class, attribute = “locations”),所以value、groovyScripts和xmlFiles也互为别名。4. 别名的传递@AliasFor注解是允许别名之间的传递的,简单理解,如果A是B的别名,并且B是C的别名,那么A是C的别名。@MyTestConfig public @interface GroovyOrXmlTestConfig { @AliasFor(annotation = MyTestConfig.class, attribute = "groovyScripts") String[] groovy() default {}; @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] xml() default {}; @ContextConfiguration public @interface MyTestConfig { @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] groovyScripts() default {}; }@GroovyOrXmlTestConfig把 @MyTestConfig作为元注解;groovy属性作为@MyTestConfig中的groovyScripts属性的别名;xml属性作为@ContextConfiguration中的locations属性的别名;因为MyTestConfig中的groovyScripts属性是ContextConfiguration中的locations属性的别名;所以xml属性和groovy属性也互为别名。二、@AliasFor在SpringBoot源码中的使用public @interface SpringBootApplication { @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" Class<?>[] scanBasePackageClasses() default {}; ..... }@SpringBootApplication(scanBasePackages = {"com.saint.spring.controller"})@SpringBootApplication的scanBasePackages属性将 @ComponentScan扫描的位置重定向到我们指定的位置;即@SpringBootApplication利用@AliasFor注解别名了@CompoentScan注解的basePackages()属性。三、@AliasFor实现原理使用@AliasFor最需要注意一点的,就是只能使用Spring的AnnotationUtils工具类来获取;因为AliasFor是Spring定义的标签。AnnotationUtils工具类中的<A extends Annotation> A synthesizeAnnotation(A annotation, AnnotatedElement annotatedElement)方法来处理@AliasFor注解;其返回一个经过处理(处理@AliasFor标签)之后的注解对象,即把A注解对象处理为支持@AliasFor的A注解对象;public static <A extends Annotation> A synthesizeAnnotation( A annotation, @Nullable AnnotatedElement annotatedElement) { if (annotation instanceof SynthesizedAnnotation || AnnotationFilter.PLAIN.matches(annotation)) { return annotation; return MergedAnnotation.from(annotatedElement, annotation).synthesize(); }本质原理就是使用AOP来对A注解对象做动态代理,而用于处理代理的对象为SynthesizedMergedAnnotationInvocationHandlerpublic Object invoke(Object proxy, Method method, Object[] args) { if (ReflectionUtils.isEqualsMethod(method)) { return annotationEquals(args[0]); if (ReflectionUtils.isHashCodeMethod(method)) { return annotationHashCode(); if (ReflectionUtils.isToStringMethod(method)) { return annotationToString(); if (isAnnotationTypeMethod(method)) { return this.type; if (this.attributes.indexOf(method.getName()) != -1) { return getAttributeValue(method); throw new AnnotationConfigurationException(String.format( "Method [%s] is unsupported for synthesized annotation type [%s]", method, this.type)); }再看getAttributeValue(method)方法逻辑:首先正常获取当前属性的值;然后得到所有的标记为别名的属性名称;接着遍历获取所有别名属性的值;判断attributeValue、aliasValue、defaultValue是否相同,@AliasFor标签的传递性也是在这里体现;如果不相同,直接抛出异常;否则正常返回属性值;
一、前言最近在搞一个SDK,在写接入文档时,需要让业务模块引入MvcInterceptor和MyBatisPlugin,因此有如下内容:在启动类@SpringBootApplication注解中配置扫描包路径:io.terminus.parana.log.sdk@SpringBootApplication(scanBasePackages = {"io.xxx.xx", "io.xxx.log.sdk"})或 在启动类中通过@Import注解注入MvcInterceptor.class@Import(MvcInterceptor.class) @SpringBootApplication public class XxxxApplication { }正好最近在三刷《Spring Boot编程思想(核心篇)》,反想那么多Spring生态组件,它们为什么不需要添加扫描包路径 或 通过@Import注解在启动类导入XxxClass,由此想到可以自定义自动装配类;二、自定义自动装配类SpringBoot自动装配的命名规则:自动装配Class类应命名为:XxxAutoConfiguration;自动装配package命名模式: ${root-package}.autoconfigure.${module-package},比如:org.springframework.boot.autoconfigure.aop.AopAutoConfiguration1)配置类/** * 导入MvcConfig类 @Configuration @Import({InterceptorConfig.class}) public class TraceInterceptorAutoConfiguration { * mybatis 自定义拦截器 @Bean @ConditionalOnMissingClass("io.xxx.log.sdk.config.mybatis.MybatisInterceptor") public Interceptor getInterceptor() { return new MybatisInterceptor(); }2)在resources目录下新建META-INF/spring.factories文件:org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ com.saint.autoconfigure.TraceInterceptorAutoConfiguration到这里自定义自动装配类也就结束了,感觉就贼简单。实际上关键点在于META-INF/spring.factories文件,SpringBoot在做自动配置时会去扫描所有的META-INF/Spring.factories配置文件。我们在https://mvnrepository.com/上可以发现很多的spring-boot-starter-xxx;我们是不是可以自己搞一个呢?下面我就自己搞一个;三、自定义xxx-spring-boot-starterstarter命名模式 --> ${module}-spring-boot-starter;3)接着上面的自定义自动装配类,我们在其同一工程下对其pom.xml文件进行修改:.... <parent> <artifactId>springbootstarter</artifactId> <groupId>com.saint</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>traceInterceptor-spring-boot-starter</artifactId> <dependencies> <!-- Spring Boot Starter 基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <!-- 表明不传递spring-boot-starter依赖 --> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 表明不传递spring-boot-starter依赖 --> <optional>true</optional> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> ....4)在其他项目(比如:auto-configure-sample)中引入starter:<dependency> <groupId>com.saint</groupId> <artifactId>traceInterceptor-spring-boot-starter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>OK,这样其他项目就可以使用到traceInterceptor-spring-boot-starter中自动装配的类了。注意:在自定义的starter 的pom中,将spring-boot-starter的maven依赖声明为<optional>true</optional>,表明formatter-spring-boot-starter(自定义starter)不应该传递spring-boot-starter依赖;否则会将spring-boot-starter版本固定,导致引用自定义starter的应用出现版本冲突问题。
1、maven打包Spring Boot项目的pom.xml文件中默认使用spring-boot-maven-plugin插件进行打包:<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>在执行完maven clean package之后,会生成来个jar相关文件:test-0.0.1-SNAPSHOT.jartest-0.0.1-SNAPSHOT.jar.original2、Jar包目录结构以笔者的test-0.0.1-SNAPSHOT.jar为例,来看一下jar的目录结构,其中都包含哪些目录和文件?可以概述为:spring-boot-learn-0.0.1-SNAPSHOT├── META-INF│ └── MANIFEST.MF├── BOOT-INF│ ├── classes│ │ └── 应用程序│ └── lib│ └── 第三方依赖jar└── org└── springframework └── boot └── loader └── springboot启动程序其中主要包括三大目录:META-INF、BOOT-INF、org。1)META-INF内容META-INF记录了相关jar包的基础信息,包括:入口程序。具体内容如下:Manifest-Version: 1.0Spring-Boot-Classpath-Index: BOOT-INF/classpath.idxImplementation-Title: tms-startImplementation-Version: 0.0.1-SNAPSHOTSpring-Boot-Layers-Index: BOOT-INF/layers.idxStart-Class: com.saint.StartApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Build-Jdk-Spec: 1.8Spring-Boot-Version: 2.4.5Created-By: Maven Jar Plugin 3.2.0Main-Class: org.springframework.boot.loader.JarLauncherMain-Class是org.springframework.boot.loader.JarLauncher,即jar启动的Main函数;Start-Class是com.saint.StartApplication,即我们自己SpringBoot项目的启动类;也是下文提到的项目的引导类。2)BOOT-INF内容BOOT-INF/classes目录:存放应用编译后的class文件源码;BOOT-INF/lib目录:存放应用依赖的所有三方jar包文件;3)org内容org目录下存放着所有SpringBoot相关的class文件,比如:JarLauncher、LaunchedURLClassLoader。3、可执行Jar(JarLauncher)从jar包内META-INF/MANIFEST.MF文件中的Main-Class属性值为org.springframework.boot.loader.JarLauncher,可以看出main函数是JarLauncher,即:SpringBoot应用中的Main-class属性指向的class为org.springframework.boot.loader.JarLauncher。其实吧,主要是 Java官方文档规定:java -jar命令引导的具体启动类必须配置在MANIFEST.MF资源的Main-class属性中;又根据“JAR文件规范”,MANIFEST.MF资源必须存放在/META-INF/目录下。所以main函数才是JarLauncher。JarLauncher类继承图如下:从JarLauncher的类注释我们看出JarLauncher的作用:加载内部/BOOT-INF/lib下的所有三方依赖jar;加载内部/BOOT-INF/classes下的所有应用class;1)JarLauncher的运行步骤?在解压jar包后的根目录下运行 java org.springframework.boot.loader.JarLauncher。项目引导类(META-INF/MANIFEST.MF文件中的Start-Class属性)被JarLauncher加载并执行。如果直接运行Start-Class(示例的StartApplication)类,会报错ClassNotFoundException。Spring Boot依赖的JAR文件均存放在BOOT-INF/lib目录下。JarLauncher会将这些JAR文件作为Start-Class的类库依赖。这也是为什么JarLauncher能够引导,而直接运行Start-Class却不行。2)JarLauncher实现原理?因为org.springframework.boot.loader.JarLauncher类存在于org.springframework.boot:spring-boot-loader中,所以看源码之前需要先引入maven依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> </dependency>再看JarLauncher源码:public class JarLauncher extends ExecutableArchiveLauncher { static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; static final String BOOT_INF_LIB = "BOOT-INF/lib/"; public JarLauncher() { protected JarLauncher(Archive archive) { super(archive); @Override protected boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); return entry.getName().startsWith(BOOT_INF_LIB); public static void main(String[] args) throws Exception { new JarLauncher().launch(args); }JarLauncher#main()中新建了JarLauncher并调用父类Launcher中的launch()方法启动程序;BOOT_INF_CLASSES、BOOT_INF_LIB变量对应BOOT-INF/classes和lib路径;isNestedArchive(Archinve.Entry entry)方法用于判断FAT JAR资源的相对路径是否为nestedArchive嵌套文档。进而决定这些FAT JAR是否会被launch。当方法返回false时,说明FAT JAR被解压至文件目录。1> Archive的概念archive即归档文件,这个概念在linux下比较常见;通常就是一个tar/zip格式的压缩包;而jar正是zip格式的。SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),也可以是文件目录(ExplodedArchive);这样也就统一了访问资源的逻辑层;public interface Archive extends Iterable<Archive.Entry>, AutoCloseable { }Archive继承自Archive.Entry,Archive.Entry有两种实现:JarFileArchive.JarFileEntry --> 基于java.util.jar.JarEntry实现,表示FAT JAR嵌入资源。ExplodedArchive.FileEntry --> 基于文件系统实现;两者的主要差别是ExplodedArchive相比于JarFileArchive多了一个获取文件的getFile()方法;public File getFile() { return this.file; }也就是说一个在jar包环境下寻找资源,一个在文件夹目录下寻找资源;所以从实现层面证明了JarLauncher支持JAR和文件系统两种启动方式。当执行java -jar命令时,将调用/META-INF /MANIFEST.MF文件的Main-Class属性的main()方法,实际上调用的是JarLauncher#launch(args)方法;3) Launcher#launch(args)方法protected void launch(String[] args) throws Exception { if (!isExploded()) { // phase1:注册jar URL处理器 JarFile.registerUrlProtocolHandler(); // phase2:创建ClassLoader ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); String jarMode = System.getProperty("jarmode"); String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); // phase3:调用实际的引导类launch launch(args, launchClass, classLoader); }launch()方法分三步:注册jar URL处理器;为所有的Archive创建可以加载jar in jar目录的ClassLoader;调用实际的引导类(Start-Class);1> phase1 注册jar URL处理器private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; public static void registerUrlProtocolHandler() { String handlers = System.getProperty(PROTOCOL_HANDLER, ""); System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); // 重置缓存的UrlHandlers; resetCachedUrlHandlers(); private static void resetCachedUrlHandlers() { try { // 由URL类实现:通过URL.setURLStreamHandlerFactory()获得URLStreamHandler。 URL.setURLStreamHandlerFactory(null); catch (Error ex) { // Ignore }JarFile#resetCachedUrlHandlers()方法利用java.net.URLStreamHandler扩展机制,实现由URL#getURLStreamHandler(String)提供。URL#getURLStreamHandler(String protocol)方法:首先,URL的关联协议(Protocol)对应一种URLStreamHandler实现类。JDK内建了一些协议的实现,这些实现均存放在sun.net.www.protocol包下,并且类名必须为Handler,其类全名模式为sun.net.www.protocol.${protocol}.Handler(包名前缀.协议名.Handler),其中${protocol}表示协议名。如果需要扩展,则必须继承URLStreamHandler类,通过配置Java系统属性java.protocol.handler.pkgs,追加URLStreamHandler实现类的package,多个package以“|”分割。所以对于SpringBoot的JarFile,registerURLProtocolHandler()方法将package org.springframework.boot.loader追加到java系统属性java.protocol.handler.pkgs中。也就是说,org.springframework.boot.loader包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler;并且按照类名模式,其实现协议为JAR。另外:在URL#getURLStreamHandler()方法中,处理器先读取Java系统属性java.protocol.handler.pkgs,无论其是否存在,继续读取sun.net.www.protocol包;所以JDK内建URLStreamHandler实现是兜底的。为什么SpringBoot要选择覆盖URLStreamHandler?Spring BOOT FAT JAR除包含传统Java Jar资源之外,还包含依赖的JAR文件;即存在jar in jar的情况;默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件,对于jar in jar的包无法加载;当SpringBoot FAT JAR被java -jar命令引导时,其内部的JAR文件无法被内嵌实现sun.net.www.protocol.jar.Handler当做class Path,故需要定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar in jar包的class类文件;2> phase2 创建可以加载jar in jar目录的ClassLoader获取所有的Archive,然后针对每个Archive分别创建ClassLoader;ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); * 获取所有的Archive(包含jar in jar的情况) protected Iterator<Archive> getClassPathArchivesIterator() throws Exception { return getClassPathArchives().iterator(); * 针对每个Archive分别创建ClassLoader protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); return createClassLoader(urls.toArray(new URL[0])); }3> phase3 调用实际的引导类(Start-Class)// case1: 通过ExecutableArchiveLauncher#getMainClass()获取MainClass String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); // 2、运行实际的引导类 launch(args, launchClass, classLoader);对于phase3,大致可以分为两步:首先通过ExecutableArchiveLauncher#getMainClass()获取mainClass(即:/META-INF/MANIFEST.MF资源中的Start-Class属性);利用反射获取mainClass类中的main(Stirng[])方法并调用;<1> 获取mainClass:Start-Class属性来自/META_INF/MANIFEST.MF资源中。Launcher的子类JarLauncher或WarLauncher没有实现getMainClass()方法。所以无论是Jar还是War,读取的SpringBoot启动类均来自此属性。<2> 执行mainClass的main()方法:获取mainClass之后,MainMethodRunner#run()方法利用反射获取mainClass类中的main(Stirng[])方法并调用。运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。4、WarLauncherWarLauncher是可执行WAR的启动器。WarLauncher与JarLauncher的差异很小,主要区别在于项目文件和JAR Class Path路径的不同。相比于FAT Jar的目录,WAR增加了WEB-INF/lib-provided,并且该目录仅存放<scope>provided</scope>的JAR文件。传统的Servlet应用的Class Path路径仅关注WEB-INF/classes/和WEB-INF/lib/目录,因此WEB-INF/lib-provided/中的JAR将被Servlet忽略。好处:打包后的WAR文件能够在Servlet容器中兼容运行。所以JarLauncher和WarLauncher并无本质区别。5、总结Spring Boot应用Jar/War的启动流程:Spring Boot应用打包之后,生成一个Fat jar,包含了应用依赖的所有三方jar包和SpringBoot Loader相关的类。Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载BOOT-INF/classes目录以及/BOOT-INF/lib下面的jar,并利用反射获取mainClass类中的main(Stirng[])方法并调用。即:运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。其他点:SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。SpringBoot通过扩展URLClassLoader --> LauncherURLClassLoader,实现了jar in jar中class文件的加载。WarLauncher相比JarLauncher只是多加载WEB-INF/lib-provided目录下的jar文件。
【云原生&微服务四】SpringCloud之Ribbon和Erueka/服务注册中心的集成细节(获取服务实例列表、动态更新服务实例信息、负载均衡出一个实例、IPing机制判断实例是否存活)