微服务学习入门
- 引入微服务
传统单体应用的优缺点:
优点:
- 简单 方便
缺点:
- 服务的耦合度高
- 高并发时数据库压力大 没扛住就崩了
微服务的优点:
- 服务解耦 数据库解耦
- 管理维护十分方便
- 请求压力被分散了 可用性好 还可以创建集群 负载均衡 进一步抗压
缺点:
-
创建复杂
-
维护成本高
-
项目拆分
2.1. 如何拆分?
2.1.1. 模块
- 创建新子模块准备为抽离服务做准备
- 分析原来项目的依赖情况 抽出公共的依赖 如 Lombok spring-test等
- 分析抽离的服务所需要的子模块和依赖
- 创建公共子模块 并导入公共的依赖 给新创建的服务子模块导入公共模块的坐标和自己所需的依赖
2.1.2. 代码
- 转移新服务所需要的实体类 方法 接口等
- 若新服务使用了数据库 还可以为新服务分配新的 mysql实例 数据库 和 表
2.1.3. 配置
- 对父模块进行依赖管理 管理cloud的依赖版本 (*)
▼xml复制代码<dependencyManagement> <dependencies> <!-- Spring Cloud 2021.0.x --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.8</version> <type>pom</type> <scope>import</scope> </dependency> <!-- Spring Cloud Alibaba 2021.0.5.0 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
更新新服务配置 配置新的启动类
-
设置新服务的端口号
-
Nacos
3.1. 现存问题:
- 抽离的服务启动的越来越多 数量一多就不好管理
- 服务可能存在集群 每个服务的端口不一样 调用时如果写死就做不到负载均衡
引入:
nacos --> 发现中心 对注册的微服务 发现和管理
功能:
-
注册服务 : 配置服务名称 启动端口自动出现在Nacos
-
发现服务: 配置服务名称 启动端口 引入提供的client 根据名称获取服务实例列表
-
使用服务: 使用随机算法获取服务实例(负载均衡) 根据实例的URl动态发送请求
3.2. Docker 安装 Nacos :
3.2.1. 安装Docker
ubantu安装Docker: https://blog.csdn.net/a1053765496/article/details/147691484
3.2.2. 安装Nacos
3.2.3. 检验是否启动
- 可以访问 http:// 你的naocs地址:8848/nacos 管理页面

- 8848 9848 9849 被占用 而且 docker logs 没有报错
▼shell复制代码# 查看运行的容器 dokcer ps # 查看容器的日志 docker logs your_container
3.2.4. 踩坑(docker版)
- 新版nacos 需要传入三个生成的base64凭证 麻烦的很
- 需要先导入数据库 并且要创建官方要求的配置表
- 内存小的服务器就在学习的话就在虚拟机部署了吧 我的2g的 一个nacos 一个mysql直接nacos挂了 命令行都卡飞
- 记得开放端口 8848 9848 9849 不然后续 在 nacos 的服务注册不了
3.3. nacos使用
- 导入nacos 发现依赖
▼xml复制代码<!-- Nacos 服务注册与配置 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
- 启动类打开 服务发现开关
▼java复制代码//当引入了 spring-cloud-starter-alibaba-nacos-discovery 依赖后, //Spring Boot 的自动配置机制会加载相关的配置类。这些配置类会自动启动服务注册和发现功能, //可无需显式添加 @EnableDiscoveryClient @SpringBootApplication @EnableDiscoveryClient public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
- 配置nacos 地址 和 服务名称
▼xml复制代码spring: application: name: #你的微服务名称: 用来访问实例的唯一特征 cloud: nacos: discovery: server-addr: # 你的naocs地址:8848 username: nacos #默认 password: nacos #默认
- 启动微服务 然后就能在 http:// 你的naocs地址:8848/nacos 管理页面看到了你的服务

当前缺点: 服务创建好了 怎么调用呢 ?
- OpenFeign
4.1. 介绍
OpenFeign :
-
提供像 SpringMVC 一样的操作发送请求的调用微服务的方式
-
提供了支持 负载均衡 的能力 多集群部署的时候分散访问压力
-
根据调用的服务名动态获取实例 不关心到底是微服务是哪个端口
4.2. 使用
依赖于nacos发现实例的能力 所以要先有上一节中 nacos的环境 和 发现配置的依赖
4.2.1. 导入依赖
▼xml复制代码<!-- openfeign 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 负载均衡依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- okhttp支持线程池 默认的 openfeign 不支持线程池 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
4.2.2. 启动类打开开关
▼java复制代码@SpringBootApplication @EnableFeignClients //打开开关 public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
4.2.3. 配置 okhttp 提升连接效率
▼xml复制代码spring: application: name: HttpService cloud: nacos: discovery: server-addr: 192.168.51.128:8848 username: nacos password: nacos server: address: 0.0.0.0 port: 8085 feign: okhttp: #打开okhttp的支持 enabled: true
4.2.4. 使用示例
创建接口 创建请求接口
▼java复制代码import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 新建 feign 接口 声明接口 * **/ @FeignClient(value = "AIService") public interface AIServiceClient { @PostMapping ("/api/ai/genChart") String getAnswer(@RequestParam String question); }
注释:
@FeignClient(value = "AIService") 值是所创建的实例名称
@PostMapping ("/api/ai/genChart") 以Post的请求访问 /api/ai/genChart 接口
@RequestParam String question 传递的参数是 question
调用的地方
▼java复制代码//直接注入就能用 底层是 代理类 实现的改接口 @Resource pravite AIServiceClient aIServiceClient public void test(){ aIServiceClient.getAnswer("something......") }
执行的地方
▼java复制代码@RestController @RequestMapping("/ai") public class AIController { @PostMapping("/genChart") public String genChart(@RequestParam String question) { if (question == null) return null; String answer = AIUtils.getAnswer(question); return answer; } }
- SpringGateway
5.1. 介绍
网关: 就是根据路由进行请求的转发 SpringGateway 能够根据路由从而转发请求到对应的 微服务 并且能在微服务中 设置过滤器 做一些处理 使用时应该注意过滤器链的执行顺序

5.1.1. 现存的问题
- 后端的端口很容易变化 前端并不想关心后端的端口的变化 只想请求一个固定的地址和端口
- 后端的接口并不是所有人都能调 比如有些需要登录后的用户可以使用 然后在每个服务都写一遍登录校验的逻辑非常冗余
- 各个微服务可能都需要前端传来的jwt或者sessionId 用来获取保存的用户信息 然而给每个服务都发送jwt或者sessionId 服务和服务之间相互调用可能也要发送 然后各自校验太麻烦了
5.1.2. 解决
对于1问题: 网关开放一个固定端口 再根据路由信息转发到对应的微服务
对于2问题: 在请求到达微服务前先在网关中校验用户的权限 不适合的直接pass
对于3问题: 在网关设置过滤器 处理好jwt保存的信息保存在请求头中再转发给对应的微服务 微服务也不必都从请求头中获取 可以在请求到达的时候加一个拦截器 拦截器用ThreadLocal 或者 redis 保存用户的信息 微服务用的时候直接取就ok了
5.2. 使用
5.2.1. 导入依赖
▼xml复制代码<!-- 网关依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- 如果父模块传递了 spring-boot-starter-web,手动排除 !!! --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> </exclusion> </exclusions> </dependency>
注意:
网关是基于flux的 而不是SpringMVC的 所以使用应该注意排除掉SpringMVC的相关依赖
5.2.2. 配置路由
▼yaml复制代码spring: cloud: gateway: routes: - id: AIService #服务名 uri: lb://AIService # lb:表示负载均衡 到服务 predicates: - Path=/api/ai/** #转发什么路径的请求
predicates 断言:
就是判断的依据 官网还提供了根据时间的先后 cookie 路径的参数 等来设置
官网:
5.3. 过滤器

5.3.1. 全局过滤器
使用:
这里在全局过滤器实现登录的校验
▼java复制代码/** * 全局过滤器 */ @Component @RequiredArgsConstructor public class GlobalFilter implements org.springframework.cloud.gateway.filter.GlobalFilter, Ordered { private final PathWhiteListProperties pathWhiteListProperties; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final RedisTemplate<String, Object> redisTemplate; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 处理全局所有的请求 登录校验 //如果是登录注册请求 直接放行 RequestPath requestPath = exchange.getRequest().getPath(); if (isAuth(requestPath)) { //放行 return chain.filter(exchange); } System.out.println("拦截器启动!"); //其他请求 校验用户是否登录 //1.从 redis 获取用户信息 String sessionId = exchange.getRequest().getHeaders().get(UserConstant.USER_Seession_ID).get(0); String jsonStr = (String) redisTemplate.opsForValue().get(UserConstant.USER_LOGIN_STATE + sessionId); try { User currentUser = JSONUtil.toBean(jsonStr, User.class); if (currentUser == null || currentUser.getId() == null) { //如果用户信息不存在 拒绝请求 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); //拒绝请求 } //对下游的请求头修改 把用户信息放入请求头 传递下去 (mutate 对请求修改) exchange.getRequest().mutate().header(UserConstant.USER_Seession_ID, sessionId); } catch (Exception e) { //如果用户信息不存在 拒绝请求 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } //2.放行 return chain.filter(exchange); } //设置过滤器的优先级 0 最高 @Override public int getOrder() { return 0; } boolean isAuth(RequestPath requestPath) { String path = requestPath.value(); List<String> exclude = pathWhiteListProperties.getExclude(); for (String excludePath : exclude) { if (pathMatcher.match(excludePath, path)) { return true; } } return false; } }
5.3.2. 与拦截器组合
现状: 正如上面的代码 实现了登录的校验 并且修改了请求头 把 USER_Seession_ID 放进去 以便后面接收到请求的微服务可以根据请求头获得用户信息
问题:
- 每个微服务都要从请求头获取 都写一遍太冗余了
- 如何把过滤后的处理后的sessionId传递给微服务
解决:
问题1. 在公共模块配置拦截器 所有微服务都会引入公共模块 这样就不必都写一遍了
问题2. 封装在请求头中传递过去
代码示例:
▼java复制代码@Component @RequiredArgsConstructor @Slf4j public class UserRequestIntercepter implements HandlerInterceptor { private final RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception { //从请求头获取 sessionId String sessionId = request.getHeader(UserConstant.USER_Seession_ID); //从redis取出登录态 String jsonStr = (String) redisTemplate.opsForValue().get(USER_LOGIN_STATE + sessionId); try { User currentUser = JSONUtil.toBean(jsonStr, User.class); if (currentUser == null || currentUser.getId() == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } LoginUserVO loginUserVO = new LoginUserVO(); BeanUtils.copyProperties(currentUser, loginUserVO); loginUserVO.setSessionId(sessionId); log.info("user服务的拦截器 从请求头获取 sessionId 成功!"); //保存 登录态 到 ThreadLocal 一个服务的应用调用ThreadLocal就可以使用了 ThreadLocalUtil.setAttribute(loginUserVO); return true; } catch (Exception e) { log.error("user服务的拦截器 从请求头获取 sessionId 失败!"); return true; } } }
要想拦截器生效 还需要在mvc配置
▼java复制代码@Configuration @RequiredArgsConstructor @ConditionalOnClass(DispatcherServlet.class) //注意 public class SpringMvcConfig implements org.springframework.web.servlet.config.annotation.WebMvcConfigurer { private final UserRequestIntercepter userRequestIntercepter; //注册拦截器 @Override public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) { registry.addInterceptor(userRequestIntercepter).addPathPatterns("/**"); } }
注意:
因为网关也是引用了common模块的 然而网关不是基于SpringMVC的 所以直接配置会报 找不到MVC包的错
解决:
使用 @ConditionalOnClass(DispatcherServlet.class) 条件
表示只有含有DispatcherServlet的服务拦截器才能够生效 ! ! !
5.3.3. 微服务间的信息传递
问题:
之前解决了网关和微服务的信息传递 那么微服务之间如何传递呢\
解决:
给 OpenFeign 加一层拦截器 保存信息到请求头 用来传递信息
▼java复制代码public class DefaultConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; //配置日志等级 } //请求拦截器 @Bean public RequestInterceptor requestInterceptor() { return template -> { LoginUserVO loginUserVO = (LoginUserVO) ThreadLocalUtil.getAttribute(); if(loginUserVO!=null){ String sessionId = loginUserVO.getSessionId(); if (StringUtils.isNotBlank(sessionId)) { template.header(UserConstant.USER_Seession_ID, sessionId); } } }; } }
开启拦截器配置
在需要拦截器生效的服务的启动类上使用拦截器
▼java复制代码@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(defaultConfiguration = DefaultConfig.class) public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
- 配置管理
6.1. 共享配置
6.1.1. 问题
问题: 现在每个微服务都引入了很多重复的配置 很冗余
解决: 可以提取公共配置交给nacos管理 自己再配置个性化配置就可以
6.1.2. 操作
6.1.2.1. 引入配置依赖
▼xml复制代码<!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- 读取 bootstrap.文件 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
6.1.2.2. 在nacos 创建各自需要的公共配置

如:
▼yaml复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${yy.db.address:localhost}:${yy.db.port:3306}/${yy.db.database:es_db} username: ${yy.db.username:root} password: ${yy.db.password:123456}
6.1.2.3. 创建 bootstrap.yml 文件 定义加载nacos地址的配置等 !
原因: 如果不先配置bootstrap 它启动会首先加载nacos的 但是nacos还没配置地址 所以就会找不到配置
配置加载顺序:

在对应的微服务的resource下 创建bootstrap.yml 配置nacos地址 引入公共的配置
▼yaml复制代码spring: application: name: AIService # 服务名称 profiles: active: dev cloud: nacos: server-addr: 192.168.51.130:8848 #nacos地t址 config: file-extension: yaml # 文件后缀名 shared-configs: # 共享配置 - data-id: shared_seta.yaml # 共享seata配置
6.1.2.4. 4.个性化公共配置
▼yaml复制代码yy: db: address: localhost username: sheep2 .....诸如此类
6.2. 热更新配置
6.2.1. 问题
问题: 每次修改配置都需要重启微服务
解决: 使用NacosConfig 在启动时自动拉取配置文件
示例:

6.2.2. 操作
- 在 在nacos 客户端页面选择对应的服务创建配置中 创建配置
注意:
- 格式是: 服务名+环境(可选)+yaml
- 短横线表示一个集合
▼yaml复制代码hm: cart: name: sheep age: 10 sex: 男 address: 上海 phone: 13888888888 habit: - 吃饭 - 睡觉 - 打豆豆
- 创建对应的配置对象 使用注解自动填充属性
示例:

此时就可以在nacos中动态更改了
- Sentinel
7.1. 分布式事务雪崩问题
7.1.1. 介绍
原因: 比如在高并发的情况下 服务可能因为压力宕机或者本身就是慢请求 而其他调用该服务的机器并不知道 只是一味的发送请求 消耗线程资源 最后服务因为资源耗尽而崩溃 其他调用此服务的机器也碰到相同问题 最终引起大面积的机器宕机

示例:
如图所示 服务B C 需要调用服务A D 然后A D 又需要服务E
现在对E的调用执行缓慢或者E已经挂了 A D并不知情 一味的发请求 线程消耗完了最后A D也挂了
最后 B C也因为线程资源耗尽也挂了 于是 整个系统服务全部不可用 引起事务雪崩
7.1.2. 解决雪崩
服务保护机制:
- 接口限流
限制接口的QPS 以免高并发挂掉 引起雪崩
- 线程隔离
每个服务请求规定一定数量的能承受的线程数 线程耗尽就不允许继续请求
- 熔断降级
统计异常请求数 超过一定阈值就熔断该服务 拒绝请求 一段时间后再发一次请求 若通过则恢复服务 反之继续熔断

引入: Sentinel
7.2. Sentinel
7.2.1. 介绍
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
7.2.2. 使用
- 下载并启动 sentinel 客户端
- 使用 sentinel 和密码 sentinel 登录 sentinel 控制台
- 引入 sentinel 依赖并配置 sentinel
▼xml复制代码<!-- 引入Sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
- 基于MVC的服务会自动加入到 sentinel 若要开启 sentinel 对 openfeign 的支持 也要配置
▼yaml复制代码feign: sentinel: enabled:true
- 点击式接口限流 线程隔离 熔断降级
官方文档: https://sentinelguard.io/zh-cn/docs/introduction.html
与SpringCloud 整合: https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
7.2.3. Fallback
Fallback 机制保证服务在降级失败之后的处理逻辑
使用:
- 自定义类,实现FallbackFactory,编写对某个FeignClient 的 Fallbacki逻辑:
▼java复制代码/** * 失败后的降级处理 */ @Slf4j class A implements FallbackFactory<AIServiceClient> { @Override public AIServiceClient create(Throwable cause) { //创建AIServiceClient.接口实现类,实现其中的方法,编写失改降级的处理逻辑 return new AIServiceClient() { @Override public String getAnswer(String question) { //记录异常信息,可以返回空或抛出异常 log.error("查询用户失败",throwable); return null; } }; } }
- 将刚刚定义的 UserClientFallbackFactory 注册为一个Bean
▼java复制代码@Bean public UserClientFallbackFactory userclientFallback(){ return new UserclientFallbackFactory(); }
- 在 UserClient 接口中使用 UserClientFallbackFactory
▼java复制代码@Feignclient(value = "userservice",fallbackFactory = UserclientFallbackFactory.class) public interface Userclient{ @GetMapping("/user/[id]") User findById(@PathVariable("id")Long id); }
此时 被拒绝的亲请求就会走 Fallback
7.2.4. 服务熔断

添加熔断规则

注释:
最大RT: 超过该值就为慢调用
比例阈值: 请求的数量有多少比例的异常会被记录
最小请求数: 在统计时长里的最少几次请求才会被记录
- 分布式事务
▼yaml复制代码server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: 127.0.0.1:4560 kafka-appender: bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: type: nacos nacos: server-addr: 172.17.0.2:8848 namespace: group: DEFAULT_GROUP username: nacos password: nacos context-path: data-id: seata.properties registry: type: nacos nacos: application: seata-server server-addr: 172.17.0.2:8848 group: DEFAULT_GROUP namespace: cluster: default username: nacos password: nacos context-path: store: mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://172.17.0.3:3306/seata_db?characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useUnicode=true&useSSL=false user: root password: 123456 min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 1000 max-wait: 5000 security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
8.1. 分布式事务介绍

如图: 此类事务因为是微服务部署 每个服务各司其职 对于下单操作可能调用如图的三个服务协同完成 然而其中的某个服务失败并不会影响其他服务的操作 这是不可以的 我们希望出现异常 事务应该回滚
引出: Seata
8.2. Seata
介绍: Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
官方中文文档: https://seata.apache.org/zh-cn/docs/overview/what-is-seata
教程1: https://cloud.tencent.com/developer/article/2363585?policyId=20240001&frompage=seopage
教程2: https://blog.csdn.net/lchmyhua88/article/details/146136099
8.2.1. 重要术语

8.2.2. Docker 部署 Seata
- 建表
github: https://github.com/apache/incubator-seata/blob/master/script/server/db/mysql.sql
- 拉取镜像 创建容器 复制配置到宿主机 配置yaml 设置与数据库的连接
▼shell复制代码docker cp seata:/seata-server/resources /home/seata
- 删除容器 重新加载容器 挂载配置yaml 启动
▼shell复制代码docker run -d --name seata \ -p 8091:8091 \ -p 7091:7091 \ -e SEATA_IP=192.168.51.130 \ -v /home/seata/resources:/seata-server/resources \ seataio/seata-server:1.5.0
8.3. Seata整合微服务
- 添加依赖
▼java复制代码<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
- nacos 添加seata 公共配置 需要的微服务自己引入
- 在事务的入口方法加上注解 即可开启
▼java复制代码@GlobalTransactional(name = "order",rollbackFor = Exception.class) public void AIService(){ start_AIService1..... start_AIService2..... start_AIService3..... }
8.4. 常见模型
Seata 把事务提交分为两个阶段 不同的模型对这两个阶段会有不同的处理方案
8.4.1. AT (默认)

一阶段:
- TM 发起全局事务 注册到 TC
- 每个 RM 执行 sql 完成后就提交事务 并生成快照 同步状态到 TC
- TC 检查各个事物的状态 若有异常 则所有事务利用快照回滚
二阶段:
- 若无异常 则批量删除快照
8.4.2. XA

一阶段:
- 每个服务执行后 不提交 汇报状态给TC
二阶段:
- 执行完成后 TC若发现异常 所有事物回滚
- 若无 则提交提交事务
