关注

【SpringCloud合集-05】Seata 分布式事务学习笔记

目录

一、核心认知:Seata 解决什么问题

1.1 分布式事务的本质痛点

1.2 四种事务模式对比

1.3 和其他分布式事务方案的区别

二、核心架构与三大角色

2.1 三大核心角色

2.2 整体执行流程

三、Spring Cloud 整合 AT 模式实战

3.1 前置准备

3.2 项目依赖引入

3.3 配置文件

3.4 业务代码改造

四、AT 模式核心原理

4.1 两阶段执行机制

一阶段:业务执行 + 生成回滚日志

二阶段:提交 / 回滚

4.2 undo_log 回滚日志结构

4.3 全局锁与写隔离

4.4 读隔离

五、TCC 模式补充说明

5.1 什么是 TCC

5.2 和 AT 的区别

5.3 TCC 三大经典问题

六、生产环境部署与配置

6.1 TC 高可用集群

6.2 存储模式选择

6.3 常用参数调优

七、项目踩坑汇总

1. 回滚失败

2. 数据源代理失效

3. 事务传播问题

4. Feign 调用 XID 丢失

5. 大事务性能差

6. 异常没回滚

八、复习速记


(基于 Spring Cloud Alibaba 2.2.9.RELEASE + Seata 1.5.2 官方适配版本整理,覆盖 AT/TCC 模式、源码原理、生产踩坑)

版本说明:根据 Spring Cloud Alibaba 官方组件版本对应关系,2.2.9.RELEASE 原生适配的 Seata 版本为 1.5.2,该版本修复了 1.4.x 系列的若干安全与稳定性问题,新增了 XID 一致性负载均衡、服务端内置控制台等能力,是 2.2.x 生态下的官方推荐生产版本。

一、核心认知:Seata 解决什么问题

1.1 分布式事务的本质痛点

单体应用里,多个操作在同一个数据库中,一个本地事务就能保证要么全成功要么全失败。但微服务架构下,一个业务链路会跨多个服务、多个数据库,比如下单流程:订单服务创订单 → 库存服务扣库存 → 账户服务扣余额,三个服务三个库,本地事务管不了,就会出现订单创了但库存没扣、钱扣了订单没生成的不一致问题。

Seata(Simple Extensible Autonomous Transaction Architecture)就是阿里开源的分布式事务解决方案,专门解决微服务场景下跨服务、跨数据库的数据一致性问题,是 Spring Cloud Alibaba 生态的标准事务组件。

它的核心设计目标:

  • 对业务代码低侵入,尽量不改变原有业务写法
  • 支持多种事务模式,适配不同业务场景
  • 高性能,尽量降低分布式事务带来的性能损耗
  • 可视化管理,支持事务监控、异常回查、人工处理

1.2 四种事务模式对比

Seata 一共提供了 4 种事务模式,对应不同的业务场景,不是所有场景都用同一种:

模式原理业务侵入性一致性强度适用场景
AT 模式自动生成回滚日志,二阶段提交 / 回滚,基于本地事务 + 全局锁极低,几乎不用改业务代码最终一致绝大多数基于关系型数据库的业务,追求开发效率
TCC 模式手动实现 Try-Confirm-Cancel 三个方法,补偿型事务高,每个事务都要写三个方法最终一致非关系型数据库、需要自定义补偿逻辑、性能要求极高的场景
SAGA 模式长事务拆分,正向执行 + 反向补偿,失败时按顺序回滚中,需要写补偿方法最终一致长链路事务、参与服务多、业务流程长的场景
XA 模式基于数据库 XA 协议,强一致两阶段提交强一致对一致性要求极高、并发量不高的金融类场景

实际项目里 90% 以上都用 AT 模式,也是 Seata 的主推模式;TCC 用在特殊场景;SAGA 和 XA 用得很少。下面笔记重点讲 AT,补充 TCC。

1.3 和其他分布式事务方案的区别

  • 可靠消息最终一致性:基于 MQ 实现,适合异步场景,一致性弱,延迟高,优点是性能好。Seata AT 是同步调用,一致性更强,链路更直观。
  • 最大努力通知:适合对一致性要求很低的场景,比如短信通知、日志记录。
  • 2PC/3PC 传统方案:资源锁定时间长,性能差,可用性低。Seata AT 优化了锁的粒度和持有时间,性能好很多。

二、核心架构与三大角色

2.1 三大核心角色

Seata 的架构里有三个核心组件,各司其职:

  1. TC(Transaction Coordinator)事务协调器 独立部署的服务端,负责管理全局事务的状态,接收 TM 的注册、提交、回滚请求,协调各个 RM 执行分支事务的提交或回滚。相当于全局事务的总指挥。

    1.5.2 版本 TC 已内置可视化控制台,默认端口 7091,无需单独部署控制台组件,支持事务状态查询、锁信息查看、手动回滚等运维能力。

  2. TM(Transaction Manager)事务管理器 在业务发起方的应用里,负责开启全局事务,发起全局提交或回滚的指令。一般加了 @GlobalTransactional 注解的方法所在的服务就是 TM。

  3. RM(Resource Manager)资源管理器 在每个参与事务的业务服务里,负责管理本地分支事务,和 TC 通信,上报分支事务状态,执行本地事务的提交和回滚。每个操作数据库的服务都是 RM。

2.2 整体执行流程

以 AT 模式下单链路为例,整体交互流程如下:

核心特点:一阶段就把本地事务提交了,释放数据库本地锁,只持有全局锁;二阶段提交就删日志,回滚就用日志补偿。这是 Seata AT 性能比传统 2PC 高的核心原因。

三、Spring Cloud 整合 AT 模式实战

3.1 前置准备

1.部署 Seata Server(TC) 下载 1.5.2 版本 Seata 服务端,修改配置文件指定注册中心(Nacos)和存储模式。1.5.x 版本同时支持 file.conf/registry.conf 传统配置和 application.yml 统一配置,生产推荐使用 YAML 配置。启动后可通过 http://tc地址:7091 访问内置控制台。

2.每个业务库都要建 undo_log 表 AT 模式依赖回滚日志表,每个参与分布式事务的数据库都必须建这张表,这是最容易漏的一步。1.5.x 版本表结构与 1.4.x 完全兼容,核心字段无变化。

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 DEFAULT CHARSET=utf8;

3.TC 服务端 DB 模式建表 如果 TC 使用 db 存储模式,还需要在 TC 的数据库中创建全局事务表、分支事务表、锁表等服务端表结构,1.5.2 官方建表语句可在发布包的 conf/db 目录下获取。

3.2 项目依赖引入

每个参与事务的服务都引入 Seata 依赖,版本由 Spring Cloud Alibaba 父工程统一管理,无需单独指定:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

版本说明:Spring Cloud Alibaba 2.2.9.RELEASE 会自动依赖 io.seata:seata-all:1.5.2,无需手动指定版本,强行指定其他版本容易出现类不兼容、注册失败等问题。

3.3 配置文件

每个服务的 application.yml 里配置 Seata,1.5.x 完全兼容原有配置项:

spring:
  cloud:
    alibaba:
      seata:
        # 事务分组名称,要和服务端配置对应
        tx-service-group: my_test_tx_group
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/order_db
    username: root
    password: 123456

seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: public
      group: SEATA_GROUP
  service:
    vgroup-mapping:
      # 事务分组映射到TC集群名
      my_test_tx_group: default

3.4 业务代码改造

AT 模式对业务代码侵入极小,只需要在全局事务的发起方法上加一个注解就行,下游服务不用加任何事务相关代码。

以订单服务为发起方为例:

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockFeignClient stockFeignClient;
    @Autowired
    private AccountFeignClient accountFeignClient;

    /**
     * 全局事务注解:开启分布式事务
     * 只需要加在事务发起的入口方法上
     * rollbackFor = Exception.class 表示所有异常都回滚
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public void createOrder(Long userId, Long goodsId, Integer count) {
        // 1. 本地创建订单
        Order order = new Order();
        order.setUserId(userId);
        order.setGoodsId(goodsId);
        order.setCount(count);
        order.setAmount(new BigDecimal(100).multiply(new BigDecimal(count)));
        orderMapper.insert(order);

        // 2. 远程调用库存服务,扣库存
        stockFeignClient.deductStock(goodsId, count);

        // 3. 远程调用账户服务,扣余额
        accountFeignClient.deductBalance(userId, order.getAmount());

        // 三个操作全部成功,全局事务自动提交
        // 任意一步抛异常,全局事务自动回滚
    }
}

下游的库存服务、账户服务,正常写业务就行,不用加任何 Seata 注解,RM 会自动拦截 SQL、生成回滚日志、和 TC 通信。

踩坑提醒:@GlobalTransactional 只能加在事务发起方,不能每个服务都加;下游服务如果有本地事务,用 @Transactional 就行,不要加全局事务注解。

四、AT 模式核心原理

4.1 两阶段执行机制

AT 模式的核心是改进版的两阶段提交,和传统 2PC 最大的区别是一阶段就提交本地事务。

一阶段:业务执行 + 生成回滚日志
  1. RM 拦截业务 SQL,解析 SQL 语义,找到要修改的数据
  2. 查询修改前的数据镜像(before image),保存下来
  3. 执行业务 SQL,更新数据
  4. 查询修改后的数据镜像(after image),保存下来
  5. 把前后镜像、SQL 信息组装成回滚日志,写入 undo_log 表
  6. 向 TC 注册分支事务,申请全局锁
  7. 本地事务提交,释放数据库本地锁
  8. 上报分支事务状态给 TC

关键:一阶段结束后,本地事务已经提交了,数据库的行锁、表锁都释放了,只在 TC 里持有全局锁。其他本地事务可以修改别的行,并发性能大大提升。

二阶段:提交 / 回滚
  • 全局提交:如果所有分支都成功,TM 通知 TC 全局提交。TC 通知各个 RM 删除对应的 undo_log 记录,释放全局锁。提交过程非常快,只是删日志。
  • 全局回滚:如果任一环节失败,TM 通知 TC 全局回滚。TC 通知各个 RM,根据 undo_log 里的前镜像数据,生成反向的 SQL,执行回滚,把数据恢复到修改前的状态,然后释放全局锁。

4.2 undo_log 回滚日志结构

回滚日志是 AT 模式能自动回滚的核心,里面存了数据修改前后的完整镜像:

  • xid:全局事务 ID,唯一标识一个全局事务
  • branch_id:分支事务 ID,每个服务的分支事务唯一
  • rollback_info:核心字段,二进制存储,包含前后镜像数据、SQL 信息、表结构等
  • log_status:日志状态,正常、回滚中、回滚完成等

回滚的时候,Seata 会根据前镜像和业务 SQL,自动生成反向的 UPDATE/DELETE/INSERT 语句,把数据还原。比如原来执行的是 update stock set count = count - 1 where id = 1,回滚就执行 update stock set count = count + 1 where id = 1

4.3 全局锁与写隔离

有个问题:一阶段就提交了本地事务,别的事务修改了同一条数据怎么办?这就是全局锁的作用。

  • 本地锁:数据库本身的行锁,一阶段执行 SQL 时持有,本地事务提交就释放
  • 全局锁:Seata TC 维护的锁,记录哪些数据行正在被全局事务修改,全局事务结束才释放

写隔离保证:

  1. 本地事务修改数据前,先拿本地锁
  2. 提交本地事务前,要先拿到全局锁
  3. 拿不到全局锁,就不能提交本地事务,会重试等待
  4. 全局事务没结束,全局锁不释放,其他事务修改同一条数据拿不到全局锁,就提交不了

这样就保证了:被全局事务修改的数据,在全局事务结束前,不会被别的事务改掉,避免了脏写。

4.4 读隔离

默认情况下,Seata AT 模式的读隔离级别是读未提交,因为一阶段本地事务就提交了,别的事务能读到还没最终提交的中间状态。

如果需要读已提交,可以用 select for update 语句,Seata 会拦截这条语句,先去申请全局锁,拿不到锁就等待,保证读到的是全局事务提交后的数据。不过会降低性能,业务里一般很少用,大部分场景能接受短暂的读未提交。

五、TCC 模式补充说明

5.1 什么是 TCC

TCC 是 Try-Confirm-Cancel 的缩写,完全靠业务代码实现补偿,不依赖数据库事务,也不需要 undo_log 表。

  • Try 阶段:预留资源,做检查,比如冻结库存、冻结余额,不真正扣减
  • Confirm 阶段:确认执行,真正扣减冻结的资源
  • Cancel 阶段:取消执行,把冻结的资源释放回去

5.2 和 AT 的区别

  • AT 是声明式的,自动生成回滚日志,侵入性低,只能用在支持事务的关系型数据库上
  • TCC 是编程式的,全靠自己写三个方法,侵入性高,但不挑数据库,Redis、Mongo 都能用,性能也更高

5.3 TCC 三大经典问题

  1. 幂等性:Confirm 和 Cancel 可能会被重复调用,必须保证多次调用结果一样,不能重复扣减
  2. 空回滚:Try 方法没执行到(比如网络丢包),但收到了 Cancel 请求,这时候不能报错,要直接返回成功
  3. 悬挂:Cancel 比 Try 先执行了(网络乱序),之后 Try 再执行就会预留资源没人释放,导致资源永久冻结

解决方法一般是建一张事务控制表,记录每个分支事务的状态:未执行、Try 成功、Confirm 成功、Cancel 成功,执行每个方法前先查状态,判断能不能执行。

六、生产环境部署与配置

6.1 TC 高可用集群

生产环境不能单点部署 TC,必须做集群:

  • 至少部署 3 个 TC 节点,注册到 Nacos 上,组成集群
  • 存储模式用 db 模式,全局事务、分支事务、锁信息都存在 MySQL 里,保证 TC 宕机数据不丢
  • 客户端通过注册中心发现 TC 节点,自动负载均衡,单节点宕机自动切换

6.2 存储模式选择

模式说明适用场景
file数据存在本地文件,性能好单机测试、演示
db数据存在 MySQL,持久化生产集群,保证数据可靠
redis数据存在 Redis,性能高高并发、对持久化要求不高的场景

生产推荐 db 模式,稳定可靠,数据不丢。

6.3 常用参数调优

  • client.rm.report-success-enable=false:关闭分支成功上报,减少 TC 压力,只上报失败
  • client.rm.lock.retry-times:全局锁重试次数,根据业务场景调整
  • server.max.commit.retry.timeout:提交重试超时时间
  • server.max.rollback.retry.timeout:回滚重试超时时间

七、项目踩坑汇总

1. 回滚失败

最常见的问题,原因一般有几个:

  • 数据库里没建 undo_log 表,或者表结构不对
  • 业务表没有主键,Seata 无法定位数据行,生成不了回滚 SQL
  • 事务过程中修改了主键,回滚的时候找不到数据
  • 其他事务手动修改了数据,导致前后镜像对不上,回滚校验失败

2. 数据源代理失效

Seata AT 必须代理数据源才能拦截 SQL,如果项目里手动配置了数据源,没被 Seata 代理,就会完全不生效,事务不回滚。

解决:确保用了 Spring Boot 自动配置的数据源,或者手动配置 DataSourceProxy 代理原始数据源。

3. 事务传播问题

@GlobalTransactional 不支持嵌套,内部方法再加注解不会开启新的全局事务。跨服务调用靠 Feign 自动传递 XID,本地调用如果不是跨服务,不会走分支事务。

4. Feign 调用 XID 丢失

默认情况下 Seata 会自动通过请求头传递 XID,但如果自定义了 Feign 拦截器、或者用了异步线程,XID 就会丢失,导致下游服务不参与全局事务。

解决:手动在拦截器里把 XID 放到请求头里,异步场景手动传递 XID。

5. 大事务性能差

全局事务链路越长、涉及的服务越多,锁持有时间就越长,并发性能越差,还容易出现死锁。

最佳实践:尽量缩短全局事务链路,非核心操作放到事务外;能不用分布式事务就不用,能用本地事务 + 可靠消息解决的就不用 Seata。

6. 异常没回滚

@GlobalTransactional 默认只回滚 RuntimeException 和 Error,如果抛出了检查型异常(比如 IOException)不会回滚。

解决:注解加上 rollbackFor = Exception.class,所有异常都回滚,和 Spring 的 @Transactional 规则一样。


八、复习速记

  1. 三大角色:TC 协调器管全局状态,TM 管理器开启 / 提交事务,RM 资源管理器管本地分支。
  2. AT 核心:一阶段执行业务 + 生成回滚日志 + 提交本地事务 + 持全局锁;二阶段提交删日志,回滚用日志反向补偿。
  3. 必备条件:每个业务库都要有 undo_log 表,业务表要有主键,数据源要被 Seata 代理。
  4. 写隔离:靠 TC 维护的全局锁保证,修改数据要先拿全局锁,避免脏写。
  5. TCC 三问题:幂等、空回滚、悬挂,用事务状态表解决。
  6. 生产建议:TC 集群部署,db 存储模式;尽量缩短事务链路,不要滥用分布式事务。
  7. 注解位置@GlobalTransactional 只加在事务发起方的入口方法上,下游服务不用加。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_44648243/article/details/162848362

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--