1. 从单体到微服务:一次真实的架构演进心路

做全栈项目,从单体架构一路走到微服务,这几乎是每个后端开发者技术成长路上的必经之路。我最近就在折腾一个“仿小红书”的项目,之前已经完成了基础的微服务拆分,把用户、内容、互动这些模块都独立成了服务。但拆分只是第一步,真正的挑战在于拆分之后,服务之间怎么优雅、可靠地“对话”。今天这篇,我想重点聊聊在微服务架构改造的第二阶段,我们是如何解决服务间通信和分布式事务这两个核心难题的,特别是围绕 Feign 和 Seata 这两个核心组件的实战落地。

很多教程会告诉你 Feign 怎么声明接口,Seata 怎么加个注解,但实际跑起来,你会发现坑一个接一个。比如,Feign 调用超时了怎么办?服务A调B,B又调C,这个调用链怎么追踪?更头疼的是分布式事务,用户发帖要扣积分,发帖成功但积分没扣,或者反过来,这种数据不一致怎么处理?靠业务代码去“补偿”和“回滚”,代码会变得极其臃肿且难以维护。

所以,这次改造的核心目标很明确:第一,建立一套声明式、可维护、具备容错能力的服务间远程调用机制;第二,引入一个相对轻量、对代码侵入性小的分布式事务解决方案,保证核心业务场景的数据最终一致性。我们技术栈选的是 Spring Boot + Spring Cloud Alibaba 这一套,Nacos 做注册和配置中心,Feign 做声明式 HTTP 客户端,Seata 来处理分布式事务。听起来都是标准答案,但魔鬼全在细节里。

2. Feign 的深度配置与最佳实践:远不止一个 @FeignClient

在微服务里,服务间调用是血脉。Spring Cloud OpenFeign 把 HTTP 调用变成了像调用本地方法一样简单,一个 @FeignClient 注解就搞定了。但如果你只停留在这一步,线上迟早会出问题。我们得把它从一个“能用”的工具,变成“好用且可靠”的基础设施。

2.1 基础定义与契约优先

首先,定义 Feign 客户端接口。这里我强烈建议采用“契约优先”的原则。也就是说,先定义好 API 的请求响应模型(DTO)和接口契约,放在一个独立的 api 模块或 client 模块中,然后服务提供方和调用方都依赖这个模块。

// 在独立的 `user-service-client` 模块中
// 1. 定义DTO
@Data
public class UserInfoDTO {
    private Long userId;
    private String nickname;
    private String avatarUrl;
}

// 2. 定义Feign客户端接口
@FeignClient(name = "user-service", path = "/api/user")
public interface UserServiceClient {
    @GetMapping("/{userId}")
    Result<UserInfoDTO> getUserById(@PathVariable("userId") Long userId);

    @PostMapping("/batch")
    Result<Map<Long, UserInfoDTO>> getUsersByIds(@RequestBody List<Long> userIds);
}

这样做的好处是,DTO 和接口定义是共享的,避免了调用方自己瞎猜字段名和类型,也保证了双方的一致性。 UserServiceClient 接口本身并不包含实现,它只是一个契约。在内容服务里,我们只需要引入这个 user-service-client 依赖,然后像注入普通 Bean 一样使用它。

// 在内容服务中
@Service
public class PostService {
    @Autowired
    private UserServiceClient userServiceClient;

    public PostDetailVO getPostDetail(Long postId) {
        Post post = postMapper.selectById(postId);
        // 通过Feign调用用户服务
        Result<UserInfoDTO> userResult = userServiceClient.getUserById(post.getAuthorId());
        // 组装VO...
    }
}

2.2 超时、重试与熔断降级配置

这是 Feign 配置的核心区,直接关系到系统的可用性。默认配置在生产环境下是绝对不够的。

连接超时与读取超时 :Feign 底层默认使用 JDK 的 HttpURLConnection ,其超时时间是无限的,这非常危险。我们必须显式配置。在 Spring Cloud 2020 之后的版本,推荐使用 spring.cloud.openfeign.client.config 进行配置。

# application.yml 中针对特定服务或默认配置
spring:
  cloud:
    openfeign:
      client:
        config:
          default: # 默认配置,对所有Feign客户端生效
            connectTimeout: 3000 # 连接超时 3秒
            readTimeout: 10000    # 读取超时 10秒
            loggerLevel: basic    # 日志级别,调试用
          user-service: # 针对user-service的特定配置
            connectTimeout: 5000
            readTimeout: 15000

为什么这么设?连接超时( connectTimeout )指建立TCP连接的时间,网络正常时这个时间很短,设3-5秒足够。读取超时( readTimeout )指从连接建立成功到收到响应数据的时间,这取决于下游服务的处理逻辑。对于简单的查询,10秒可能够了;对于复杂操作,可能需要更长。你需要根据下游服务的性能 SLA 来定。

重试机制 :网络抖动是常态,一次调用失败就报错,体验太差。Feign 默认不重试,我们需要配置一个 Retryer 。但要注意,重试只对幂等操作(GET、PUT、DELETE)是安全的,对于非幂等操作(POST)要非常小心,或者直接禁用重试。

@Configuration
public class FeignConfig {
    @Bean
    public Retryer feignRetryer() {
        // 重试间隔100ms,最大重试间隔1s,最大重试次数3次(加上第一次调用,共4次)
        return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1), 3);
    }
}

熔断降级 :当某个服务持续超时或失败,再不断地重试和调用只会雪上加霜,拖垮调用方。我们需要快速失败,并执行降级逻辑。这里我们整合 Sentinel 或 Hystrix。以 Sentinel 为例,首先引入依赖,然后在 Feign 配置中启用 Sentinel。

feign:
  sentinel:
    enabled: true

接着,为你的 Feign 客户端接口编写一个降级实现类(FallbackFactory),它可以在熔断时返回一个托底数据。

@Component
public class UserServiceClientFallbackFactory implements FallbackFactory<UserServiceClient> {
    @Override
    public UserServiceClient create(Throwable cause) {
        return new UserServiceClient() {
            @Override
            public Result<UserInfoDTO> getUserById(Long userId) {
                // 记录日志,告警
                log.error("调用用户服务获取用户信息失败, userId: {}", userId, cause);
                // 返回一个兜底数据,比如一个默认用户
                UserInfoDTO defaultUser = new UserInfoDTO();
                defaultUser.setUserId(userId);
                defaultUser.setNickname("用户暂不可用");
                return Result.success(defaultUser);
            }
            // ... 其他方法的降级实现
        };
    }
}

// 在Feign客户端接口上指定
@FeignClient(name = "user-service", path = "/api/user", fallbackFactory = UserServiceClientFallbackFactory.class)
public interface UserServiceClient { ... }

这样,当用户服务不可用时,内容服务在获取作者信息时,不会一直阻塞等待或抛异常导致整个帖子详情页挂掉,而是展示一个友好的默认信息,保证了核心流程的可用性。

2.3 全局拦截器与请求传递

在微服务调用链中,我们经常需要传递一些上下文信息,比如追踪链路用的 traceId 、用户登录态的 token 、当前租户ID等。通过 Feign 的请求拦截器可以优雅地实现。

@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求上下文(如ThreadLocal)中获取traceId
        String traceId = MDC.get("traceId");
        if (StringUtils.isNotBlank(traceId)) {
            template.header("X-Trace-Id", traceId);
        }

        // 传递认证token
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("Authorization");
            if (StringUtils.isNotBlank(token)) {
                template.header("Authorization", token);
            }
        }
    }
}

这个拦截器会自动作用于所有 Feign 请求,确保必要的上下文信息在服务间无损传递。这对于后续的链路追踪和权限校验至关重要。

3. 分布式事务的抉择:为什么最终选了 Seata AT 模式

服务拆分了,一个业务操作横跨多个数据库,事务就成了大问题。经典的“发帖扣积分”场景:发帖服务在内容数据库插入一条记录,同时需要调用积分服务,在积分数据库里扣除相应积分。如何保证这两个操作要么都成功,要么都回滚?

3.1 常见方案的对比与权衡

在引入 Seata 之前,我们评估过几种方案:

  1. 本地消息表 :在发起事务的业务中,同时往本地数据库插入一条业务记录和一条消息记录,利用本地事务保证一致性。然后通过定时任务扫描消息表,发送消息给下游服务。下游消费成功则回调确认。这种方式可靠性高,但实现复杂,需要建表、开发消息状态机、重试机制等,对业务侵入也不小。
  2. 消息队列(MQ)的最终一致性 :比如发帖成功后,发送一条 MQ 消息,积分服务监听并消费。这要求发帖操作本身是幂等的,并且消息不能丢失。RocketMQ 的事务消息可以较好地支持,但同样有编码复杂度,且强依赖于 MQ 的可靠性。
  3. TCC 模式 :Try、Confirm、Cancel。需要业务方编写三个阶段的逻辑,补偿逻辑复杂,开发成本很高,适用于对一致性要求极高且性能敏感的场景。
  4. Seata AT 模式 :自动补偿型事务。对代码侵入小,只需在分布式事务的入口方法上加一个 @GlobalTransactional 注解,Seata 框架会自动拦截 SQL,生成前后镜像,实现回滚。看起来是最简单的。

对于我们这个“仿小红书”项目,核心诉求是: 开发效率高、对现有代码改造小、能覆盖大部分跨库写操作场景 。Seata AT 模式几乎是为这种场景量身定制的。它不用我们写反向补偿逻辑,不用操心消息的可靠投递,框架层自动搞定。虽然其性能有一定损耗(因为要解析SQL、记录undo_log),并且对数据库操作有部分限制(如不支持跨库的关联查询更新),但在我们当前业务规模和复杂度下,是完全可接受的。

3.2 Seata 1.4.2 的核心组件与部署

Seata 有三个核心角色:

  • TC (Transaction Coordinator) : 事务协调器。独立部署的服务,负责维护全局事务的状态,驱动全局事务的提交或回滚。它是大脑。
  • TM (Transaction Manager) : 事务管理器。嵌入在应用中的组件,负责开启、提交或回滚一个全局事务。通常就是加了 @GlobalTransactional 注解的那个方法所在的服务。
  • RM (Resource Manager) : 资源管理器。也嵌入在应用中,负责管理分支事务(即每个微服务自己的本地事务)的资源,向 TC 注册分支事务,并执行 TC 发出的提交或回滚指令。

我们的部署架构是:使用 Docker Compose 在开发环境一键部署一个 Seata-Server(TC),生产环境则部署高可用集群。各个微服务应用(TM和RM)通过配置连接到这个 TC。

# docker-compose-seata.yml
version: '3.8'
services:
  seata-server:
    image: seataio/seata-server:1.4.2
    container_name: seata-server
    ports:
      - "8091:8091" # TC 控制台端口
      - "7091:7091" # TC RPC 端口
    environment:
      - SEATA_CONFIG_NAME=file:/root/seata-config/registry
      - STORE_MODE=db # 事务日志存储模式,这里用数据库
    volumes:
      - "./seata/config:/root/seata-config"
      - "./seata/logs:/root/logs"
    networks:
      - microservice-net

networks:
  microservice-net:
    external: true

关键点在于 STORE_MODE 和配置文件。我们选择 db 模式,将全局事务会话信息存储在 MySQL 中,而不是默认的 file 模式,这样更适合生产环境。这就需要我们提前准备好数据库,并执行 Seata 提供的 global_table.sql branch_table.sql lock_table.sql 来建表。

3.3 Spring Boot 2.4 与 Nacos 的集成配置

我们的微服务用的是 Spring Boot 2.4.x 和 Spring Cloud Alibaba 2021.0.1.0。集成 Seata 客户端需要以下步骤:

  1. 引入依赖 :在需要参与分布式事务的服务中(通常是涉及写操作的服务),引入 spring-cloud-starter-alibaba-seata
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.0.1.0</version> <!-- 版本与你的Spring Cloud Alibaba保持一致 -->
</dependency>
  1. 配置 Seata :在 application.yml 中配置 Seata 客户端。重点是 tx-service-group 和 TC 的地址。
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group # 事务组名称,需与seata-server配置对应

seata:
  enabled: true
  application-id: ${spring.application.name} # 应用ID,一般用服务名
  tx-service-group: ${spring.cloud.alibaba.seata.tx-service-group}
  registry:
    type: nacos # 注册中心类型
    nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      namespace: ${spring.cloud.nacos.discovery.namespace}
      group: SEATA_GROUP # Seata Server在Nacos中的分组,需与server配置一致
  config:
    type: nacos # 配置中心类型
    nacos:
      server-addr: ${spring.cloud.nacos.config.server-addr}
      namespace: ${spring.cloud.nacos.discovery.namespace}
      group: SEATA_GROUP
      data-id: seataServer.properties

这里有个大坑: tx-service-group 的配置 。在 Seata Server 的配置文件( file.conf 或 Nacos 中的 seataServer.properties )里,有一个 service.vgroupMapping 的配置。客户端配置的 tx-service-group (例如 my_test_tx_group )必须映射到 Server 端的某个集群名(例如 default )。这个映射关系一定要对齐,否则客户端启动时会找不到 TC。

  1. 数据源代理 :这是 Seata AT 模式能自动回滚的关键。它需要代理你的数据源,以拦截和解析 SQL。如果你用的是 Spring Boot 默认的 DataSource 自动配置,并且引入了 seata-spring-boot-starter ,那么代理通常是自动完成的。但如果你用了多数据源(比如 dynamic-datasource),就需要特别注意。

4. 多数据源与 Seata 集成的深水区

我们的内容服务因为历史原因,使用了读写分离,所以引入了 dynamic-datasource-spring-boot-starter 来管理多个数据源。这在与 Seata 集成时,带来了最大的挑战。

4.1 dynamic-datasource 的版本适配与数据源代理

首先,版本匹配是关键。我们项目是 Spring Boot 2.4.x,对应的 Spring 版本是 5.3.x。 dynamic-datasource 的 3.5.x 版本是为 Spring Boot 2.5+ 和 Spring 5.3+ 设计的,而 3.4.x 版本更兼容 Spring Boot 2.4。经过测试,我们选择了 com.baomidou:dynamic-datasource-spring-boot-starter:3.4.1

单纯的引入 Seata 和 dynamic-datasource 依赖,Seata 是无法正确代理到 dynamic-datasource 管理的数据源的。因为 Seata 默认代理的是 Spring 容器中名为 dataSource 的 Bean,而 dynamic-datasource 创建的是一个 DynamicRoutingDataSource

解决方案是,我们需要手动配置,将 dynamic-datasource 创建的数据源,用 Seata 的 DataSourceProxy 包装一层,然后再放回容器。

@Configuration
public class DataSourceConfiguration {
    @Primary
    @Bean("dataSource")
    public DataSource dataSource(DataSourceProperties properties) {
        // 1. 这里假设你通过其他方式(如@ConfigurationProperties)已经构建了Hikari数据源
        HikariDataSource dataSource = properties.initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
        // 2. 用Seata的DataSourceProxy进行包装
        return new DataSourceProxy(dataSource);
    }

    // 如果你的dynamic-datasource配置是通过@DS注解动态切换的,那么你需要代理的是DynamicRoutingDataSource
    // 但更常见的做法是,在配置每个具体数据源时就用DataSourceProxy包装
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        HikariDataSource ds = new HikariDataSource();
        // ... 设置连接池参数
        return new DataSourceProxy(ds); // 关键:包装
    }

    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        HikariDataSource ds = new HikariDataSource();
        // ... 设置连接池参数
        return new DataSourceProxy(ds); // 关键:包装
    }

    @Bean
    @DependsOn({"masterDataSource", "slaveDataSource"}) // 确保具体数据源先初始化
    public DynamicRoutingDataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);

        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setDefaultTargetDataSource(masterDataSource);
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
}

这样配置后, @DS(“slave”) 注解切换到的读库数据源,也是被 Seata 代理过的。但这里要特别注意: Seata 的 AT 模式只对写操作(INSERT, UPDATE, DELETE)生成 undo_log。对于纯读操作(SELECT),即使数据源被代理,也没有任何额外开销,所以可以放心用于读库。

4.2 @GlobalTransactional 注解的精准使用

在集成了 Seata 并配置好多数据源代理后,使用起来就简单了。在分布式事务的入口方法上,加上 @GlobalTransactional 注解即可。

@Service
public class PostServiceImpl implements PostService {
    @Autowired
    private PostMapper postMapper;
    @Autowired
    private PointServiceClient pointServiceClient; // Feign客户端

    @Override
    @GlobalTransactional(name = “createPost”, rollbackFor = Exception.class, timeoutMills = 60000)
    public Result<Long> createPost(CreatePostRequest request) {
        // 1. 本地事务:插入帖子
        Post post = convertToPost(request);
        postMapper.insert(post);

        // 2. 远程调用:扣除发帖积分
        DeductPointRequest deductRequest = new DeductPointRequest();
        deductRequest.setUserId(request.getUserId());
        deductRequest.setPoints(10);
        deductRequest.setBizType(“CREATE_POST”);
        Result<Void> deductResult = pointServiceClient.deduct(deductRequest);
        if (!deductResult.isSuccess()) {
            // 这里抛出的异常会触发全局回滚
            throw new BusinessException(“扣减积分失败: ” + deductResult.getMessage());
        }

        // 3. 可能还有其他本地或远程操作...
        // ...

        return Result.success(post.getId());
    }
}

关键点解析

  • rollbackFor = Exception.class :这是默认值,意味着发生任何 Exception 都会触发回滚。如果你希望某些检查异常不触发回滚,需要单独指定。
  • timeoutMills = 60000 :设置全局事务的超时时间,单位毫秒。如果超过这个时间事务还未完成(所有分支未完成提交或回滚),TC 会主动触发回滚。这个时间要设置得比所有分支事务可能的最长处理时间之和还要长一些。
  • 异常传播 :全局事务的回滚依赖于异常。必须在事务边界内(即 @GlobalTransactional 注解的方法内),当需要回滚时,抛出异常。Feign 调用失败会抛 FeignException ,业务失败我们抛自定义的 BusinessException ,这些都会触发回滚。
  • 不要在异步方法中使用 @GlobalTransactional 的原理是基于 Spring 的 @Transactional ,它通过线程上下文传递事务上下文。如果你在方法内启用了新线程,或者用了 @Async ,事务上下文会丢失,导致分布式事务失效。

5. 实战排坑:那些官方文档没细说的坑

理论配置都通了,一跑起来全是坑。下面是我在整合过程中遇到的几个典型问题及其解决方案。

5.1 表结构必须包含主键

Seata AT 模式在回滚时,需要根据 SQL 执行前的前镜像(before image)和主键来定位要回滚的数据行。如果你的表没有主键,Seata 就无法生成正确的回滚 SQL。在项目初期设计表时,务必为每张表都加上主键,最好是单列自增主键或业务无关的雪花算法ID。对于已有的无主键表,需要先进行表结构改造。

5.2 Undo_Log 表冲突与序列化问题

每个参与分布式事务的数据库,都需要创建 Seata 的 undo_log 表。这张表记录了数据修改的前后镜像。常见问题有两个:

  1. 表已存在错误 :在应用启动时,如果 Seata 配置了 client.undo.logTable=undo_log (默认),并且数据源代理成功,Seata 会尝试在第一次操作时自动创建这张表。但如果你的数据库用户没有 CREATE TABLE 权限,或者表名冲突,就会报错。稳妥的做法是,在项目上线前,手动在每个业务库中执行建表 SQL。
  2. 序列化异常 :Undo log 中的镜像数据默认使用 Java 序列化( serializer = “jackson” 需要额外配置)。这意味着你的实体类必须实现 Serializable 接口。否则,在记录 undo log 时会抛序列化异常。这是一个很容易被忽略的点,记得给你所有可能被 Seata 代理操作的实体类加上 implements Serializable

5.3 全局事务ID(XID)的传递与丢失

XID 是 Seata 全局事务的唯一标识,它需要在调用链中传递。Seata 通过拦截器自动在 HTTP 请求头中注入和提取 txXid 。但在以下几种情况下,XID 可能会丢失,导致分支事务无法关联到全局事务:

  • Feign 拦截器覆盖了Header :如果你自定义了 Feign 的 RequestInterceptor ,并且使用了 template.header(key, value) 方法,它会覆盖已有的 header。正确做法是使用 template.header(key, Collections.singletonList(value)) 或者先检查是否已存在。
  • 异步调用 :在新线程中进行的数据库操作或远程调用,无法获取到父线程的 XID。对于必须异步的场景,需要手动将 XID 传递过去: RootContext.bind(xid)
  • 非Spring托管的线程池 :比如你使用 ExecutorService 自己创建的线程池,Spring 和 Seata 的上下文无法传递。建议使用 Spring 的 ThreadPoolTaskExecutor @Async (配合自定义的 TaskExecutor 以支持上下文传递)。

5.4 连接池与 Seata 数据源代理的顺序

这是一个非常隐蔽的坑。如果你的应用同时使用了连接池(如 HikariCP)和 Seata,那么 Bean 的初始化顺序必须是: 原始 DataSource -> DataSourceProxy -> 连接池包装

错误的顺序(比如先被连接池包装,再被 Seata 代理)会导致 Seata 无法正确拦截到 SQL。在我们上面的配置示例中,我们是先构建 HikariDataSource ,然后用 DataSourceProxy 包装它,最后这个代理数据源被 Spring 管理,连接池属性( HikariConfig )是在构建 HikariDataSource 时设置的,这个顺序是正确的。

如果你在 application.yml 中通过 spring.datasource.hikari.* 配置连接池,Spring Boot 会自动创建一个 HikariDataSource Bean。此时,你需要通过 @PostConstruct BeanPostProcessor 来确保这个 Bean 被 DataSourceProxy 替换。更推荐我们前面手动配置的方式,清晰可控。

6. 性能考量与监控闭环

引入 Seata 带来了数据一致性的保障,但也带来了额外的性能开销和复杂度。我们不能只求能用,还得知道用起来代价如何,以及出了问题怎么查。

6.1 AT 模式下的性能损耗分析

Seata AT 模式的性能损耗主要来自三个方面:

  1. SQL 解析 :Seata 需要拦截所有写操作的 SQL,进行解析,以生成前后镜像。这部分会有一定的 CPU 开销。
  2. Undo Log 的写入 :每次写操作,都需要在同库同事务中插入一条 undo_log 记录。这增加了一次数据库 IO。
  3. 全局事务协调 :TM 与 TC、RM 与 TC 之间需要多次 RPC 通信(开启、注册分支、上报状态、提交/回滚)。

优化建议

  • 缩小事务边界 @GlobalTransactional 注解的范围要尽可能小,只包含必须在一个事务内的操作。避免在全局事务内进行长时间的计算或无关的 IO 操作。
  • 避免长事务 :设置合理的 timeoutMills ,并及时处理异常,避免事务悬挂。
  • TC 集群与高可用 :生产环境一定要部署 Seata-Server 集群,并使用数据库或 Redis 作为存储模式,避免单点故障。
  • 关注 undo_log :这张表会随着写操作增长,需要定期清理已提交事务的日志(Seata 有内置的异步任务,但也要监控表大小)。

6.2 搭建可观测性:日志、链路与监控

分布式事务出了问题,排查起来比单体事务困难得多。必须建立完善的可观测性体系。

  1. 日志整合 :确保每个微服务的日志都输出全局事务 ID (XID) 和分支事务 ID (Branch ID)。这可以通过配置 Logback 或 Log4j2 的 MDC 实现。在日志格式中加入 %X{traceId}|%X{xid} ,这样在排查问题时,可以通过 XID 把所有相关服务的日志串联起来。
  2. Seata 控制台 :Seata-Server 自带一个控制台,可以查看全局事务会话、分支事务的详细状态、锁信息等。在开发测试环境,这是排查事务问题的利器。生产环境可以考虑将其部署在内网,供运维人员使用。
  3. 链路追踪集成 :将 XID 注入到你的链路追踪系统(如 SkyWalking, Zipkin)中。通常可以在 Tracing 的 Baggage 或 Tag 里加上 XID。这样,在链路追踪的视图中,你不仅能看到服务调用关系,还能看到这次调用属于哪个分布式事务,一目了然。
  4. 业务状态监控 :对于核心的分布式事务场景,可以增加业务层的监控。例如,在“发帖扣积分”场景,可以记录一个日志:事务开始、本地帖子创建成功、远程扣积分调用成功、全局事务提交。通过监控这些日志的成功率,可以快速发现是哪个环节出了问题。

微服务架构改造,特别是分布式事务的引入,是一个从“能用”到“好用且可靠”的跨越。Feign 解决了服务间如何通信的问题,而 Seata 则试图解决通信后数据如何保持一致的问题。没有银弹,Seata AT 模式用一定的复杂性和性能损耗,换来了开发效率的提升和大部分场景下的数据安全。在项目中期,这是一个性价比很高的选择。但也要清醒地认识到它的边界,对于超高并发或对性能极度敏感的场景,可能还需要结合消息队列最终一致性、TCC等模式做更精细的设计。我们的“仿小红书”项目走到这一步,算是把微服务的骨架真正搭稳了,接下来就是往里面填充更复杂的业务血肉,比如推荐流、搜索、实时消息等等,那又是另一片广阔的天地了。

更多推荐