微服务拆分

微服务拆分的核心是按业务边界解耦,将单体应用拆分为独立运行的服务。

  • 原则:遵循 “单一职责”,每个服务专注于特定业务域(如订单、商品、用户),做到高内聚低耦合。
  • 目的:便于团队独立开发、部署和扩展,避免单体应用的 “牵一发而动全身”。
  • 关键:拆分粒度需平衡(过细会增加服务通信成本,过粗则失去微服务优势),需考虑服务间依赖关系。

NACOS

在微服务远程调用的过程中,包括两个角色:

  • 服务提供者:提供接口供其它微服务访问,比如item-service
  • 服务消费者:调用其它微服务提供的接口,比如cart-service

在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:

image-20250808172540550

流程如下:

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用

当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?

  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

我们项目使用阿里的NACOS,用来注册服务,只要给我们的微服务引入依赖和配置,就可以在注册中心看到微服务,目前我们只使用了nacos的配置中心,可以将几个微服务公共的yaml写入配置列表

image-20250808172938602

OpenFeign

OpenFeign 是声明式 HTTP 客户端,简化微服务间远程调用。

  • 基于接口 + 注解定义调用规则(如@FeignClient指定目标服务),无需手动编写 HTTP 请求代码。
  • 自动集成 Ribbon,实现负载均衡(从 Nacos 获取服务列表后,分发请求到不同实例)。
  • 支持整合 Sentinel,为远程调用添加熔断、降级能力(如配置fallback处理调用失败场景)。

项目中使用OpenFeign主要是定义了一个api模块,将一些多个微服务都要调用的接口或者方法,放在api模块中通过在微服务启动类@EnableFeignClients,在Client中使用@FeignClient(“item-service”)注解,使用远程调用服务,同时我们配置了okHttp替换默认的HttpURLConnection,支持连接池。

支持日志输出需要创建一个配置类注册bean,在需要打印日志的client加上

1
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class) //启动类配置 全局生效
1
2
3
4
5
6
7
8
9
10
11
package com.hmall.api.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}

Gateway

Gateway其实就是一个网关微服务,我们定义一个新的微服务,在项目中我们快速入门是使用hm-gateway完成了路由转发,nginx统一请求8080,我们通过gateway,将进入8080的请求,通过路由规则转发给对应的目标服务,同时它还支持断言和路由过滤、动态路由等作用。总结:Gateway 是微服务的统一入口网关,处理所有外部请求,核心功能是路由与过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.219.128:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

网关登录校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

image-20250808174705433

  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

首先登录校验必须在请求转发到微服务之前做。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。

image-20250808174859617

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到**NettyRoutingFilter**之前,这就符合我们的需求了。

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

GatewayFilter

该过滤器实现,需要先创建一个类继承AbstractGatewayFilterFactory,然后重写Apply方法。还需要在对应的application文件中进行配置,注意!该类的名称一定要以GatewayFilterFactory为后缀。在yaml需要配置,可以全局也可以单个服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- PrintAny=key, value # 逗号之前是请求头的key,逗号之后是value

我这里测试直接使用了全局过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}

// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}

}

GlobalFilter

这个是在项目中使用的过滤器,他使用比较简答其实,不支持动态参数,因为项目只做一个过滤并且将用户信息传参,所以就用GlobalFilter,他只要实现两个接口,一个GlobalFilter,一个ordered(排序的可以不给)。ordered可以设置过滤器的执行顺序,按照自己业务情况来。因为项目中使用的这个,测试用例就不写了。

登录校验

这里我们要先把配置类写好在gateway服务中,创建过滤器,当然秘钥和校验路径要在yaml配置好。exchange中有请求头信息,通过chain转发给其他服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Component
@RequiredArgsConstructor
public class AutoGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//用户拦截 校验
ServerHttpRequest request = exchange.getRequest();
//判断是否需要拦截
if (ifExclude(request.getPath().toString())) {
return chain.filter(exchange);
}
String token = null;
List<String> headers = request.getHeaders().get("Authorization");
if (!CollectionUtils.isEmpty(headers)){
token = headers.get(0);
}
//检验并解析 token
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//传递用户信息
System.out.println("userId = " + userId); // 打印用户id
ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info", userId.toString())).build();
//放行
return chain.filter(swe);
}

private boolean ifExclude(String string) {
// 判断当前请求路径是否在排除路径列表中,如果在任意一个排除路径下,则不需要拦截
return authProperties.getExcludePaths().stream().anyMatch(path -> antPathMatcher.match(path, string));
}
@Override
public int getOrder() {
return 0;
}
}

用户信息传递

现在我们把用户ID发送到了请求头中并且命名为user-info,那我们再定义一个拦截器,将请求头中的用户id拿到放到threadlocal里面就可以了。下面是流程图。

image-20250808212831153

由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。在hm-common定义一个拦截器实现HandlerInterceptor接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserInfoInterceptor implements HandlerInterceptor {
//之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//取出用户id并存到thread local
String userId = request.getHeader("user-info");
if (StrUtil.isNotEmpty(userId)){
UserContext.setUser(Long.valueOf(userId));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();//移除用户
}
}

使用到了这个接口就讲一下把。

HandlerInterceptor 接口
这是 Spring MVC 提供的拦截器接口,用于在控制器(Controller)方法执行前后插入自定义逻辑,主要有三个方法:

  • preHandle:请求到达控制器之前执行(预处理)
  • postHandle:控制器方法执行完成但视图未渲染之前执行(后处理)
  • afterCompletion:整个请求完成(包括视图渲染)之后执行(资源清理)

同时这里我们要引入一个springMvc的配置类,配置刚刚的拦截器。

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}

这里并不会直接生效,因为其他微服务的并不会扫描到这个包,基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中。这里我后面会复习自动装配的原理的。这个时候threadlocal里面就有我们存入的用户信息了。

OpenFeign传递用户

还有存在一个问题,就是当我们下单服务的时候,订单服务获取到了用户信息保存订单,但是到清理购物车的到时候,在购物车服务是没有当前用户的ID的,就会导致购物车服务不知道删除哪个用户的购物车。注意因为是不同的服务,所以不是同一个线程也就不是同一个threadlocal。由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。因为我们是使用OpenFeign实现微服务之间互相调用的,我们可以将OpenFeign每次的发起请求都携带登录用户信息就好了。

image-20250808214052181

Feign有一个拦截器接口feign.RequestInterceptor,只要实现这个接口并实现apply方法就行了,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。可以在之前api模块中DefaultFeignClient里面注册一个方法bean。这样就完成了通过gateway实现服务之前通讯啦!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}

配置管理

后面的配置管理其实就是公共yaml的配置,通过定义一个属性类@ConfigurationProperties(prefix = “hm.cart”)可以实现热更新 。后面放一个动态路由的配置代码。具体就不讲了,网上教程比较多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.hmall.gateway.routers;

import cn.hutool.json.*;
import com.alibaba.cloud.nacos.*;
import com.alibaba.nacos.api.config.listener.*;
import com.alibaba.nacos.api.exception.*;
import com.hmall.common.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
import org.springframework.cloud.gateway.route.*;
import org.springframework.stereotype.*;
import org.springframework.util.*;
import reactor.core.publisher.*;

import javax.annotation.*;
import java.util.*;
import java.util.concurrent.*;

/**
* @ClassName DynamicRouteLoader
* @Description TODO
* @Author CC
* @DATE 2025/8/5 16:15
* @Version 1.0
*/

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter writer;
private final Set<String> routeIds = new HashSet<>();
private final String DATA_ID = "dynamic-route.json";
private final String GROUP = "DEFAULT_GROUP";
@PostConstruct
public void initRouteConfigListener() throws NacosException {
String configAndSignListener = nacosConfigManager.getConfigService()
.getConfigAndSignListener(DATA_ID, GROUP, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}

@Override
public void receiveConfigInfo(String s) {
//监听到更新了 更新路由表
log.debug("监听到路由配置变更,{}", s);
updateRouteConfig(s);
}
});
//第一次启动时候初始化路由表
log.info("初始化路由表");
updateRouteConfig(configAndSignListener);
}
private void updateRouteConfig(String config){
List<RouteDefinition> routeDefinitions = JSONUtil.toList(config, RouteDefinition.class);
// 2.1.删除旧的路由表
routeIds.forEach(routeId -> {
try {
writer.delete(Mono.just(routeId)).subscribe();
} catch (Exception e) {
log.error("删除路由表失败", e);
}
});
routeIds.clear();
// 2.2.判断是否有新的路由要更新
if (CollUtils.isEmpty(routeDefinitions)) {
// 无新路由配置,直接结束
return;
}
routeDefinitions.forEach(routeDefinition -> {
try {
writer.save(Mono.just(routeDefinition)).subscribe();
routeIds.add(routeDefinition.getId());
} catch (Exception e) {
log.error("更新路由表失败", e);
}
});
}
}

微服务保护(Sentinel)

我们可以通过阿里开发的Sentinel实现微服务的保护,它里面自带qps实时监控和直接对请求接口簇点链路进行熔断和流控以及隔离。

报错±-add-opens=java.base/java.lang=ALL-UNNAMED

请求限流

服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。

项目:我们这里直接对购物车设置qps为10,每秒发100个请求,发现拒绝qps90,通过10。实现限流

image-20250808150758362

线程隔离

当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。t

image-20250808151348705

轮船的船舱会被隔板分割为N个相互隔离的密闭舱,假如轮船触礁进水,只有损坏的部分密闭舱会进水,而其他舱由于相互隔离,并不会进水。这样就把进水控制在部分船体,避免了整个船舱进水而沉没。为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。

image-20250808151437176

如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。因为我们是FeignClien接口,所以项目中进行隔离的时候要开启Feign的Sentinel功能支持。

项目:我们对查询商品接口进行流控,因为查询购物车的下级链路是查询商品。

服务熔断

线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。

所以,我们要做两件事情:

  • 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。

项目:这里我们用了FallbackFactory给FeignClient编写降级逻辑,有异常的时候返回空数据。

image-20250808151531472

分布式事务(Seata)

在我们购物车提交订单业务中,在原本的单体项目中,我通过在提交订单方法中@Transactional,保证了事务的一致性。防止提交订单的时候购物车删除商品,但是在业务执行完成之前发生了报错(模拟库存没了),没有进行数据的回滚,导致购物车数据消失了而且没提交订单的情况。注册为单体事务,会保证ACID特性。

image-20250808130021852

由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务:

  • 交易服务:下单事务
  • 购物车服务:清理购物车事务
  • 库存服务:扣减库存事务

整个业务中,各个本地事务是有关联的。因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败,每一个分支事务就是传统的单体事务,都可以满足ACID特性,经过测试在微服务中无法满足整个业务流程的ACID,参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性了。

这就是分布式事务问题,出现以下情况之一就可能产生分布式事务问题:

  • 业务跨多个服务实现
  • 业务跨多个数据源实现

Seata

这里采用使用阿里开源的Seata解决分布式的事务问题。分布式事务产生的根本原因就是互相没感知么,那解决思路就像之前学Redis分布式锁一样,找一个能管理所有单体事务的东西。这里事务协调者,他的任务就是监控每个单体事务,判断事务提交和失败,然后找一个全局事务,保证全局事务下每个事务都要一起提交或者回滚。Seata也是这么个思路。

他分了TM,TC,RM。三个角色管理事务。

  • TC(Transaction Coordinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager)-资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

image-20250808132858193

其中,TMRM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TMRM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。

TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。部署过程就省略了,这里直接说一下Seata的两种分布式解决方案,AT和XA。一共有四种还有TCC和SAGA

XA模式

这种模式是强一致性事务,分两阶段的操作。首先TM会开启全局事务并管理分支事务

RM一阶段操作

  1. 注册分支事务到TC
  2. 执行业务sql但是不提交
  3. 报告事务的状态成功或失败

TC和RM二阶段操作

  1. TC检查各分支的状态,全成功就提交,有一个失败就回滚,通知RM执行操作
  2. 听到TC检查分支事务的状态决定是否全部提交

image-20250808135450904

1
2
seata:
data-source-proxy-mode: XA

优点:

  • 事务的强一致性,保证了ACID原则
  • 常用数据库都支持,实现简单,没有代码入侵@GlobalTransaction

缺点:

  • 类似于串行操作,当二阶段没结束的时候,一阶段会锁定数据库资源,RM管理的微服务无法再执行其他操作,当二阶段执行完毕以后才会释放一阶段的资源,会导致性能过差。

AT模式(默认)

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。他的工作原理是这样的前面部分一样,但是在RM执行sql 的时候会直接提交**(这个时候会释放数据库锁),那如何进行数据回滚呢,他采用了快照,要使用AT模式,要在每个数据库建一个undo log表,他会在执行sql前记录更新前的数据,放到undo log表中,当后期TC通知RM提交的时候会删除log,如果要回滚,就按照记录的log进行数据回滚,回滚以后再删除数据,它是最终一致性**事务。这其实就是空间换时间的一种思想,保证直接提交,那么如果数据要回滚,就要记录数据。但是正常业务中其实回滚的操作是比较少的,所以用AT模式性能比较好,综合下来也比较好用,它也有缺点

——(假设这次会失败回滚)当分支业务提交以后~回滚之前,这段时间有其他业务访问数据库,会出现数据库不一致的问题,这是无法避免的,但是这段时间很短,要作取舍。

image-20250808141301723

区别

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致