当前位置: 首页 > news >正文

Seata分布式事务实践

理论篇
什么是事务
关于事务我们一定会想到下面这四大特性:

原子性:所有操作要么全都完成,要么全都失败。
一致性: 保证数据库中的完整性约束和声明性约束。
隔离性:对统一资源的操作不会同时发生的。
持久性:对事务完成的操作最终会持久化到数据库中。

理解:

一致性:可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据。比如你账上有400,我账号上有100,你给我打2000,此时你账上的钱,因该是200,我账上的钱应该是300,不会存在我账号上钱加了,你账上钱没扣的中间状态。

隔离性:指的是多个事务并发执行的时候不会互相干扰。既一个事务内部的数据对于其他事务来说是隔离的。

分布式事务简介
而分布式事务,不仅包含上述四个特性,这个事务可能是跨服务也可能是跨数据源的一种事务。

什么是Seata

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

Seata这个开源工具实现分布式事务则是基于以下三个角色:

Seata的三大角色

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

 可以查看相关文档:

Seata 是什么? | Apache Seata

部署安装前言

部署Seata并注册到Nacos上。

首先我们来部署以下Seata,首先我们得去官网下载一下资源,以笔者为例,笔者当前使用的是2.0.0这个版本:

一、下载seata安装包

先在seata官网下载seata安装包

压缩包解压后目录如下:

config目录下,根据application.example.yml文件里面配置,修改配置文件application.yml

script目录:

二、指定nacos为配置中心、注册中心

打开config/application.yml文件,我们主要关注以下几个模块的配置。

1、server端配置中心指定,seata.config.type

2、server端注册中心指定,seata.registry.type

3、server端存储模式指定,seata.store.mode

修改config/application.yml文件,指定nacos为配置中心、注册中心,并将seata.store.mode屏蔽,后续中配置中心中进行配置。

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seata
seata:config:# support: nacos 、 consul 、 apollo 、 zk  、 etcd3type: nacos #使用nacos作为配置中心nacos:server-addr: 127.0.0.1:8088namespace: 01025263-d24e-4558-a2a1-2448142e9d57 #nacos命名空间id,""为nacos保留public空间空间group: DEFAULT_GROUP #指定配置至nacos注册中心的分组名#username:#password:#context-path:##if use MSE Nacos with auth, mutex with username/password attribute#access-key:#secret-key:data-id: seataServer.propertiesregistry:# support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofatype: nacos #使用nacos作为注册中心nacos:application: seata-server #指定注册至nacos注册中心的服务名server-addr: 127.0.0.1:8088group: DEFAULT_GROUPnamespace: 01025263-d24e-4558-a2a1-2448142e9d57cluster: default #指定注册至nacos注册中心的集群名,与配置中心文件中的service.vgroupMapping.[事务分组配置项]=TC集群的名称保持一致#username:#password:#context-path:##if use MSE Nacos with auth, mutex with username/password attribute#access-key:#secret-key:#store:# support: file 、 db 、 redis 、 raft#mode: db#  server:#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**

其余配置可参考config/application.example.yml文件。

三、上传配置到nacos配置中心

请确保后台已经启动 Nacos 服务。

首先你需要在nacos新建seata配置,此处dataId为seataServer.propertie

​​

配置内容参考:

# socket通信方式, 公共部分
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=falsetransport.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# 首先应用程序(客户端)中配置了事务分组,若应用程序是SpringBoot则通过配置seata.tx-service-group=[事务分组配置项]
# 事务群组,service.vgroupMapping.[事务分组配置项]=TC集群的名称
#事务分组需要和服务端配置文件中一致
#service.vgroupMapping.default_tx_group=default
service.vgroupMapping.vmi-service-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# undo配置
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#事务会话信息存储方式
store.mode=db
#事务锁信息存储方式
store.lock.mode=db
#事务回话信息存储方式
store.session.mode=db
#db或redis存储密码解密公钥
#store.publicKey=#存储方式为db
store.db.datasource=druid
store.db.dbType=mysql
#store.db.driverClassName=com.mysql.jdbc.Driver
#store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT%2B8
store.db.user=root
store.db.password=root12345
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# 事务规则配置
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=falseserver.raft.cluster=127.0.0.1:7091,127.0.0.1:7092,127.0.0.1:7093
server.raft.snapshotInterval=600
server.raft.applyBatch=32
server.raft.maxAppendBufferSize=262144
server.raft.maxReplicatorInflightMsgs=256
server.raft.disruptorBufferSize=16384
server.raft.electionTimeoutMs=2000
server.raft.reporterEnabled=false
server.raft.reporterInitialDelay=60
server.raft.serialization=jackson
server.raft.compressor=none
server.raft.sync=true#Metrics配置
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

以上主要注意以下几个配置:

1.配置事务分组,注意,这个在springboot应用引入seata时配置需要。

#service.vgroupMapping.default_tx_group=default

service.vgroupMapping.vmi-service-group=default

2.存储方式为db时的配置

注意我的mysql为8.0版本,所以使用com.mysql.cj.jdbc.Driver

#存储方式为db
store.db.datasource=druid
store.db.dbType=mysql
#store.db.driverClassName=com.mysql.jdbc.Driver
#store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
#我的mysql版本为8.0
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT%2B8
store.db.user=root
store.db.password=root12345

四、创建表

在上面的nacos配置文件中,已经配置了存储模型为db以及数据库连接配置,还需建表。

​建表语句在文件script/server/db/mysql.sql

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
create database if not exists seata DEFAULT CHARACTER SET utf8mb4;
use seata;
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_status_gmt_modified` (`status` , `gmt_modified`),KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- 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 = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key`        VARCHAR(128) NOT NULL,`xid`            VARCHAR(128),`transaction_id` BIGINT,`branch_id`      BIGINT       NOT NULL,`resource_id`    VARCHAR(256),`table_name`     VARCHAR(32),`pk`             VARCHAR(36),`status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',`gmt_create`     DATETIME,`gmt_modified`   DATETIME,PRIMARY KEY (`row_key`),KEY `idx_status` (`status`),KEY `idx_branch_id` (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;CREATE TABLE IF NOT EXISTS `distributed_lock`
(`lock_key`       CHAR(20) NOT NULL,`lock_value`     VARCHAR(20) NOT NULL,`expire`         BIGINT,primary key (`lock_key`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

五、启动seata server

支持的启动命令参数:

命令启动
liunx:

cd 到bin目录下

./seata-server.sh -h 127.0.0.1 -p 8091

windows:

打开cmd,cd到bin目录下运行seata-server.bat

或者到bin目录下直接双击seata-server.bat

可以看到启动成功

也可以输入http://localhost:7091,输入用户名seata,密码seata

另外在nacos也可以看到seata-server注册成功 

如果seata-server需要集群部署,多启几个实例,就行。

六、在服务中集成Seata

<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
<io.seata.version>2.0.0</io.seata.version>
 <!--引入seata--><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>${io.seata.version}</version><exclusions><exclusion><groupId>io.netty</groupId><artifactId>netty-all</artifactId></exclusion></exclusions></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.73.Final</version></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><version>${spring-cloud-alibaba.version}</version><exclusions><exclusion><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></exclusion></exclusions></dependency>

然后在application.yml中添加下面这样一段配置,具体含义参加注释

server:port: 9990
spring:application:name: dc-userprofiles:active: localcloud:nacos:discovery:server-addr: 192.168.31.186:8088namespace: 01025263-d24e-4558-a2a1-2448142e9d57config:server-addr: 192.168.31.186:8088namespace: 01025263-d24e-4558-a2a1-2448142e9d57group: DEFAULT_GROUPprefix: ${spring.application.name}file-extension: yaml
#seata配置alibaba:seata:tx-service-group: vmi-service-group #事务分组需要和服务端配置文件中一致
seata:enabled: true  # 是否开启seata,默认true#enable-auto-data-source-proxy: true #数据源自动代理#tx-service-group: vmi-service-group #事务分组需要和服务端配置文件中一致,这个配置无效可删除data-source-proxy-mode: ATservice:#开启全局事务,默认为false开启#disable-global-transaction: false#可以不需要vgroup-mapping:vmi-service-group: default #事务分组需要和服务端配置文件中一致registry:type: nacosnacos:application : seata-servernamespace: 01025263-d24e-4558-a2a1-2448142e9d57serverAddr : 127.0.0.1:8088group : DEFAULT_GROUPcluster : default#username : nacos#password : nacosconfig:type: nacosnacos:server-addr: 127.0.0.1:8088namespace: 01025263-d24e-4558-a2a1-2448142e9d57group: DEFAULT_GROUP#username: nacos#password: nacosdata-id: seataServer.properties
特别注意,事务分组需要和服务端配置文件中一致,与seata服务seataServer.propertie配置文件中保持一致

注意以上文件这两点配置

spring:cloud:alibaba:seata:tx-service-group: vmi_tx_group #事务分组需要和服务端配置文件中一致seata:service:vgroup-mapping:vmi_tx_group: default #事务分组需要和服务端配置文件中一致

七,基于Seata实现分布式事务的四种方式

(1)XA模式
了解XA模式的工作原理和优缺点

如下图,XA模式的分布式事务执行流程大抵如下,简而言之,我们可以将其分为两个阶段,首先是:

RM第一阶段:所有的分支事务向TC注册自己的状态,完成后各自执行SQL但是不提交,并将状态报告给TC。

TC 第二阶段:

1.TC查看当前事务的所有分支事务是否都成功了,如果都成功则协通知所有RM提交事务,反之回滚事务。
2.收到TC全局事务提交后,RM将自己管理的分支事务也提交了。

XA模式的分布式事务优缺点:

  1. 优点: 强一致性,符合ACID原则。 且实现简单,没有代码侵入。
  2. 缺点:为了保证强一致性,所以必须保证所有SQL执行没有问题才能提交,所以一阶段这些数据会被锁住,导致其他需要执行这些SQL的事务被阻塞,性能较差。
实践:

首先修改每个需要使用XA模式的服务,在yml使用下面这样一段配置

seata:data-source-proxy-mode: XA

XA模式代码如下所示,可以看到笔者仅仅是在方法上加一个@GlobalTransactional注解就能保证服务1和服务2之间分布式事务的ACID。感兴趣的读者可以自行编写一个demo,可以看到一旦任意服务报错,控制台就会输出RollBack将事务操作回滚。

 @GlobalTransactionalpublic BaseResponse hello(VerifyRegisterDto dto) {IdentityDocument identityDocument=new IdentityDocument();identityDocument.setIdentityName(dto.getUserName()); //证件类型名identityDocument.setIdPictureDescOne("证件图片描述1"); // 证件图片描述1identityDocument.setIdPictureDescTwo(dto.getUserName()); //证件图片描述2// 证件信息保存identityDocumentMapper.insert(identityDocument);PSysUser sysUser = new PSysUser();sysUser.setUserName(dto.getUserName());sysUser.setPhone(dto.getPhone());sysUser.setRoleId(2);sysUser.setUid(UIDUtil.getUUID());sysUser.setUserId(UIDUtil.nextId());String salt = UIDUtil.getUUID();sysUser.setSalt(salt);String md5Password = DigestUtils.md5Hex(salt + dto.getPassword());sysUser.setPassword(md5Password);// 保存用户信息sysUserMapper.insertSelective(sysUser);return ResultUtil.success();}

这段自行编写

(2)AT模式

AT模式的工作流程图如下所示,总结以下它的工作流程:

RM第一阶段工作

  1. 注册分支事务。
  2. 解析SQL记录修改前后的SQL镜像并存储到undo-log
  3. 执行业务SQL并直接提交事务。
  4. 通知TC当前事务的状态。

RM第二阶段工作

  1. 如果TC确定所有事务都成功且发起通知告知当前RM,则RM会将undo-log删除。
  2. 如果TC通知失败则RM会根据undo-log将数据还原。

对着我们服务需要用到的所有数据库,刷入undo-log表。(应用所用到的所有数据库刷入此表)

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

完成后对着yml文件修改或添加下面这样一段配置

seata:data-source-proxy-mode: AT # 默认就是AT

笔者代码如下所示,可以看到进行数据库的证件信息保存操作,和其他数据库保存用户信息的操作,但是再保存篇用户信息时故意添加了一个报错。

 @GlobalTransactionalpublic BaseResponse hello(VerifyRegisterDto dto) {IdentityDocument identityDocument=new IdentityDocument();identityDocument.setIdentityName(dto.getUserName()); //证件类型名identityDocument.setIdPictureDescOne("证件图片描述1"); // 证件图片描述1identityDocument.setIdPictureDescTwo(dto.getUserName()); //证件图片描述2// 证件信息保存identityDocumentMapper.insert(identityDocument);PSysUser sysUser = new PSysUser();sysUser.setUserName(dto.getUserName());sysUser.setPhone(dto.getPhone());sysUser.setRoleId(2);sysUser.setUid(UIDUtil.getUUID());sysUser.setUserId(UIDUtil.nextId());String salt = UIDUtil.getUUID();sysUser.setSalt(salt);String md5Password = DigestUtils.md5Hex(salt + dto.getPassword());sysUser.setPassword(md5Password);// 保存用户信息sysUserMapper.insertSelective(sysUser);return ResultUtil.success();}

经过debug我们可以发现,在证件信息完成数据提交后

查询数据库,可以看到证件信息数据已保存在表中

其中

global_table记录了一个全局事务信息。

branch_table记录了一条分支事务信息。

lock_table记录了一条锁的信息。

应用所用到数据库中undo-log表也会记录一条信息,我们不妨点入查看详细内容

查看详细内容可以看到,这里面记录的就是插入前后的数据库的镜像

{"@class": "io.seata.rm.datasource.undo.BranchUndoLog","xid": "192.168.31.186:8091:3558411956758468259","branchId": 3558411956758468261,"sqlUndoLogs": ["java.util.ArrayList", [{"@class": "io.seata.rm.datasource.undo.SQLUndoLog","sqlType": "INSERT","tableName": "userbusiness.identity_document","beforeImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName": "identity_document","rows": ["java.util.ArrayList", []]},"afterImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords","tableName": "identity_document","rows": ["java.util.ArrayList", [{"@class": "io.seata.rm.datasource.sql.struct.Row","fields": ["java.util.ArrayList", [{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "id","keyType": "PRIMARY_KEY","type": 4,"value": 15}, {"@class": "io.seata.rm.datasource.sql.struct.Field","name": "identity_name","keyType": "NULL","type": 12,"value": "aaa333"}, {"@class": "io.seata.rm.datasource.sql.struct.Field","name": "Id_picture_desc_one","keyType": "NULL","type": 12,"value": "证件图片描述1"}, {"@class": "io.seata.rm.datasource.sql.struct.Field","name": "Id_picture_desc_two","keyType": "NULL","type": 12,"value": "aaa333"}, {"@class": "io.seata.rm.datasource.sql.struct.Field","name": "create_time","keyType": "NULL","type": 93,"value": null}, {"@class": "io.seata.rm.datasource.sql.struct.Field","name": "update_time","keyType": "NULL","type": 93,"value": null}]]}]]}}]]
}

一旦报错控制台就会输出一段Rollback的内容,并且事务也会根据undo-log回滚

一旦报错,查询数据库,可以看到证件信息数据会回滚。

AT模式优缺点以及和XA模式的区别

XA一阶段会锁定资源,AT模式则是直接提交事务不锁定资源。
XA模式回滚依赖数据库事务,AT模式则是根据我们上面自己创建的undo-log的内容进行还原。
XA模式是强一致性,AT模式是最终一致性。
AT模式优缺点:

一阶段提交不会锁定资源,性能较好。
利用全局锁实现读写隔离。
没有代码侵入,便于使用。
缺点:

框架记录undo-log会有一定开销,但是性能相比XA会好很多。
两阶段属于软阶段,属于最终一致性,中间可能会有数据不一致问题。

(3)TCC模式
TCC模式依旧延续之前的架构,只不过TCC各个阶段都需要人工实现,TCC实现分布式事务我们必须实现以下三个方法:

try:进行资源检测和预留,例如我们要将user,id=1的name从1改为2,那么我们就得将1这个值预留下来(存到一张资源预留表中),并提交1改为2这个操作,并提交事务。
confirm:完成资源的业务操作(将预留表中的内容删除),try成功就要求confirm方法逻辑一定要成功。
cancel:事务提失败,将预留资源释放(将预留表中的数据状态设置为取消,并拿着这个旧值去还原数据,即手动回滚补偿)。

TCC模式优缺点:

优点

  1. 一阶段即可完成数据提交,释放数据库资源,性能好。
  2. 无需像AT模式那样需要生成快照,使用全局锁,性能最强。
  3. 不依赖数据库事务,而是手动补偿依赖操作,可以用非事务型数据库。

缺点:

  1. 有代码侵入,需要手写资源消耗、补偿等逻辑。
  2. 软状态,属于最终一致性。
  3. 需要考虑confirmcancel的情况,做好幂等相关处理。

为了演示TCC模式,笔者就基于自己的服务演示一下TCC的使用,举个例子,笔者现在有个为system的服务,system服务会将id为1的user名字进行修改,完成后同步修改file服务的相关数据库。

​ 

 所以笔者就以system为例实现一下TCC相关的服务,我们首先梳理一下思路,首先我们要做的就是将id为1的user的name由1改为2。那么我的try方法就需要进行以下操作:

		1. 将user表id为1的用户的旧值冻结起来(这条数据状态设置为0,代表处于try状态),便于后续回滚补偿。2. 将user表id为1的用户name值更新。

完成后编写confirm方法,因为只有try成功了才会走到confirm方法,它的逻辑很简单,将记录user表冻结的值的数据删除即可。

而cancel方法就是对资源的补偿处理,框架走到cancel则说明事务执行失败了,我们需要将修改的数据进行补偿,例如我们的user表id为1的name由1改为2,那么我们就需要到资源冻结表找到这个条数据的冻结记录,拿着旧值还原user数据,完成后再将这张资源冻结表数据状态设置为3(已取消)

完成上述逻辑后,我们还需要考虑两个问题,第一个是空悬挂问题,如下图,我们的try方法执行太久导致超时,框架自动执行cancel回滚补偿事务,结果try方法再次执行,因为此时资源已经回滚所以也没有try的必要了。
对此我们的要在try方法加上这么一个逻辑,如果资源冻结表关于本次操作的数据状态为3,则直接返回失败。

还有一个就是空回滚问题,即try方法执行过程中直接报错或者长时间阻塞了还没执行到sql逻辑,cancel就已经完成回滚了,结果还是走到了cancel方法,cancel并没有需要回滚的数据。
对此,我们只需要将修改前的值手动存到资源冻结表中并设置状态为已取消,制造一条资源冻结数据直接返回即可。

成上述分析后,我们就可以进行编码操作了,我们首先需要编写一个@LocalTCC接口,定义try、confirm、cancel方法,可以看到我们用TwoPhaseBusinessAction注解告知框架三大行为用哪个方法,并用BusinessActionContextParameter设置全局参数,confirm和cancel都可以通过BusinessActionContext 获取到这些参数

@LocalTCC
public interface SystemTCCService {@TwoPhaseBusinessAction(name = "doTry", commitMethod = "confirm", rollbackMethod = "cancel")void doTry(@BusinessActionContextParameter(paramName = "id") String id,@BusinessActionContextParameter(paramName = "oldVal") String oldVal,@BusinessActionContextParameter(paramName = "val") String val);boolean confirm(BusinessActionContext ctx);boolean cancel(BusinessActionContext ctx);}

完成后,我们继承这个类实现一个service,代码如下可以看到笔者doTry做的就是拿着上下文的oldVal(即user表的旧值)冻结起来存到资源冻结表,并将状态设置为0(代表这条数据处于try状态)。通过xid查询资源冻结表看看是否有数据,若有则说明这是一个空悬挂操作,直接返回。

confirmcancel逻辑就比较简单了,读者可以自行阅读代码和注释,无非是删除补偿数据或回滚数据并作废资源冻结表数据而已。

@Service
public class SystemTCCServiceImpl implements SystemTCCService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate SystemFreezeTblMapper systemFreezeTblMapper;@Overridepublic void doTry(String id, String oldVal,String val) {// 0.获取事务idString xid = RootContext.getXID();// 业务悬挂判断: 判断freeze中是否有冻结记录,如果有,一定是cancel执行过,拒绝业务操作if (systemFreezeTblMapper.selectByPrimaryKey(xid) != null) {// cancel执行过,我要拒绝业务return;}User user = new User();user.setId(id);user.setLoginName(val);user.setName(val);user.setPassword(val);userMapper.updateByPrimaryKeySelective(user);//冻结住原有的值SystemFreezeTbl systemFreezeTbl = new SystemFreezeTbl();systemFreezeTbl.setXid(xid);systemFreezeTbl.setUserId(user.getId());systemFreezeTbl.setFreezeVal(user.getName());systemFreezeTbl.setFreezeOldVal(oldVal);//0 try 1 confim 2 cancelsystemFreezeTbl.setState(0);systemFreezeTblMapper.insert(systemFreezeTbl);}@Overridepublic boolean confirm(BusinessActionContext ctx) {//成功则删除冻结的表String xid = ctx.getXid();int count = systemFreezeTblMapper.deleteByPrimaryKey(xid);return count > 0;}@Overridepublic boolean cancel(BusinessActionContext ctx) {// 0.查询冻结记录String xid = ctx.getXid();// 根据id查询冻结表的记录SystemFreezeTbl freeze = systemFreezeTblMapper.selectByPrimaryKey(xid);// 处理空回滚if (freeze == null) {//空回滚freeze = new SystemFreezeTbl();String userId = ctx.getActionContext("id").toString();freeze.setUserId(userId);freeze.setFreezeVal(ctx.getActionContext("val").toString());freeze.setFreezeOldVal(ctx.getActionContext("oldVal").toString());freeze.setState(2);freeze.setXid(xid);systemFreezeTblMapper.insert(freeze);return true;}// 幂等判断if (freeze.getState() == 2) {// 已经处理过了cancel,无需重复return true;}//手动补偿user表,进行数据还原User user = new User();user.setId(ctx.getActionContext("id").toString());user.setName(freeze.getFreezeOldVal());userMapper.updateByPrimaryKeySelective(user);//将资源冻结表状态设置为2 代表已取消freeze.setFreezeVal("");freeze.setState(2);int count = systemFreezeTblMapper.updateByPrimaryKey(freeze);return count > 0;}
}

完成system核心服务层代码编写之后,我们也可以照猫画虎的完成file服务的编写,笔者这里就不多做演示了,直接贴出controller的代码。我们可以开着debug模式试着将让fileService报错,调试时就会发现systemTCCService最终会执行cancel完成数据补偿。

@GetMapping("/test")@GlobalTransactionalpublic String hello() {systemTCCService.doTry("1", "1", "2");fileService.hello();return "success";}
(4)SAGA模式

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。

总的来说它和TCC差不多,它也是分为两个阶段:

  1. 一阶段将修改提交。
  2. 二阶段则是根据一阶段进行反馈,若是成功则什么都不做,反之进行回滚补偿。

优缺点

优点:

	1. 基于事件驱动实现异步调用,性能较好,吞吐高。2. 一阶段直接提交事务,无锁,性能较好。3. 无需像TCC一样手动编写不同阶段的方法。

缺点:

 1. 软状态时间不确定,时效性较差。2. 无事务隔离,可能出现脏写的情况。

 总结优缺点

分布式事务使用seata处理呢?为什么不用mq解决分布式事务的问题呢
主要还是考虑兼容性问题,目前主流的消息中间件中rocketMQ支持事务。而且考虑将来可扩展,可能我们还会更换中间件以及兼容多数据库,所以使用seata实现分布式事务是最合适的。
而且消息队列主要作用也并不是用于分布式事务问题,它的主要作用是解耦、异步、削峰。而seata是目前比较主流的分布式事务解决方案。

相关文章:

Seata分布式事务实践

理论篇 什么是事务 关于事务我们一定会想到下面这四大特性: 原子性:所有操作要么全都完成&#xff0c;要么全都失败。 一致性: 保证数据库中的完整性约束和声明性约束。 隔离性:对统一资源的操作不会同时发生的。 持久性:对事务完成的操作最终会持久化到数据库中。 理解&…...

数字IC设计\FPGA 职位经典笔试面试整理--基础篇2

1. 卡诺图 逻辑函数表达式可以使用其最小项相加来表示&#xff0c;用所有的最小项可以转换为卡诺图进行逻辑项化简 卡诺图讲解资料1 卡诺图讲解资料2 卡诺图讲解资料3 最小项的定义 一个函数的某个乘积项包含了函数的全部变量&#xff0c;其中每个变量都以原变量或反变量的形…...

(务必收藏)推荐市面上8款AI自动写文献综述的网站

在当前的学术研究和论文写作中&#xff0c;AI技术的应用已经变得越来越普遍。特别是在文献综述这一环节&#xff0c;AI工具能够显著提高效率并减少人工劳动。以下是市面上8款推荐的AI自动写文献综述的网站&#xff1a; 一、千笔-AIPassPaper 是一款备受好评的AI论文写作平台&…...

【python】运算符

学习目标 了解 Python 中常见 算术&#xff08;数学&#xff09;运算符赋值运算符 算术&#xff08;数学&#xff09;运算符 a 是 10&#xff0c;b 是 20 运算符描述实例加两个对象相加 a b 输出结果 30-减得到负数或是一个数减去另一个数 a - b 输出结果 -10*乘两个数相…...

C++深入学习string类成员函数(1):默认与迭代

引言 在 C 编程中&#xff0c;std::string 类是处理字符串的核心工具之一。作为一个动态管理字符数组的类&#xff0c;它不仅提供了丰富的功能&#xff0c;还通过高效的内存管理和操作接口&#xff0c;极大地方便了字符串操作。通过深入探讨 std::string 的各类成员函数&#…...

DataGrip远程连接Hive

学会用datagrip远程操作hive 连接前提条件&#xff1a; 注意&#xff1a;mysql是否是开启状态 启动hadoop集群 start-all.sh 1、启动hiveserver2服务 nohup hiveserver2 >> /usr/local/soft/hive-3.1.3/hiveserver2.log 2>&1 & 2、beeline连接 beelin…...

go 读取excel

一、安装依赖 go get github.com/tealeg/xlsx二、main.go package mainimport "fmt" import "github.com/tealeg/xlsx"type Student struct {Name stringSex string }func (student Student) show() {fmt.Printf("Name:%s Sex:%s\r\n", stude…...

Linux进阶系列(四)——awk、sed、端口管理、crontab

目录 1. 写在前面2. awk —— 强大的文本处理工具2.1 awk 概述2.2 awk 脚本结构2.3 awk 的内置变量2.4 awk 的高级用法2.5 awk实践 3. sed —— 流式文本编辑器3.1 sed 的基本语法3.2 sed 常用命令3.3 sed 的高级用法 4. Linux 端口管理4.1 端口的概念4.2 查看端口状态4.3 开放…...

利用Metasploit进行信息收集与扫描

Metasploit之信息收集和扫描 在本文中&#xff0c;我们将学习以下内容 使用Metasploit被动收集信息 使用Metasploit主动收集信息 使用Nmap进行端口扫描 使用db_nmap方式进行端口扫描 使用ARP进行主机发现 UDP服务探测 SMB扫描和枚举 SSH版本扫描 FTP扫描 SMTP枚举 …...

基于Pytorch框架的深度学习MODNet网络精细人像分割系统源码

第一步&#xff1a;准备数据 人像精细分割数据&#xff0c;可分割出头发丝&#xff0c;为PPM-100开源数据 第二步&#xff1a;搭建模型 MODNet网络结构如图所示&#xff0c;主要包含3个部分&#xff1a;semantic estimation&#xff08;S分支&#xff09;、detail prediction…...

Go语言中的并发编程

Go语言中的并发编程Go语言中的并发编程主要依赖于两个核心概念&#xff1a;goroutine 和 channel。1. Goroutinegoroutine 的特点结束 goroutine2. Channel创建 Channel发送和接收数据Channel 的类型使用 select 语句简单的多个 goroutine使用 WaitGroup 等待所有 goroutine 完…...

python学习笔记(3)——控制语句

控制语句 我们在前面学习的过程中&#xff0c;都是很短的示例代码&#xff0c;没有进行复杂的操作。现在&#xff0c;我们将开始学习流程控制语句。 前面学习的变量、数据类型&#xff08;整数、浮点数、布尔&#xff09;、序列&#xff08;字符串、列表、元组、字 典、集合&am…...

关系数据库设计之Armstrong公理详解

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” 一、Armstrong公理简介 Armstrong公理是一组在关系数据库理论中用于推导属性依赖的基本规则。这些公理是以著名计算机科学家威廉阿姆斯特朗&…...

【Geoserver使用】SRS处理选项

文章目录 前言一、Geoserver的三种SRS处理二、对Bounding Boxes计算的影响总结 前言 今天来看看Geoserver中发布图层时的坐标参考处理这一项。根据Geoserver官方文档&#xff0c;坐标参考系统 (CRS) 定义了地理参考空间数据与地球表面实际位置的关系。CRS 是更通用的模型&…...

python里面的单引号和双引号的区别

在Python中&#xff0c;单引号&#xff08;‘’&#xff09;和双引号&#xff08;“”&#xff09;在大多数情况下是等价的&#xff0c;没有本质区别。它们都用于创建字符串。以下是一些关键点&#xff1a; 功能相同&#xff1a; 两者都可以用来定义字符串&#xff0c;例如&…...

为什么不要在循环,条件或嵌套函数中调用hooks

为什么不要在循环&#xff0c;条件或嵌套函数中调用hooks 前言useState Hook 的工作原理具体实现1、初始化2、第一次渲染3、后续渲染4、事件处理简单代码实现 为什么顺序很重要Bad Component 第一次渲染Bad Component 第二次渲染 总结 前言 自从 React 推出 hooks 的 API 后&a…...

将成功请求的数据 放入apipost接口测试工具,发送给后端后,部分符号丢失

将成功请求的数据 放入apipost接口测试工具&#xff0c;发送给后端后&#xff0c;部分符号丢失 apipost、接口测试、符号、丢失、错乱、变成空格背景 做CA对接&#xff0c;保存CA系统的校验数据&#xff0c;需要模仿前端请求调起接口&#xff0c;以便测试功能完整性。 问题描…...

N诺计算机考研-错题

B A.LLC,逻辑链路控制子层。一个主机中可能有多个进程在运行,它们可能同时与其他的一些进程(在同一主机或多个主机中)进行通信。因此在一个主机的 LLC子层的一个服务访问点,以便向多个进程提供服务。B.MAC地址,称为物理地址、硬件地址,也称为局域网地址,用来定义网络设…...

vue3 数字滚动组件封装

相关参考文献 干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React) Vue3 插件方式 安装插件: countup.js 封装组件: components/count-up/index.js <template><div class="countup-wrap"><slot name="prefix"></slot&g…...

如何确保消息只被消费一次:Java实现详解

引言 在分布式系统中&#xff0c;消息传递是系统组件间通信的重要方式&#xff0c;而确保消息在传递过程中只被消费一次是一个关键问题。如果一个消息被多次消费&#xff0c;可能会导致业务逻辑重复执行&#xff0c;进而产生数据不一致、错误操作等问题。特别是在金融、电商等…...

Web3技术在元宇宙中的应用:从区块链到智能合约

随着元宇宙的兴起&#xff0c;Web3技术正逐渐成为其基础&#xff0c;推动着数字空间的重塑。元宇宙不仅是一个虚拟世界&#xff0c;它还代表着一个由去中心化技术驱动的新生态系统。在这个系统中&#xff0c;区块链和智能合约发挥着至关重要的作用&#xff0c;为用户提供安全、…...

关于QSizeGrip在ui界面存在布局的情况下的不显示问题

直接重写resizeEvent你会发现&#xff1a;grip并没有显示 void XXXXX::resizeEvent(QResizeEvent *event) {QWidget::resizeEvent(event);this->m_sizeGrip->move(this->width() - this->m_sizeGrip->width() - 3,this->height() - this->m_sizeGrip->…...

开始场景的制作+气泡特效的添加

3D场景或2D场景的切换 1.新建项目时选择3D项目或2D项目 2.如下图操作&#xff1a; 开始前的固有流程 按照如下步骤进行操作&#xff0c;于步骤3中更改Company Name等属性&#xff1a; 本案例分辨率可以如下设置&#xff0c;有能力者可根据需要自行调整&#xff1a; 场景制作…...

位运算--(二进制中1的个数)

位运算是计算机科学中一种高效的操作方式&#xff0c;常用于处理二进制数据。在Java中&#xff0c;位运算通常通过位移操作符和位与操作符实现。 当然位运算还有一些其他的奇淫巧计&#xff0c;今天介绍两个常用的位运算方法&#xff1a;返回整数x的二进制第k位的值和返回x的最…...

使用Docker和Macvlan驱动程序模拟跨主机跨网段通信

以下是使用Docker和Macvlan驱动程序模拟跨主机跨网段通信的架构图&#xff1a; #mermaid-svg-b7wuGoTr6eQYSNHJ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-b7wuGoTr6eQYSNHJ .error-icon{fill:#552222;}#mermai…...

RestCloud webservice 流程设计

RestCloud webservice 流程设计 操作步骤 离线数据集成&#xff08;首页&#xff09; → \rightarrow → 示例应用数据集成流程&#xff08;边栏&#xff09; → \rightarrow → 所有数据流程 → \rightarrow → webservice节点获取城市列表 → \rightarrow → 流程设计 …...

从入门到精通:QT 100个关键技术关键词

Qt基础概念 Qt Framework - 一个跨平台的C图形用户界面应用程序开发框架。它不仅提供了丰富的GUI组件&#xff0c;还包括网络、数据库访问、多媒体支持等功能。 Qt Creator - Qt官方提供的集成开发环境&#xff08;IDE&#xff09;&#xff0c;集成了代码编辑器、项目管理工具、…...

2024年双十一值得入手的好物有哪些?五大性价比拉满闭眼入好物盘点

随着2024年双十一购物狂欢节的临近&#xff0c;消费者们纷纷开始关注各类好物&#xff0c;期待在这一天能够以最优惠的价格入手心仪的商品&#xff0c;在这个特殊的时刻&#xff0c;我们为大家盘点了五大性价比拉满的闭眼入好物&#xff0c;这些产品不仅品质卓越&#xff0c;而…...

Hbase日常运维

1 Hbase日常运维 1.1 监控Hbase运行状况 1.1.1 操作系统 1.1.1.1 IO 群集网络IO&#xff0c;磁盘IO&#xff0c;HDFS IO IO越大说明文件读写操作越多。当IO突然增加时&#xff0c;有可能&#xff1a;1.compact队列较大&#xff0c;集群正在进行大量压缩操作。 2.正在执行…...

鸿蒙开发的基本技术栈及学习路线

随着智能终端设备的不断普及与技术的进步&#xff0c;华为推出的鸿蒙操作系统&#xff08;HarmonyOS&#xff09;迅速引起了全球的关注。作为一个面向多种设备的分布式操作系统&#xff0c;鸿蒙不仅支持手机、平板、智能穿戴设备等&#xff0c;还支持IoT&#xff08;物联网&…...