维基百科:https://zh.wikipedia.org/wiki/X/Open_XA
分布式事务的实现有许多种,其中较经典是由Tuxedo提出的XA分布式事务协议,XA协议包含二阶段提交(2PC)和三阶段提交(3PC)两种实现。其他还有 TCC、MQ 等最终一致性解决方案。
https://www.ibm.com/docs/zh/db2/10.5?topic=managers-designing-xa-compliant-transaction
X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是了定义了规范和API接口,由厂商进行具体的实现
2PC、3PC,都是基于 XA 协议的
在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
核心思想就是对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。
阶段1:准备阶段
1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
2、各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
3、如参与者执行成功,给协调者反馈yes,即可以提交;如执行失败,给协调者反馈no,即不可提交。
阶段2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程
— 情况1,当所有参与者均反馈yes,提交事务:
1、协调者向所有参与者发出正式提交事务的请求(即commit请求)。
2、参与者执行commit请求,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack(应答)完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。
— 情况2,当任何阶段1一个参与者反馈no,中断事务:
1、协调者向所有参与者发出回滚请求(即rollback请求)。
2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务中断。
方案总结
2PC是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的 刚性事务
2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制(2PC 中只有协调者有超时机制)。
三阶段提交将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
阶段1:canCommit
协调者向参与者发送canCommit请求,参与者如果可以提交就返回yes响应(参与者不执行事务操作),否则返回no响应:
1、协调者向所有参与者发出包含事务内容的canCommit请求,询问是否可以提交事务,并等待所有参与者答复。
2、参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈no。
阶段2:preCommit
协调者根据阶段1 canCommit参与者的反应情况来决定是否可以基于事务的preCommit操作。根据响应情况,有以下两种可能。
情况1,阶段1所有参与者均反馈yes,参与者预执行事务:
1、协调者向所有参与者发出preCommit请求,进入准备阶段。
2、参与者收到preCommit请求后,执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
3、各参与者向协调者反馈ack响应或no响应,并等待最终指令。
情况2,阶段1任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
1、协调者向所有参与者发出abort请求。
2、无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
阶段2任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
1、如果协调者处于工作状态,向所有参与者发出abort请求。
2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务中断
注意:进入阶段3后,如果协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或rollback请求。此时,参与者都会在等待超时之后,继续执行事务提交。
阶段三 只允许成功不允许失败,如果服务器宕机或者停电,因为记录的阶段二的数据,重启服务后在提交事务,所以,到了阶段三,失败了也不进行回滚。
方案总结
方案简介
TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC是服务化的二阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现,基本类似两阶段提交
处理流程
根据Try阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。
Confirm和Cancel操作满足幂等性,如果Confirm或Cancel操作执行失败,将会不断重试直到执行完成。
方案总结
TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制相比于上面介绍的XA事务机制,有以下优点:
缺点:
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,开放以来,广受欢迎,不到一年已经成为最受欢迎的分布式事务解决方案。
官方中文网:https://seata.io/zh-cn
github项目地址:https://github.com/seata/seata
官方example:https://github.com/seata/seata-samples
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中AT模式最受欢迎
AT模式的相关资料请参考官方文档说明:https://seata.io/zh-cn/docs/overview/what-is-seata.html
下图是AT模式的执行流程:
见官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
在选择用Seata版本的时候,可以先参考下官方给出的版本匹配(Seata版本也可以按自己的要求选择):跳转
直接基于docker启动:
docker run --name seata-server -p 8091:8091 -d -e SEATA_IP=192.168.200.200 -e SEATA_PORT=8091 --restart=on-failure seataio/seata-server:1.3.0
官方示例, 可以 跳转
各种集成模式自行的去看对应的samples
集成可以按照如下步骤实现:
1: 引入依赖包spring-cloud-starter-alibaba-seata
2: 配置Seata
3: 创建代理数据源
4: @GlobalTransactional全局事务控制
集成SpringCloud Alibaba需求:
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: default_tx_group
enable-auto-data-source-proxy: true
use-jdk-proxy: false
excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
client:
rm:
async-commit-buffer-limit: 1000
report-retry-count: 5
table-meta-check-enable: false
report-success-enable: false
saga-branch-register-enable: false
lock:
retry-interval: 10
retry-times: 30
retry-policy-branch-rollback-on-conflict: true
tm:
degrade-check: false
degrade-check-period: 2000
degrade-check-allow-times: 10
commit-retry-count: 5
rollback-retry-count: 5
undo:
data-validation: true
log-serialization: jackson
log-table: undo_log
only-care-update-columns: true
log:
exceptionRate: 100
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 192.168.200.200:8091
enable-degrade: false
disable-global-transaction: false
transport:
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
server-executor-thread-prefix: NettyServerBizHandler
share-boss-worker: false
client-selector-thread-prefix: NettyClientSelector
client-selector-thread-size: 1
client-worker-thread-prefix: NettyClientWorkerThread
worker-thread-size: default
boss-thread-size: 1
type: TCP
server: NIO
heartbeat: true
serialization: seata
compressor: none
enable-client-batch-send-request: true
配置文件内容参数比较多,注意部分:
1.1.0: seata-all取消属性配置,改由注解@EnableAutoDataSourceProxy开启,并可选择jdk proxy或者cglib proxy
1.0.0: client.support.spring.datasource.autoproxy=true
0.9.0: support.spring.datasource.autoproxy=true
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.demo.driver.feign"})
@EnableAutoDataSourceProxy
public class OrderApplication {
}
@Override
@GlobalTransactional
public OrderInfo addOrder(String id) {
...
}
关于使用feign降级功能导致seata事务无法回滚的问题请看:移步
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
AT 模式(参考链接 TBD)基于 支持本地 ACID事务 的 关系型数据库:
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
TCC实现原理:
有一个 TCC 拦截器,它会封装 Confirm 和 Cancel 方法作为资源(用于后面 TC 来 commit 或 rollback 操作)
封装完,它会本地缓存到 RM (缓存的是方法的描述信息),可以简单认为是放到一个 Map 里面
当 TC 想调用的时候,就可以从 Map 里找到这个方法,用反射调用就可以了
另外,RM 不光是注册分支事务(分支事务是注册到 TC 里的 GlobalSession 中的)
它还会把刚才封装的资源里的重要属性(事务ID、归属的事务组等)以资源的形式注册到 TC 中的 RpcContext
这样,TC 就知道当前全局事务都有哪些分支事务了(这都是分支事务初始化阶段做的事情)
举个例子:RpcContext里面有资源 123,但是 GlobalSession 里只有分支事务 12
于是 TC 就知道分支事务 3 的资源已经注册进来了,但是分支事务 3 还没注册进来
这时若 TM 告诉 TC 提交或回滚,那 GlobalSession 就会通过 RpcContext 找到 1 和 2 的分支事务的位置(比如该调用哪个方法)
当 RM 收到提交或回滚后,就会通过自己的本地缓存找到对应方法,最后通过反射或其他机制去调用真正的 Confirm 或 Cancel
参看:https://github.com/seata/seata/tree/1.3.0/script/config-center 可以看到seata支持多种注册中心!
服务端注册中心(位于seata-server的registry.conf配置文件中的registry.type参数),为了实现seata-server集群高可用不会使用file类型,一般会采用第三方注册中心,例如zookeeper、redis、eureka、nacos等。
以下使用nacos
seata-server的registry.conf配置如下:
由于是基于docker启动的seata,故可以直接进入到容器内部修改配置文件/resources/registry.conf
registry {
# file ...nacos ...eureka...redis...zk...consul...etcd3...sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.200.200:8848"
group = "SEATA_GROUP"
namespace = "1ebba5f6-49da-40cc-950b-f75c8f7d07b3"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
此时再重新启动容器,访问:http://192.168.200.200:8848/nacos 看seata是否已注册到nacos中
项目中,我们需要使用注册中心,添加如下配置即可:
参看:https://github.com/seata/seata/blob/1.3.0/script/client/spring/application.yml
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.200.200:8848
group : "SEATA_GROUP"
namespace: 52abf927-d578-45f2-ade5-144a59c93a29
username: "nacos"
password: "nacos"
此时就可以注释掉配置中的default.grouplist=“192.168.200.200:8091”
上面配置也只是将注册中心换成了nacos,而且是单机版的,如果要想实现高可用,就得实现集群,集群就需要做一些动作来保证集群节点间的数据同步(会话共享)等操作
需要准备2个seata-server节点,并且seata-server的事务日志存储模式,共支持3种方式
这里选择redis存储会话信息实现共享。
1、启动第二个seata-server节点
docker run --name seata-server-n2 -p 8092:8092 -d -e SEATA_IP=192.168.200.200 -e SEATA_PORT=8092 --restart=on-failure seataio/seata-server:1.3.0
2、进入容器修改配置文件 registry.conf,添加注册中心的配置
registry {
# file ...nacos ...eureka...redis...zk...consul...etcd3...sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.200.200:8848"
group = "SEATA_GROUP"
namespace = "1ebba5f6-49da-40cc-950b-f75c8f7d07b3"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
3、修改seata-server 事务日志的存储模式,resources/file.conf
基于redis来存储集群每个节点的事务日志,通过docker运行一个redis
docker run --name redis6.2 --restart=on-failure -p 6379:6379 -d redis:6.2
然后修改seata-server的file.conf,修改如下:
## transaction log store, only used in seata-server
store {
## store mode: file...db...redis
mode = "redis"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "192.168.200.200"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
queryLimit = 100
}
}
如果基于DB来存储seata-server的事务日志数据,则需要创建数据库seata,表信息如下:
https://github.com/seata/seata/blob/1.3.0/script/server/db/mysql.sql