聊聊Seata模式设置与拦截器(XID的传播)、异常处理的封装思路

202 阅读6分钟

嘿,大家好!今天咱们来聊聊分布式事务框架Seata的模式设置,以及我在项目中怎么封装RequestInterceptorGlobalExceptionHandler的思路。这篇文章会从最简单的想法出发,一步步发现问题,再慢慢逼近现在主流的解决方案。过程中我会带大家看看“简单”策略会暴露啥短板,然后思考咋优化,最后对齐那些成熟的方案。走起!


一、Seata模式设置:从“啥都手动”到“优雅配置”

先说Seata的模式设置。Seata的核心是解决分布式事务问题,它有好几种模式,比如AT模式(自动事务)、TCC模式(手动补偿)、Saga模式(状态机驱动)啥的。咱们假设简历上写了AT模式,那就从最朴素的思路开始讲。

最朴素的办法:硬编码,手动开关

想象一下,刚接触Seata时,我可能会这么干:在代码里直接写死用AT模式。比如,配置个seata.tx-service-groupseata.service.vgroup-mapping,然后每个服务启动时手动指定用AT模式,事务传播全靠程序员自己盯着。这种方式简单粗暴,写个Demo跑起来没啥问题。

但问题很快就出来了:

  1. 扩展性差:如果我想换成TCC模式咋办?改代码,重启服务,太麻烦了吧。
  2. 配置散乱:每个微服务都要自己配一遍,稍微多几个服务,维护起来就头大。
  3. 误操作风险:手动控制事务传播,万一忘了传XID(全局事务ID),分布式事务不就崩了?

优化方向:集中配置 + 动态切换

发现这些坑后,我开始琢磨:能不能把模式配置抽出来,集中管理?Seata其实早就想到了这点。它支持通过配置文件(比如application.yml)或者配置中心(像Nacos、Apollo)来设置模式。比如:

seata:
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  data-source-proxy-mode: AT

这样,AT模式是通过data-source-proxy-mode统一设置的,想换TCC就改成TCC,服务重启就生效。配置集中了,扩展性也上来了。而且,Seata还支持动态加载配置,配合配置中心甚至可以不重启服务就切换模式,简直不要太香。

再逼近主流:自动化与透明化

现在的方案已经很不错了,但主流做法还更进一步。Seata的客户端会自动根据业务场景加载合适的模式,开发者只需要引入依赖(比如seata-spring-boot-starter),配置好数据源代理,剩下的事Seata自己搞定。XID的传播也不用手动操心,后面讲拦截器时会细说。这种“无感化”设计,才是当今分布式事务框架的趋势。


二、封装RequestInterceptor:从“啥都不管”到“智能拦截”

接下来聊聊RequestInterceptor的封装思路。Seata里,XID需要在服务间传递,不然分布式事务没法连起来。咱们从最简单的想法开始。

朴素策略:啥都不拦截,手动加Header

最开始,我可能会这么想:每次调用下游服务时,手动在HTTP请求头里塞个XID,就像这样:

httpClient.addHeader(RootContext.KEY_XID, RootContext.getXID());

代码跑起来没问题,但用着用着就发现不对劲:

  1. 重复劳动:每个接口调用都要写一遍,太累了吧。
  2. 漏传风险:万一哪个开发忘了加Header,事务链就断了。
  3. 性能隐患:每次都手动查RootContext.getXID(),逻辑零散,效率也不高。

优化一步:统一拦截

既然手动加Header这么麻烦,那为啥不弄个拦截器统一处理呢?我就写了个SeataRequestInterceptor

@Component
public class SeataRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        String currentXid = RootContext.getXID();
        if (StrUtil.isNotBlank(currentXid)) {
            template.header(RootContext.KEY_XID, currentXid);
        }
    }
}

这样,所有HTTP请求都会自动带上XID,调用方完全不用操心。但这还没完,跑了一段时间,又发现新问题:

  • 无差别拦截:有些请求(比如权限校验)根本不需要XID,平白增加开销。
  • 耦合感强:代码里直接引用了RootContext,不够灵活。

逼近主流:条件过滤 + 解耦

再优化一下,就有了文章开头那段代码的雏形:

if (StrUtil.isNotBlank(currentXid) && !template.url().startsWith(Auth.CHECK_TOKEN_URI) && !template.url().startsWith(Auth.CHECK_RBAC_URI)) {
    template.header(RootContext.KEY_XID, currentXid);
}

这里加了条件,只对需要事务的请求传XID,像校验Token这种无关请求就跳过。至于解耦,可以再抽象一层,把XID获取逻辑抽成接口,方便以后扩展。这种“智能拦截 + 松耦合”的思路,跟Spring Cloud的FeignClient拦截机制不谋而合,也是现在微服务框架的标配。


三、封装GlobalExceptionHandler:从“打印就完”到“精细回滚”

最后聊聊GlobalExceptionHandler。异常处理是个老生常谈的话题,咱们也从最简单的方式开始。

朴素策略:逮住异常就打印

一开始,我可能就这么写:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
    logger.error("出错了", e);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器开小差了");
}

简单,能跑,但问题不少:

  1. 信息不透明:前端只收到一句“服务器开小差”,啥具体错误都不知道。
  2. 事务没处理:分布式环境下,异常不回滚,数据库可能会留一堆脏数据。
  3. 状态码混乱:全用500,前端没法根据状态码做精细处理。

优化一步:分类处理 + 统一响应

意识到这些问题后,我开始分类处理异常,并且统一返回格式:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ServerResponseEntity<List<String>>> handleValidationException(MethodArgumentNotValidException e) {
    List<String> errors = e.getBindingResult().getFieldErrors().stream()
        .map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
        .collect(Collectors.toList());
    return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(ResponseEnum.METHOD_ARGUMENT_NOT_VALID, errors));
}

这样,前端拿到具体错误信息,还能根据ResponseEnum的码做逻辑判断。状态码全用200,错误交给业务码处理,减少了HTTP层面的歧义。

再逼近主流:事务回滚 + 全局兜底

但分布式事务场景下,光返回错误不够,还得管回滚。看看最终代码:

@ExceptionHandler(Exception.class)
public ResponseEntity<ServerResponseEntity<Object>> exceptionHandler(Exception e) throws TransactionException {
    logger.error("exceptionHandler", e);
    if (StrUtil.isNotBlank(RootContext.getXID())) {
        GlobalTransactionContext.reload(RootContext.getXID()).rollback();
    }
    return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(ResponseEnum.EXCEPTION));
}

这步加了事务回滚,只要有XID就触发Seata回滚,保证数据一致性。跟主流方案对齐的地方在于:异常分层处理(参数校验、自定义异常、兜底异常),加上分布式事务的完整支持。现在Spring Cloud Alibaba的异常处理也是这个路子,既优雅又健壮。


四、总结与展望

从朴素到复杂,咱们一步步走过来,发现问题、解决问题,最后对齐主流方案。Seata的模式设置从硬编码到动态配置,拦截器从手动塞Header到智能过滤,异常处理从简单打印到事务回滚,每一步都在解决“简单”带来的短板。未来还可以往哪些方向优化呢?比如:

  • 配置热加载:用配置中心实现零重启切换模式。
  • 拦截器插件化:支持动态插拔拦截逻辑。
  • 异常监控:集成Sentry或SkyWalking,实时追踪异常。

希望这篇博客能给大家点启发,下次聊聊别的实战经验,拜拜!