一、基本概念
什么是数据库事务?
1、一个操作数据库数据的执行单元
2、到围从开始到结束的多个操作组成
3、事务内的多个操作要么都成功,要么都失败
什么是分布式事务?
1.分布式场景下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库
2.由多个分支事务构成的一个全局事务我们一般叫分布式事务
CAP定理
'C'代表一致性、'A'代表可用性、'P'代表分区容错性
一致性:
对于给定的客户端,要保证读操作返回的是最近写操作成功的结果。
对于多个并发请求,要保证每个请求同时读到的数据一定是一致的。
所以当发生写操作时,分布式系统中,要等到每个节点都写入成功才能确保此次写操作是成功的,否则应当回滚事务。
可用性:
每一个请求都能在有限时间内收到非错误的响应,但不保证返回的是最新版本的数据。
非错误的响应指的是这个请求不会超时或返回报错的结果。
所以顾名思义就是,保证服务是可用的状态。
分区容错性:
网络分区是指网络中的某些部分因为故障或其他原因无法通信。分区容忍性意味着即使网络分区发生,系统仍然能够继续运行,部分节点之间的通信可能中断,但系统整体仍然可用。
CAP理论指的是三个特性之间,智能同时满足其中两个
CP(一致性 + 分区容忍性):
特点:系统保证数据的一致性和分区容忍性,但可能牺牲可用性。
解释:当网络分区发生时,比如就会产生节点之间通信故障,无法及时同步数据,那么系统要优先保证数据的一致性,就可能会拒绝部分请求以避免数据不一致。
例子:传统的关系型数据库(如 MySQL)通常选择 CP,因为它们强调事务的一致性和持久性。
AP(可用性 + 分区容忍性):
特点:系统保证可用性和分区容忍性,但可能牺牲一致性。
解释:当网络分区发生时,比如就会产生节点之间通信故障,那么在AP下,系统会优先保证可用性,可能会返回旧数据或不一致的数据,但确保每个请求都能得到响应。
例子:NoSQL 数据库(如 Cassandra)通常选择 AP,因为它们强调高可用性和分区容忍性,允许在某些情况下返回旧数据。
CA(一致性 + 可用性):
特点:系统保证数据的一致性和可用性,但可能牺牲分区容忍性。
解释:系统在任何情况下都能提供一致的数据,并且每个请求都能得到响应,但当网络分区发生时,系统可能无法继续运行。
例子:单节点数据库(如单台 MySQL 服务器)通常选择 CA,因为它们在一个节点上运行,不存在网络分区的问题。
CAP实践中的选择
1、虽然理论上是在AP 和 CP中做选择,但并不意味着我们要放弃另一个
2、在实践中选择了AP不代表放弃了C,不一致只是出现在发生网络分区时,网络分区恢复后最终需要保证一致
3、在实践中选择了CP也不代表放弃了A,比如:写入操作只需过半确认,未确认的节点依然能提供读能力(注:你可能会疑问:存在未确认节点,但这些节点任然提供服务,这样不就会造成数据不一致吗。在CP系统重会采用一些机制保证一致性,比如读取操作也需要从超过一半的节点读取数据,然后返回最新的数据。)
BASE理论
BASE 是指基本可用(Basically Available)、软状态(Soft state)、最终一致性(Eventualconsistency)基于CAP定理演化而来
基本可用:分布式系统在出现故障时,允许损失部分可用性,即保证核心可用
软状态:允许系统存在中间状态[数据不一致],而该中间状态不会影响系统整体正确性
最终一致性:系统中的所有数据经过一定时间后,最终能够达到一致的状态
刚柔事务
刚性事务:满足ACID理论
柔性事务:满足BASE理论
柔性事务并不是完全放弃ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐
二、分布式事务解决方案
常见的分布式解决方案主要分为两大类:
1、较经典的是XA分布式事务协议,XA协议包含二阶段提交(2PC)和三阶段提交(3PC)两种实现
2、基于TCC、本地消息表、MO事务消息等相关的实现
2PC两阶段提交
将事务的处理过程分为两个阶段:准备阶段和提交阶段
事务的发起者称协调者,事务的执行者称参与者。
在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
阶段1-准备阶段
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
- 各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
- 如参与者执行成功,给协调者反馈yes,即可以提交;如执行失败,给协调者反馈no,即不可提交。
阶段2-提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
情况1,当所有参与者均反馈yes,提交事务:
情况2,当任何阶段1一个参与者反馈no,中断事务:
2PC的问题
2PC是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务
2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
- 性能问题
所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。 - 可靠性问题
如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。 - 数据一致性问题
在阶段2中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3PC三阶段提交
三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制(2PC 中只有协调者有超时机制)。
三阶段提交将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
阶段1:canCommit
协调者向参与者发送canCommit请求,参与者如果可以提交就返回yes响应(参与者不执行事务操作),否则返回no响应:
- 1、协调者向所有参与者发出包含事务内容的canCommit请求,询问是否可以提交事务,并等待所有参与者答复。
- 2、参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈no。
阶段2:preCommit
协调者根据阶段1 canCommit参与者的反应情况来决定是否可以基于事务的preCommit操作。根据响应情况,有以下两种可能。
情况1,阶段1所有参与者均反馈yes,参与者预执行事务:
情况2,阶段1任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
阶段3:do Commit
该阶段进行真正的事务提交,也可以分为以下两种情况:
情况1:阶段2所有参与者均反馈ack响应,执行真正的事务提交:
阶段2任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
注意:进入阶段3后,如果协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或rollback请求。此时,参与者都会在等待超时之后,继续执行事务提交(默认提交)。
阶段三 只允许成功不允许失败,如果服务器宕机或者停电,因为记录的阶段二的数据,重启服务后在提交事务,所以,到了阶段三,失败了也不进行回滚,只允许成功。
TCC
TCC是服务化的二阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现,基本类似两阶段提交
- Try操作作为一阶段,负责资源的检查和预留。
- Confirm操作作为二阶段提交操作,执行真正的业务。(理解为:try成功,就提交事务)
- Cancel是预留资源的取消。(理解为:try失败,就回滚事务)
本地消息表
本地消息表通过在本地数据库中维护一个消息表,可以记录事务的状态和相关信息,从而实现事务的最终一致性。
在上述执行过程中,只要任何一步执行失败,都要回滚事务,如果事务不可逆,则可能需要人工做一些补偿机制
MQ事务
正常情况——事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
1、发送方向 MQ服务端(MQ Server)发送half消息。
2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
3、发送方开始执行本地事务逻辑。
4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况——事务主动方消息恢复
在断网或者应用重启等异常情况下,图中第4步提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
- 5、MQ Server 对该消息发起消息回查。
- 6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 7、发送方根据检查得到的本地事务的最终状态再次提交二次确认
- 8、MQ Server基于commit / rollback 对消息进行投递或者删除
三、Seata的使用
Seata支持多种事务模式的实现,包含AT、TCC、SAGA、XA等模式。
本文章以AT模式的使用为例
1、基本概念
- 全局事务:由多个分支事务组成的一个事务,确保所有分支事务要么全部成功提交,要么全部回滚。
- 分支事务:全局事务中的一个子事务,对应一个具体的业务操作。
- TC(Transaction Coordinator):事务协调器,管理全局事务的生命周期,协调各分支事务的提交和回滚。
- TM(Transaction Manager):事务管理器,负责开启全局事务、提交或回滚全局事务。
- RM(Resource Manager):资源管理器,管理分支事务,与 TC 交互,报告分支事务的状态,执行分支事务的提交和回滚。
2、工作流程
-
开启全局事务:
- TM 向 TC 申请开启一个全局事务,并获得全局事务 ID(XID)。
- TM 将 XID 传递给业务服务,业务服务在执行业务操作时带上 XID。
-
执行业务操作:
- RM 在执行业务 SQL 时,拦截 SQL 并生成对应的 undo_log 记录。
- undo_log 记录了 SQL 的反向操作,用于在回滚时恢复数据。
- RM 将 undo_log 记录和业务数据一起提交到数据库。
-
提交或回滚全局事务:
- TM 向 TC 发起全局事务的提交或回滚请求。
- TC 与各个 RM 交互,协调分支事务的提交或回滚。
- 如果所有分支事务都成功提交,则全局事务提交成功;如果有任何一个分支事务回滚,则全局事务回滚。
3、使用步骤
首先在你的SpringBoot对应需要用Seata的应用中,创建undo_log数据表
-- for AT mode you must to init this sql for you business database. the seata server not need it.
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 = InnoDBAUTO_INCREMENT = 1DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
依赖的引入
<!--seata-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><version>2.2.5.RELEASE</version>
</dependency>
启动SeataServer(TC的位置)
添加Seata配置:在application.yml中添加如下配置
seata:enabled: trueapplication-id: ${spring.application.name}tx-service-group: my_seata_groupenable-auto-data-source-proxy: trueuse-jdk-proxy: falseexcludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExcludeclient:rm:async-commit-buffer-limit: 1000report-retry-count: 5table-meta-check-enable: falsereport-success-enable: falsesaga-branch-register-enable: falselock:retry-interval: 10retry-times: 30retry-policy-branch-rollback-on-conflict: truetm:degrade-check: falsedegrade-check-period: 2000degrade-check-allow-times: 10commit-retry-count: 5rollback-retry-count: 5undo:data-validation: truelog-serialization: jacksonlog-table: undo_logonly-care-update-columns: truelog:exceptionRate: 100service:vgroup-mapping:my_seata_group: defaultgrouplist:default: localhost:8091enable-degrade: falsedisable-global-transaction: falsetransport:shutdown:wait: 3thread-factory:boss-thread-prefix: NettyBossworker-thread-prefix: NettyServerNIOWorkerserver-executor-thread-prefix: NettyServerBizHandlershare-boss-worker: falseclient-selector-thread-prefix: NettyClientSelectorclient-selector-thread-size: 1client-worker-thread-prefix: NettyClientWorkerThreadworker-thread-size: defaultboss-thread-size: 1type: TCPserver: NIOheartbeat: trueserialization: seatacompressor: noneenable-client-batch-send-request: true
关于配置文件内容参数比较多,我们需要掌握核心部分:
seata_transaction: default:事务分组,前面的seata_transaction可以自定义,通过事务分组很方便找到集群节点信息。
tx-service-group: seata_transaction:指定应用的事务分组,和上面定义的分组前部分保持一致。
default: localhost:8091:服务地址,seata-server服务地址。
接下来在Application中添加@EnableAutoDataSourceProxy注解
在需要进行事务控制的方法上标明@GlobalTransactional注解
到此,方法内部如果发生异常就会触发全局事务回滚了