Skip to content

分布式事务

视频地址: https://www.bilibili.com/video/BV1Q4411y7ip

参考文档: https://blog.csdn.net/hancoder/article/details/120213532

建议看一下旗下的视频课件文档

一、基础概念

CAP 理论

CAP理论是分布式计算中的一个重要理论,由Eric Brewer提出。CAP指的是Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性)。理论声称,一个分布式系统不可能同时满足这三个要求,最多只能同时满足其中两项:

  1. 一致性(C):所有节点在同一时间具有相同的数据。
  2. 可用性(A):保证每个请求都能得到一个响应,无论响应是成功还是失败。
  3. 分区容忍性(P):系统中任意信息的丢失或失败都不会影响系统的继续运作。

在实际应用中,分区容忍性是必须要保证的,因为网络分区在实际环境中是常见的(在分布式环境中,系统节点之间肯定的需要有网络连接的,因此分区(P) 是必然存在的)。因此,大多数分布式系统设计的抉择通常是在一致性和可用性之间做权衡。

BASE 理论

相对于CAP的严格要求,BASE理论提供了一种较为宽松的事务一致性模型。BASE是Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性)的缩写:

  1. 基本可用(Basically Available):分布式系统在出现故障的时候,允许损失部分可用性——例如,响应时间可能会延长。
  2. 软状态(Soft state):系统的状态不需要时刻一致,允许在不同节点间存在中间状态,而这种状态会随着时间的推移而逐渐一致。
  3. 最终一致性(Eventually consistent):系统保证在一定时间范围内,数据最终将是一致的。

BASE:牺牲强一致性,保证可用性,确保最终一致性。

BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终一致性。满足BASE理论的事务,我们称之为“柔性事务”。

分布式事务

数据库事务回顾:事务的特性是:ACID;

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

我们需要理解的是,什么情况下会出现分布式事务问题:

分布式事务产生的场景

  • 1、典型的场景:微服务架构
    • 微服务之间通过远程调用完成事务操作
      • image.png
    • 跨 JVM 进程产生分布式事务问题
  • 2、单体系统访问多个数据库实例
    • 跨数据库实例产生分布式事务
  • 3、多服务访问同一个数据库实例
    • 跨 JVM 进程
    • 两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。

二、分布式事务解决方案

在Java中处理分布式事务时,常见的方法有以下几种:

  1. 两阶段提交(2PC):是XA事务的一个实现,它分为“准备阶段”和“提交阶段”,确保所有参与者都同意提交事务。这种方式强调的是一致性,但牺牲了系统的可用性。
  2. 补偿事务(TCC,Try-Confirm-Cancel):这种方式首先执行试操作,如果所有参与者都成功,则进行确认操作,否则执行取消操作。它适合于业务逻辑较为复杂的系统。
  3. 最终一致性框架:如 Apache Kafka 和 RocketMQ 等消息中间件,通过异步消息确保最终一致性。这类方法强调的是可用性和分区容忍性。
  4. 分布式事务中间件:例如 Seata ,它通过创建分布式事务协调者来管理各个微服务之间的事务,实现分布式事务的一致性。

2PC 两阶段提交

  • 两阶段:准备阶段 Prepare phase、提交阶段 comomit phase

数据库支持的2pc【二阶段提交】,又叫做 XA Transactions

2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。

XA:强一致性,比较适⽤于执⾏时间确定的短事务,整体性能比较差。

在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议:

    1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
    • (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
    1. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

如果任一资源管理器在第一阶段返回准备失败,那么事务管理器会要求所有资源管理器在第二阶段执行回滚操作。通过事务管理器的两阶段协调,最终所有资源管理器要么全部提交,要么全部回滚,最终状态都是一致的

TCC

Seata

MQ

三、Seata 的使用示例

基础概念

文档: https://seata.apache.org/zh-cn/blog/seata-quick-start/

Seata 是阿里开源的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务

事务模式

image.png

Seata 的角色

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
  • RM ( Resource Manager ) - 资源管理器:管理分支事务处理的资源( Resource ),与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端

image.png

分布式事务的生命周期

image.png

  • TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。
    • XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
  • RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
  • TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
  • TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。

image.png


环境准备

虚拟机安装一下 MySQL 5.7 、Nacos 1.3.2 版本

部署单机 TC Server

TC 需要进行全局事务和分支事务的记录,所以需要对应的存储。目前,TC 有两种存储模式( store.mode ):

  • file 模式:适合单机模式,全局事务会话信息在内存中读写,并持久化本地文件 root.data,性能较高。
  • db 模式:适合集群模式,全局事务会话信息通过 db 共享,相对性能差点。

这个不用管,写的有点问题 👆

Seata 服务

根据项目的介绍文档,这里下载 1.3 版本的 seata 的服务

这里先不用 docker 的方式,有点理解问题,先使用一下 windows 下的 zip 方式

参考: https://github.com/WinterChenS/spring-cloud-hoxton-study/blob/main/doc/SpringCloud系列教程(八)之整合seata分布式事务.md

下载地址: https://github.com/apache/incubator-seata/releases?page=2

window下载zip,linux/mac下载tar.gz

image.png

前期准备

这里就下载一下这个项目,并将数据库文件执行一下,windows 的相关操作先不管

看到一个很不错的项目,这里使用一下它:https://github.com/YunaiV/SpringBoot-Labs 的 labx-17 目录

seata + openfeign

改一下这个 pom 文件

<!--    <parent>-->  
<!--        <artifactId>labx-17</artifactId>-->  
<!--        <groupId>cn.iocoder.springboot.labs</groupId>-->  
<!--        <version>1.0-SNAPSHOT</version>-->  
<!--    </parent>-->  
    <groupId>cn.iocoder.springboot.labs</groupId>  
    <artifactId>labx-17-sc-seata-at-feign-demo</artifactId>  
    <packaging>pom</packaging>  
    <version>1.0-SNAPSHOT</version>  
    <modelVersion>4.0.0</modelVersion>

执行 data.sql 文件

部署过程看一下这个视频,挺详细的: https://www.bilibili.com/video/BV1wW4y147k3

改一下这两个文件的配置内容

image.png

file.conf

修改配置为通过 db 的方式,改一下 mysql 相关的配置文件内容

image.png

对应数据库中,执行以下脚本文件: https://github.com/apache/incubator-seata/blob/1.3.0/script/server/db/mysql.sql

先创建一下数据库

image.png

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

registry.conf

改一下配置中心和注册中心的配置内容

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "192.168.56.105:8848"
    group = "SEATA_GROUP"
	#不写默认是 public
    namespace = ""  
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  ...
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "192.168.56.105:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties" 
  }
  ...

在 nacos 中添加配置文件

image.png

配置内容: https://github.com/apache/incubator-seata/blob/1.3.0/script/config-center/config.txt

微改了一下,后面有需要再进行改动

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
service.vgroupMapping.my_test_tx_group=default
# 端口 8091 通常用于 Seata 服务的默认通信端口
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
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=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.56.105:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
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
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.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

启动操作

直接点击 seata-server.bat 文件进行启动操作

image.png

查看服务实例,发现是已经在服务列表了

image.png

然后再启动一下客户端,启动前先过一下基础概念内容

image.png

启动客户端有报错信息

2024-04-27 23:03:46.363 ERROR 15912 --- [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager  : no available service 'default' found, please make sure registry config correct
部署 服务端 TC

有问题,这里改一下为 docker 的方式进行部署,看一下是否是 TC 端的问题(看很多是可以进行一个 config.txt 文件推送的)

这里使用 docker 的方式进行部署

docker pull seataio/seata-server:1.3.0

部署参考:

新建目录

mkdir -p /home/apps/seata/config

# 进入目录
cd /home/apps/seata/config

nacos 中新建一个 seata 的命名空间

新建 registry.conf

# 注册中心
registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "192.168.56.105:8848"
    group = "SEATA_GROUP"
    namespace = "6487cc37-2434-4574-81f9-83d2c2fb0dd5"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

# 配置中心
config {
  type = "nacos"
  nacos {
    serverAddr = "192.168.56.105:8848"
    namespace = "6487cc37-2434-4574-81f9-83d2c2fb0dd5"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}

启动命令

docker run \
-d \
--name seata-server \
--restart=always \
--privileged=true \
-p 8091:8091 \
-e SEATA_IP=192.168.56.105 \
-e SEATA_PORT=8091 \
-e SEATA_CONFIG_NAME=file:/root/seata-config/registry \
-v /home/apps/seata/config:/root/seata-config  \
seataio/seata-server:1.3.0
推送配置信息

下载地址: https://github.com/seata/seata/tree/1.3.0

目录: incubator-seata-1.3.0\script\config-center\config.txt

config.txt

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
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=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.56.105:3306/seata?useUnicode=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.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
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
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.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

image.png

推送操作,进入改目录

nacos-config.sh -h 192.168.56.105 -p 8848 -g SEATA_GROUP -t 6487cc37-2434-4574-81f9-83d2c2fb0dd5

这个操作会推送对应配置到相应的命令空间区域

根据后面反馈情况,需要再改一下这个配置

15 行后面再加三行配置

transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.vgroupMapping.account-service-group=default
service.vgroupMapping.order-service-group=default
service.vgroupMapping.product-service-group=default
service.default.grouplist=127.0.0.1:8091

添加配置后,再进行一次推送(重复命令即可,看了一下他 set 执行的是覆盖操作)

修改配置文件,客户端启动

改一下配置文件,主要是命名空间,然后还有 tx-service-group 的相关配置

三个模块的内容都参考下面的改一下;另外还有就是 seata 的 版本统一改为了 1.3.0

server:  
  port: 8081 # 端口  
  
spring:  
  application:  
    name: order-service  
  
  datasource:  
    url: jdbc:mysql://192.168.56.105:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8  
    driver-class-name: com.mysql.jdbc.Driver  
    username: root  
    password: 123456  
  
  cloud:  
    # Nacos 作为注册中心的配置项  
    nacos:  
      discovery:  
        server-addr: 192.168.56.105:8848  
        namespace: 6487cc37-2434-4574-81f9-83d2c2fb0dd5  
#        group: SEATA_GROUP  
  
  
seata:  
  enabled: true  
  tx-service-group: my_test_tx_group  
  service:  
    vgroup-mapping:  
      rapid_cloud_tx_group: default  
  registry:  
    type: nacos  
    nacos:  
      application: seata-server  
      server-addr: 192.168.56.105:8848  
      group: SEATA_GROUP  
      namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'  
      username: 'nacos'  
      password: 'nacos'  
  config:  
    type: nacos  
    nacos:  
      server-addr: 192.168.56.105:8848  
      group: SEATA_GROUP  
      namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'  
      username: 'nacos'  
      password: 'nacos'

这个配置再改一下

seata:
  application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
  tx-service-group: ${spring.application.name}-group
  service:
    vgroup-mapping:
      rapid_cloud_tx_group: default
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.56.105:8848
      group: SEATA_GROUP
      namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
      username: 'nacos'
      password: 'nacos'
  config:
    type: nacos
    nacos:
      server-addr: 192.168.56.105:8848
      group: SEATA_GROUP
      namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
      username: 'nacos'
      password: 'nacos'

后面看了下,再改一下

其中 product-service-group: default 是对应 tx-service-group 前面的 config.txt 中的分组映射关系

# Seata 配置项,对应 SeataProperties 类
seata:
  application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
  tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
  # 服务配置项,对应 ServiceProperties 类
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      product-service-group: default
      # account-service-group: default
      # order-service-group: default
    # 分组和 Seata 服务的映射
    grouplist:
      #设置Seata TC 的地址
      default: 192.168.56.105:8091

部署集群 TC Server

这里后面再看一下 to be contined....

业务场景

继续项目:https://github.com/YunaiV/SpringBoot-Labs 的 labx-17

文档: https://www.iocoder.cn/Spring-Cloud-Alibaba/Seata/?self

业务逻辑

image.png

项目模块

image.png

  • order 下单操作
  • product 商品服务(扣除库存)
  • account 账户服务(扣除余额)

data.sql 文件中: 每个库中的 undo_log 表,是 Seata AT 模式必须创建的表,主要用于分支事务的回滚

考虑到测试方便,数据库中插入了一条 id = 1 的 account 记录,和一条 id = 1 的 product 记录

简单测试

测试两种情况:

  1. 分布式事务正常提交
  2. 分布式事务异常回滚

分布式事务正常提交

如果要正常提交的话,因为账户表中只有一条数据,用户 1 有10 余额;产品表一条数据,产品 1 有10个库存

image.png

这个是一次正常请求记录

image.png

日志,记录正常操作:

image.png

分布式事务异常回滚

模仿余额不足的情况下

image.png

相关的日志

image.png

工程代码

看一下核心代码

@Override  
@GlobalTransactional  
public Integer createOrder(Long userId, Long productId, Integer price) {  
    Integer amount = 1; // 购买数量,暂时设置为 1。  
  
    logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());  
  
    // 扣减库存  
    productService.reduceStock(new ProductReduceStockDTO().setProductId(productId).setAmount(amount));  
  
    // 扣减余额  
    accountService.reduceBalance(new AccountReduceBalanceDTO().setUserId(userId).setPrice(price));  
  
    // 保存订单  
    OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);  
    orderDao.saveOrder(order);  
    logger.info("[createOrder] 保存订单: {}", order.getId());  
  
    // 返回订单编号  
    return order.getId();  
}

中间几个都是远程调用的操作

然后对方执行的是本地事务,比如你看一下扣除余额的操作

@Override  
@Transactional // 开启新事物  
public void reduceBalance(Long userId, Integer price) throws Exception {  
    logger.info("[reduceBalance] 当前 XID: {}", RootContext.getXID());  
  
    // 检查余额  
    checkBalance(userId, price);  
  
    logger.info("[reduceBalance] 开始扣减用户 {} 余额", userId);  
    // 扣除余额  
    int updateCount = accountDao.reduceBalance(price);  
    // 扣除成功  
    if (updateCount == 0) {  
        logger.warn("[reduceBalance] 扣除用户 {} 余额失败", userId);  
        throw new Exception("余额不足");  
    }  
    logger.info("[reduceBalance] 扣除用户 {} 余额成功", userId);  
}