微服务学习-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 可以解决微服务分布式事务的问题。

相关推荐
WeiLai111228 分钟前
面试基础--微服务架构:如何拆分微服务、数据一致性、服务调用
java·分布式·后端·微服务·中间件·面试·架构
Alidme1 小时前
cs106x-lecture14(Autumn 2017)-SPL实现
c++·学习·算法·codestepbystep·cs106x
奔跑吧邓邓子1 小时前
【Python爬虫(44)】分布式爬虫:筑牢安全防线,守护数据之旅
开发语言·分布式·爬虫·python·安全
小王努力学编程1 小时前
【算法与数据结构】单调队列
数据结构·c++·学习·算法·leetcode
ZxsLoves1 小时前
【【Systemverilog学习参考 简单的加法器验证-含覆盖率】】
学习·fpga开发
明阳mark2 小时前
Ansible 学习笔记
笔记·学习·ansible
~kiss~2 小时前
python的thrift2pyi学习
windows·python·学习
转身後 默落2 小时前
11.Docker 之分布式仓库 Harbor
分布式·docker·容器
奔跑吧邓邓子2 小时前
【Python爬虫(45)】Python爬虫新境界:分布式与大数据框架的融合之旅
开发语言·分布式·爬虫·python·大数据框架
m0_748232922 小时前
分布式与集群,二者区别是什么?
分布式