微服务学习-Seata 解决分布式事务

1. 为什么要使用分布式事务?

1.1. 问题重现

使用微服务架构,当账户余额为 0 时,还可以继续下单,而且扣减库存;或者当库存不足时,也可以下单继续扣减余额等问题,造成数据不一致。

1.2. 新的需求

下单逻辑需要保证数据一致性,当账户余额不够或者库存不足,该回滚库存回滚库存,该回滚账户回滚账户,让当前下单失败。

1.3. 解决方案

思考:使用 Spring 事务能解决问题吗? 不能;

使用分布式事务解决方案 Seata(官方推荐)

2. Seata 是什么?

官方文档:Seata 是什么? | Apache Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

首选 Seata AT 模式(官方推荐),可以做到业务无侵入。

Seata AT 模式 | Apache Seata

3. Seata AT 模式的工作流程

3.1. 非常重要的三个概念

  • TC(Transaction Coordinator):事务协调器,负责全局事务的管理,包括事务的开启、提交、回滚等操作,它会记录全局事务和分支事务的状态信息。
  • TM(Transaction Manager):事务管理器,主要用于开启、提交或回滚全局事务。在应用代码中,开发人员通过 TM 来定义全局事务的边界。
  • RM(Resource Manager):资源管理器,负责分支事务的管理,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

例如:当用户通过订单服务下单,调用库存服务扣减库存,调用账户服务扣减账户余额。

订单服务要接入 TM 组件,下单操作需要开启全局事务(向 TC 申请一个全局事务 XID),进入下单逻辑,如果下单正常,需要通知 TC 提交全局事务;如果下单出现异常,比如余额不够,需要通知 TC 回滚全局事务。

订单服务、库存服务、账户服务都需要接入 RM 组件,提交本地事务的同时,需要向 TC 注册分支事务信息,接受 TC 的通知,提交或回滚分支事务。

TC 是独立的服务,维护 TM 申请的全局事务信息和 RM 提交的分支事务信息,TM 通知 TC 全局事务提交或回滚的时候, TC 要通知 RM 分支事务提交或回滚。

3.2. AT 模式工作流程

问题:如何知道调用的是同一个分布式事务?

开启全局事务会分配一个全局事务 XID,微服务链路会传递 XID,注册每个分支事务都会带上 XID。

4. Seata Server(TC)安装部署

4.1. 注意版本

Seata Version:2.0.0

4.2. 下载地址

Seata Java Download | Apache Seata

4.3. 官网参考资料

4.3.1. Seata 新手部署指南

新人文档 | Apache Seata

4.3.2. Seata 官网参数配置

参数配置 | Apache Seata

4.4. TC 端存储模式

全局事务、分支事务信息存储到哪里了?

4.4.1. Seata 1.x 支持的模式
4.4.1.1. file:单机模式

全局事务、分支事务信息内存中读写并持久化本地文件 root.data,性能较高,但是只支持单机模式部署,生产环境不考虑。

4.4.1.2. db:高可用模式

全局事务、分支事务信息通过 db 共享,相应性能差点。

4.4.1.3. redis:1.3及以上版本支持,性能较高

存在事务信息丢失风险,请提前配置适合当前场景的 redis 持久化配置。

4.4.2. Seata2.x 新增的 Raft 模式

利用 Raft 算法实现多个 TC 之间数据的同步。Raft 模式是最理想的方案,但当前并不成熟,所以不考虑。

4.4.3. 从稳定性角度考虑,最终选择采用 db 模式

创建 Seata 数据库,sql 脚本在 seata 目录中

例如:seata-server-2.0.0\seata\script\server\db\mysql.sql

4.5. 思考: RM 和 TM 如何找到 TC 服务的?

可以将 TC 注册到 Nacos 注册中心,TM 和 RM 通过 Nacos 注册中心实现 TC 服务的发现。

注意:Seata 的注册中心是作用于 Seata 自身的,和微服务中的配置的注册中心无关,但可以共用注册中心。可以创建一个 Seata 的命名空间,区分 Seata 的 TC 服务和业务微服务。

4.6. 思考: TC 的配置是不是也可以交给 Nacos 配置中心配置?

可以。

4.7. 最终方案:db 存储模式 + Nacos(注册&配置中)方式部署

4.7.1. 前置环境准备
4.7.1.1. db 模式准备好 seata 的数据库

例如:seata-server-2.0.0\seata\script\server\db\mysql.sql

4.7.1.2. 准备好 Nacos 环境

Nacos 控制台中创建一个 seata 的命名空间

4.7.2. Seata 配置融合 Nacos 注册中心

配置将 Seata Server 注册到 Nacos 中。主要配置 seata 的配置文件(路径:seata-server-2.0.0\seata\conf\application.yml)

seata:
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: icoolkj-mall-nacos-server:8848
      namespace: seata
      group: SEATA_GROUP
      cluster: default

注意:

  • 这个 cluster 配置,默认 TC 是 default 集群,TM 和 RM 都要通过这个集群名找 TC 集群。
  • 请确保client(RM 和 TM)与 server(TC)的注册出于同一个 namespace 和 group,不然会找不到 server(TC)服务。

4.8. Seata 配置融合 Nacos 配置中心

4.8.1.1. 配置 Seata 使用 Nacos 配置中心

主要配置 seata 的配置文件(路径:seata-server-2.0.0\seata\conf\application.yml)

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: icoolkj-mall-nacos-server:8848
      namespace: seata
      group: SEATA_GROUP
      data-id: seataServer.properties
4.8.1.2. 将 seata server 的配置上传至 Nacos 配置中心
    1. 获取 seata server 配置信息(文件路径:seata\script\config-center\config.txt)
    2. 修改为 db 存储模式,并修改 mysql 连接配置

    #Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
    store.mode=db
    store.lock.mode=db
    store.session.mode=db
    #Used for password encryption
    store.publicKey=

    #These configurations are required if the store mode is db. If store.mode,store.lock.mode,store.session.mode are not equal to db, you can remove the configuration block.
    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.cj.jdbc.Driver
    store.db.url=jdbc:mysql://icoolkj-mall-mysql:33060/seata2.0.0?useSSL=false&characterEncoding=utf8&useUnicode=true&rewriteBatchedStatements=true
    store.db.user=root
    store.db.password=icoolDP1988
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.distributedLockTable=distributed_lock
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000

    1. 配置事务分组,TC 要与 client(TM 和 RM)配置的事务分组一致
  • 事务分组:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。

  • 集群:seata-server 服务端一个或多个节点组成的集群 cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与 Seata 服务端集群的映射关系。

    service.vgroupMapping.default_tx_group=default

注意:事务分组如何找到后端的 Seata 集群?

事务分组介绍 | Apache Seata

    1. 在 Nacos 控制台 seata 命名空间下新建 dataId 为 seataServer.properties 配置,配置内容为修改后的 config.txt 信息。

    #Transport configuration, for client and server
    transport.type=TCP
    transport.server=NIO
    transport.heartbeat=true
    transport.enableTmClientBatchSendRequest=false
    transport.enableRmClientBatchSendRequest=true
    transport.enableTcServerBatchSendResponse=false
    transport.rpcRmRequestTimeout=30000
    transport.rpcTmRequestTimeout=30000
    transport.rpcTcRequestTimeout=30000
    transport.threadFactory.bossThreadPrefix=NettyBoss
    transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
    transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
    transport.threadFactory.shareBossWorker=false
    transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
    transport.threadFactory.clientSelectorThreadSize=1
    transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
    transport.threadFactory.bossThreadSize=1
    transport.threadFactory.workerThreadSize=default
    transport.shutdown.wait=3
    transport.serialization=seata
    transport.compressor=none

    #Transaction routing rules configuration, only for the client
    service.vgroupMapping.default_tx_group=default
    #If you use a registry, you can ignore it
    #service.default.grouplist=127.0.0.1:8091
    #service.enableDegrade=false
    #service.disableGlobalTransaction=false

    client.metadataMaxAgeMs=30000
    #Transaction rule configuration, only for the client
    client.rm.asyncCommitBufferLimit=10000
    client.rm.lock.retryInterval=10
    client.rm.lock.retryTimes=30
    client.rm.lock.retryPolicyBranchRollbackOnConflict=true
    client.rm.reportRetryCount=5
    client.rm.tableMetaCheckEnable=true
    client.rm.tableMetaCheckerInterval=60000
    client.rm.sqlParserType=druid
    client.rm.reportSuccessEnable=false
    client.rm.sagaBranchRegisterEnable=false
    client.rm.sagaJsonParser=fastjson
    client.rm.tccActionInterceptorOrder=-2147482648
    client.tm.commitRetryCount=5
    client.tm.rollbackRetryCount=5
    client.tm.defaultGlobalTransactionTimeout=60000
    client.tm.degradeCheck=false
    client.tm.degradeCheckAllowTimes=10
    client.tm.degradeCheckPeriod=2000
    client.tm.interceptorOrder=-2147482648
    client.undo.dataValidation=true
    client.undo.logSerialization=jackson
    client.undo.onlyCareUpdateColumns=true
    server.undo.logSaveDays=7
    server.undo.logDeletePeriod=86400000
    client.undo.logTable=undo_log
    client.undo.compress.enable=true
    client.undo.compress.type=zip
    client.undo.compress.threshold=64k
    #For TCC transaction mode
    tcc.fence.logTableName=tcc_fence_log
    tcc.fence.cleanPeriod=1h

    You can choose from the following options: fastjson, jackson, gson

    tcc.contextJsonParserType=fastjson

    #Log rule configuration, for client and server
    log.exceptionRate=100

    #Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
    store.mode=db
    store.lock.mode=db
    store.session.mode=db
    #Used for password encryption
    store.publicKey=

    #These configurations are required if the store mode is db. If store.mode,store.lock.mode,store.session.mode are not equal to db, you can remove the configuration block.
    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.cj.jdbc.Driver
    store.db.url=jdbc:mysql://icoolkj-mall-mysql:3306/seata2.0.0?useUnicode=true&rewriteBatchedStatements=true&useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true
    store.db.user=root
    store.db.password=123456
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.distributedLockTable=distributed_lock
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000

    #Transaction rule configuration, only for the server
    server.recovery.committingRetryPeriod=1000
    server.recovery.asynCommittingRetryPeriod=1000
    server.recovery.rollbackingRetryPeriod=1000
    server.recovery.timeoutRetryPeriod=1000
    server.maxCommitRetryTimeout=-1
    server.maxRollbackRetryTimeout=-1
    server.rollbackRetryTimeoutUnlockEnable=false
    server.distributedLockExpireTime=10000
    server.session.branchAsyncQueueSize=5000
    server.session.enableBranchAsyncRemove=false
    server.enableParallelRequestHandle=true
    server.enableParallelHandleBranch=false

    #Metrics configuration, only for the server
    metrics.enabled=false
    metrics.registryType=compact
    metrics.exporterList=prometheus
    metrics.exporterPrometheusPort=9898

注意:在 seata 命名空间下建立的 seataServer.properties 配置,要和 seata 的配置文件(seata-server-2.0.0\seata\conf\application.yml)的 config 配置要对应上,特别注意 seataServer.properties 是否是 SEATA_GROUP。

4.9. 启动 Seata Server

  • windows 点击 bin 目录下 seata-server.bat 直接启动。
  • 启动成功,查看控制台 http://localhost:7091,账号密码:seata。
  • 在 Naocs 控制台,查看 seata-server 注册情况。

5. 微服务整合 Seata AT 模式实战

5.1. 业务场景

用户下单,订单服务调用库存服务扣减库存,调用账户服务扣减账户余额。

事务发起者:订单服务。

事务参与者:库存服务,账户服务,商品服务。

5.2. 订单服务(事务发起者)整合 Seata

5.2.1. 订单服务 pom.xml 引入 Seata 的依赖
<!-- seata 依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
5.2.2. 订单服务对应数据库中添加 undo_log 表(仅支持 AT 模式)
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
5.2.3. 订单服务 application.yml 中添加 seata 配置
seata:
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: icoolkj-mall-nacos-server:8848
      namespace: seata
      group: SEATA_GROUP

  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr: icoolkj-mall-nacos-server:8848
      namespace: seata
      group: SEATA_GROUP
      data-id: seataServer.properties

优化:可以将 seata 配置移到 Nacos 配置中心。

    1. Nacos 控制台创建一个 dataId 为 seata-client.yml 的配置,配置内容如下:

    seata:

    seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应

    tx-service-group: default_tx_group
    registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
    application: seata-server
    server-addr: icoolkj-mall-nacos-server:8848
    namespace: seata
    group: SEATA_GROUP

    config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
    server-addr: icoolkj-mall-nacos-server:8848
    namespace: seata
    group: SEATA_GROUP
    data-id: seataServer.properties

    1. 订单微服务的 application.yml 引入 seata-client.yml

    spring:
    config:
    import:
    - optional:nacos:${spring.application.name}.yml
    - optional:nacos:db-mysql-common.yml # mysql数据库公共配置
    - nacos:nacos-discovery.yml
    - optional:nacos:seata-client.yml # Seata Client 配置

5.2.4. 订单服务作为全局事务发起者,在下单的方法上面添加 @GlobalTransactional 注解
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
//@Transactional
public Result<?> createOrder(Long userId, Long productId, Integer orderQuantity) {

5.3. 库存服务(事务参与者)整合 Seata

5.3.1. 和整合订单服务前三步一样
5.3.2. 库存服务只需要再扣减库存方法上添加 Spring 事务 @Transaction 注解
@Transactional
public void reduceInventory(Long productId, Integer inventoryQuantity) {

5.4. 账户服务(事务参与者)整合 Seata

5.4.1. 和整合订单服务前三步一样
5.4.2. 账户服务只需要再扣减余额方法上添加 Spring 事务 @Transaction 注解
@Transactional
public void reduceBalance(Long userId, BigDecimal orderCost) {

5.5. 商品服务(事务参与者)整合 Seata

5.5.1. 和整合订单服务前三步一样
5.5.2. 订单调用商品服务的查询商品价格,无需增加事务

5.6. Seata2.x 常见问题

5.6.1. 微服务启动报错

io.seata.conf.exception.ConfigNotFoundException:service.vgroupMapping.default_tx_group configuration itme is required

产生原因:无法拉取到 service.vgroupMapping.default_tx_proup=default 这个配置,也就是找不到集群名称为 default 的 seata server 服务。

解决方式:

思路1:检查下微服务 seata 配置是否未配置事务分组 seata.tx-service-group: default_tx_group

思路2:检查下 namespace 和 group 配置 Server 端和 Client 端是否对应,特别注意 seataSever.properties 是否是 SEATA_GROUP

6. 重启所有服务,测试分布式事务是否生效

6.1. 分布式事务成功场景:模拟正常下单,扣库存,扣余额

6.2. 分布式事务失败场景:模拟下单扣库存成功,扣余额失败,事务是否回滚

6.3. 目前 seata2.0.0 版本的中存在 bug

事务发生回滚,回滚成功了,但是最外层代码无法捕捉到原始的 BusinessException 异常,只能捕捉到 RuntimeException 运行时异常。

建议不要在生产上使用该版本。

7. 小结

通过 seata 可以解决微服务分布式事务的问题。

相关推荐
楠了个难13 分钟前
以太网实战AD采集上传上位机——FPGA学习笔记27
笔记·学习·fpga开发
eyuhaobanga33 分钟前
Go入门学习笔记
笔记·学习·golang
东小黑1 小时前
java方法以及与C语言对比学习
java·c语言·学习
筑梦之路1 小时前
kafka学习笔记1 —— 筑梦之路
笔记·学习·kafka
筑梦之路1 小时前
kafka学习笔记5 PLAIN认证——筑梦之路
笔记·学习·kafka
筑梦之路1 小时前
kafka学习笔记2 —— 筑梦之路
笔记·学习·kafka
Xudde.2 小时前
100条Linux命令汇总
linux·运维·笔记·学习
eyuhaobanga2 小时前
高质量编程 & 性能优化学习笔记
笔记·学习·性能优化
王子良.3 小时前
使用 Hadoop 实现大数据的高效存储与查询
大数据·hadoop·分布式
可可鸭~4 小时前
鸿蒙学习构建视图的基本语法(二)
android·学习·harmonyos