分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
就是指不是单个服务或者单个数据库架构下产生的事务,例如:
在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用「至多只能同时支持上面的两个属性」 。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证 核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态
。满足BASE理论的事务,我们称之为 “柔性事务
”。
事务有刚性事务和柔性事务之分。
刚性事务(如单数据库中的本地事务)完全遵循 ACID 规范,即数据库事务正确执行的四个基本要素:
柔性事务,主要就是只分布式事务了,柔性事务为了满足可用性、性能与降级服务的需要,降低一致性(Consistency)与隔离性(Isolation)的要求,遵守 BASE 理论:
分布式事务解决方案大致分为如下3种:
2PC也就是两阶段提交:
第一阶段:
第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。
第二阶段:
3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
第一阶段(CanCommit):
第二阶段(PreCommit):
根据参与者返回的请求,来决定是否进入PreCommit
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
1.发送预提交请求 发送一个prepare-commit请求,进入PreCommit状态
2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
1.发送中断请求 协调者向所有参与者发送abort请求。
2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段(DoCommit):
进行真正的事务提交
执行提交
1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
3.响应反馈 事务提交完之后,向协调者发送Ack响应。
4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
1.发送中断请求 协调者向所有参与者发送abort请求
2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
4.中断事务 协调者接收到参与者反馈ACK消息之后,执行事务的中断。
阻塞问题:2PC在第二阶段(提交阶段)可能会出现阻塞,因为协调者需要等待所有参与者的响应才能决定是否提交事务。这种阻塞可能导致整个系统的性能下降。3PC通过引入预提交阶段,可以在某些情况下减少阻塞问题,使得参与者可以在预提交阶段确认是否可以提交事务。
可靠性和一致性:2PC和3PC都是刚性事务解决方案,强调事务的强一致性和可靠性。在2PC中,所有参与者在接收到准备请求后都将执行事务,并在接收到提交请求时进行事务的提交。在3PC中,预提交阶段可以使得参与者在准备阶段时就能够拒绝事务,从而增加了可靠性和灵活性。
单点故障:2PC中存在单点故障问题,即协调者的故障可能导致整个事务无法完成。3PC通过引入预提交阶段来减少单点故障的影响,当协调者故障时,参与者可以根据预提交消息自行决策是否提交事务。
总的来说,3PC相对于2PC引入了预提交阶段,以减少阻塞问题和单点故障的影响,增加了系统的可靠性和灵活性。然而,3PC也带来了额外的复杂性和通信开销。在实际应用中,需要根据具体的场景和需求来选择2PC还是3PC作为分布式事务解决方案。
TCC分别是Try、Confirm、Cancle的简称。大致包含2个节点,一是Try,二是Confirm/Cancle。
TCC的优缺点:
优点:
灵活性:TCC允许开发人员在业务逻辑中显式地定义事务的尝试(Try)、确认(Confirm)和取消(Cancel)阶段。这种显式的编程模型使得开发人员可以更加精确地控制事务的行为,适应各种复杂的业务场景。
高并发性:TCC的尝试阶段通常是在本地执行,不涉及资源锁定或网络通信。这使得尝试阶段可以以高并发的方式执行,提高了系统的吞吐量和性能。
可扩展性:TCC对于分布式系统的扩展性较好。每个参与者负责自己的事务逻辑,可以独立地进行水平扩展,减少了对中心化事务协调器的依赖。
容错性:TCC可以通过取消阶段来处理各种异常情况,如参与者失败、网络故障或超时。在出现异常时,可以执行取消操作,回滚之前的尝试操作,确保数据的一致性。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
发送消息方:
需要有一个消息表,记录着消息状态相关信息。
业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。
在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。
消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
处理消息队列中的消息,完成自己的业务逻辑。
如果本地事务处理成功,则表明已经处理成功了。
如果本地事务处理失败,那么就会重试执行。
如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
开发中使用的消息中间件并不支持事务消息的功能,那么本地消息表是一种不错的最终一致性解决方案
2.MQ通知发送方消息发送成功;
3.在发送半消息成功后执行本地事务;
4.根据本地事务执行结果返回commit或者是rollback;
5.如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;
6.订阅方根据消息执行本地事务;
7.订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
8.如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
9.Consumer端的消费成功机制有MQ保证;
MQ事务消息:
需要MQ支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理;
具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;
DB本地消息表:
使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;
事务消息使用了异步投递,增大了消息重复投递的可能性;
参考:https://blog.csdn.net/weixin_40777510/article/details/129685726
github地址:https://github.com/seata/seata/releases
conf目录下有一个application.example.yml
,这是模板配置文件,还有一个application.yml
,这是真正的配置文件,修改此文件,增加nacos的config和registry配置:
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 29abe988-e6d6-4ca3-85e4-188e3b7e41dd
group: DEFAULT_GROUP
username: nacos
password: nacos
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
namespace: 29abe988-e6d6-4ca3-85e4-188e3b7e41dd
cluster: default
username: nacos
password: nacos
store:
# support: file 、 db 、 redis
mode: file
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
注意:
新建namespace命名空间,需与上述seata-server的application.yml中配置一致
在上述namespace下新建application.yml中seata.config.nacos.data-id提到的配置文件:seataServer.properties
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#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
# 此处的mygroup名字可以自定义,只修改这个值即可
service.vgroup_mapping.stock-service-group=default
service.vgroup_mapping.order-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
#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
#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.
# 默认为file,一定要改为db,我们自己的服务启动会连接不到seata
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
#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.
# 修改mysql的配置
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 指定seata的数据库,下面会提
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=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.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
注意:
在seata数据库中新建查询,执行如下sql,sql文件存放在:seata/script/server/db/mysql.sql中
-- -------------------------------- 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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT 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 = InnoDB
DEFAULT 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 = InnoDB
DEFAULT 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 = InnoDB
DEFAULT 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);
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
双击:seata-server.bat 启动服务
查看两件事:
1.seata-server是否注册进nacos中
2.访问http://127.0.0.1:7091/#/login 能否进入seata的webui,用户名与密码默认为seata
参考官网的版本信息:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
我用的是
父pom.xml
<properties>
<java.version>1.8java.version>
<spring-cloud-alibaba.version>2021.0.5.0spring-cloud-alibaba.version>
<springboot.version>2.6.13springboot.version>
<lombok.version>1.18.8lombok.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<sikpTests>truesikpTests>
<maven-source-plugin.version>3.0.1maven-source-plugin.version>
<maven-surefire-plugin.version>2.22.1maven-surefire-plugin.version>
<seata.version>1.6.1seata.version>
<mybatis-plus-boot-starter.version>3.3.0mybatis-plus-boot-starter.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-loadbalancerartifactId>
<version>3.1.1version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>${spring-cloud-alibaba.version}version>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>${seata.version}version>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.6.1version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
<version>3.0.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis-plus-boot-starter.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-slf4jartifactId>
<version>11.8version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>2.2.6.RELEASEversion>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${springboot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.28version>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
stock和order的pom.xml一样
<properties>
<java.version>1.8java.version>
<spring-cloud.version>2021.0.5spring-cloud.version>
properties>
<dependencies>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
order的配置文件
spring.application.name=order-service
server.port=9091
# Nacos 注册中心地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.loadbalancer.ribbon.enabled: false
# seata 服务分组,要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
spring.cloud.alibaba.seata.tx-service-group=order-service-group
spring.seata.order-service-group=default
logging.level.io.seata=info
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/seata_order?allowMultiQueries=true
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
stock的配置文件
spring.application.name=stock-service
server.port=9092
# Nacos 注册中心地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# seata 服务分组,要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应
spring.cloud.alibaba.seata.tx-service-group=stock-service-group
spring.seata.stock-service-group=default
logging.level.io.seata=info
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/seata_stock?allowMultiQueries=true
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
参考官网的业务代码:https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata
启动项目测试:
浏览器输入:http://localhost:9091/order/placeOrder/commit
,数据正常扣减
查看控制台日志,能看出是2PC模式
浏览器输入:http://localhost:9091/order/placeOrder/rollback
,测试回滚