一、事务
1. 什么是事务
事务是指数据库管理系统中 DBMS 提供一种数据操作模式,一般的数据库系统都支持事务管理。
事务,它不是代码级别的,所有事务的控制都是因为数据库本身支持,代码中编写的事务只是一个
控制。
最终回滚和提交一定是数据库层面来决定。
比如:MYSQL — Innodb 引擎可以支持事务。
一句话:如果你数据库不不支持事务。你在代码中使用事务编程是没有任何意义的。都是失效。
2. 事务目的
事务出现主要目的:保证业务的数据的一致性,持久性,隔离性,防止脏数据。
举例子:比如用户下单
下单 - > 扣减库存 - > 扣用户钱包
事务机制:它们执行的动作一定是个完整体,不论下单失败还是扣库存失败都应该回滚,不应该保
存。
反之,一定是所有的操作都成功才进行整体提交。
3. 事务的流程
一般的事务处理的流程如下:
- starter transcation -开启事务
- insert/update/delete —DBMS 提供了一系列的对数据库的操作集
- commit/rollback—事务回滚
正常执行逻辑,提交事务:
START TRANSACTION;
INSERT INTO travel_user (id,username) values (2,"xxxx");
COMMIT;
正常执行逻辑,事务回滚:
START TRANSACTION;
INSERT INTO travel_user (id,username) values (3,"xxxx");
ROLLBACK;
比如:用户下单扣库存
START TRANSACTION;
INSERT INTO travel_order (id,username,ordernumber,price) values (3,"xxxx","22222",125.5);
update travel_product set store = store - 1 where id = 1
ROLLBACK;
映射成 java 代码
@Transcational
public void makeOrder(Order order){
orderService.insertOrder(order);
productService.updateStore(id);
}
上面告诉我们一个什么道理?
- 事务是由数据库层面来决定的。
- 我们程序,比如:后面马上要接触的 @Transtional 注解还是编程式事务,其实都只不过是把我们的方法,包裹成一个事务体,然后把里面的对应 sql 语句提出出来,然后发送数据库。然后让数据库去决定提交还是回滚。但是程序代码的作用只是告诉数据库在哪里进行提交还是回滚。
上面的 makeOrder 怎么就编程了事务体呢?
- 执行方法肯定有一个线程 tomcat-nio-thread-100 线程,同时 MYSQL 也开启线程 mysql-tx-thread-1
- 先判断当前的方式是不是有注解 @Transcational 如果有事务执行 start transcation
- 然后开始执行方法,把对应方法的sql全部进行放入到事务队列中,开始进行执行SQL。
- 这个时候 SQL 执行全部都暂时放在 MYSQL 内存中。
4. 事务四大特性
数据库事务需要满足 ACID(原子性、一致性、隔离性、持久性)四个特性。
原子性(Atomicity)
指事务作为整体来执行,要么全部执行,要么全不执行;原子代表的是不课分割,原子性就是指事务应该
具备不可分割的特性,不论你包含多少个 upate / insert / delete(DDL)操作,整个过程都应该具备:
要么全部执行,要么同时全部失败。不允许部分成功。
提交(commit)
回滚(rollback)
一致性(Consistency)
指事务应确保数据从一个一致的状态转变为另一个一致的状态;事务的一致可以理解对数据完整性约束的遵循,这些约束可能包括主健
约束,外加约束或者是一些用户自定义的陎,事务执行的前后都是合法的数据状态,不会违背任何的数据完整性:
比如:你在提前数据之前和数据之后应该都是一致。比如存钱取钱,
持久性(Durability)
持久性是指一个事务一旦被提交了,对数据库中的数据的改变是永久性的。
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为本地事务。
几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。
但是在基于微服务的分布式应用环境下,越来越多的应用场景要求对多个服务的访问及其相对应的多
个数据库资源能纳入到同一个事务
当中,分布式事务应运而生。
关系型数据库虽然对本地事务提供了完美的 ACID 原生支持。
但在分布式的场景下,它却成为系统性能的桎梏。
如何让数据库在分布式场景下满足 ACID 的特性或找寻相应的替代方案,是分布式事务的重点工作。
隔离性(Isolation)
隔离性通常是指在事务进行的过程中,对中间数据进行隔离的一种特性。这个由数据库的隔离级别所决定。
以 Mysql为例,共有四种隔离级别。
5. MySQL VS Oracle 默认事务隔离级别
MYSQL : REPEATABLE READ(可重复读)
Oralce:READ COMMITTED (读已提交)
MySQL 中事务的隔离级别一共分为四种,分别如下:
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
6. 本地事务
在不开启任何分布式事务管理器的前提下,让每个数据节点各自管理自己的事务。
它们之间没有协调以及通信的能力,也并不互相知晓其他数据节点事务的成功与否。
本地事务在性能方面无任何损耗,但在强一致性以及最终一致性方面则力不从心。
支持项
- 完全支持非跨库事务,例如:仅分表,或分库但是路由的结果在单库中;
- 完全支持因逻辑异常导致的跨库事务。例如:同一事务中,跨两个库更新,更新完毕后,抛出空指针,则两个库的内容都能够回滚。
不支持项
- 不支持因网络、硬件异常导致的跨库事务。例如:同一事务中,跨两个库更新,更新完毕后、未提交之前,第一个库宕机,则只有第二个库数据提交,且无法回滚。
7. 基于 XA 协议的分布式事务
最早的分布式事务模型是由 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing (DTP) 模型,简称 XA 协议。
基于XA协议实现的分布式事务对业务侵入很小。
它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用基于XA协议的分布式事务。
XA协议能够严格保障事务 ACID 特性。
严格保障事务 ACID 特性是一把双刃剑。
事务执行在过程中需要将所需资源全部锁定,它更加适用于执行时间确定的短事务。
对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务系统并发性能衰退明显。
因此,在高并发的性能至上场景中,基于 XA 协议的分布式事务并不是最佳选择。
支持项
- 支持数据分片后的跨库事务;
- 两阶段提交保证操作的原子性和数据的强一致性;
- 服务宕机重启后,提交/回滚中的事务可自动恢复;
- 支持同时使用 XA 和非 XA 的连接池。
不支持项
- 服务宕机后,在其它机器上恢复提交/回滚中的数据。
通过 XA 语句控制的分布式事务
- 通过 XA START 可以手动开启 XA 事务,注意该事务完全由用户管理,ShardingSphere 只负责将语句转发至后端数据库;
- 服务宕机后,需要通过 XA RECOVER 获取未提交或回滚的事务,也可以在 COMMIT 时使用 ONE PHASE 跳过 PERPARE。
8. 柔性事务
如果将实现了 ACID 的事务要素的事务称为刚性事务的话,那么基于 BASE 理论的事务要素的事务则称为柔性事务。
BASE 是基本可用、柔性状态和最终一致性这三个要素的缩写。
- 基本可用(Basically Available)保证分布式事务参与方不一定同时在线;
- 柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉;
- 最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性。
在 ACID 事务中对隔离性的要求很高,在事务执行过程中,必须将所有的资源锁定。
柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。
通过放宽对强一致性要求,来换取系统吞吐量的提升。
基于 ACID 的强一致性事务和基于 BASE 的最终一致性事务都不是,只有在最适合的场景中才能发挥它们的最大长处。
可通过下表详细对比它们之间的区别,以帮助开发者进行技术选型。
本地事务 | 两(三)阶段事务 | 柔性事务 | |
业务改造 | 无 | 无 | 实现相关接口 |
一致性 | 不支持 | 支持 | 最终一致 |
隔离性 | 不支持 | 支持 | 业务方保证 |
并发性能 | 无影响 | 严重衰退 | 略微衰退 |
适合场景 | 业务方处理不一致 | 短事务 & 低并发 | 长事务 & 高并发 |
支持项
- 支持数据分片后的跨库事务;
- 支持 RC 隔离级别;
- 通过 undo 快照进行事务回滚;
- 支持服务宕机后的,自动恢复提交中的事务。
不支持项
- 不支持除 RC 之外的隔离级别。
待优化项
- Apache ShardingSphere 和 SEATA 重复 SQL 解析。
二、MySQL事务
三、JDBC 事务
四、Spring事务
1. 事务失效问题
请阅读本人生产问题专栏文章:@Transactional事务失效问题
五、分布式事务
1. 如何理解分布式事务
1.1 先看一个例子,思考解决方案
订单系统 ——— 商品系统,针对这两个系统,该如何考虑分布式事务问题?
a. 分布式架构特征
服务得部署是独立得,数据库可能是独立的。
b. 考虑问题
上述的问题,需要考虑分布式事务的问题,主要体现在创建订单时,可能会造成事务回滚。
但是商品系统的库存被修改了,这就不正确了。
c. 解决方案
分布式事务,是指在分布式系统架构中,两个独立得服务部署在不同机器上,或者跨域 JVM 进程,独立的数据源都可能引发分布式事务得问题。
1.2 分布式事务的理论和解决方案
a. 简介
分布式事务是来源微服务的架构中,一定存在着跨服务或者跨进程和跨数据库的调用关系。
且整个调用链路上存在着多读多写数据表的行为,那么分布式事务就要保证这些操作要么全部成功,要么全部失败。
b. 分布式事务的一致性问题
- 强一致性:任何一次读都能读到某个数据的最近一次写的数据(要求高)
- 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,大部分的场景中是不允许这样做的。
- 最终一致性:不保证在任意时刻数据的完整性(也就是状态一致),但是随着时间的推移,数据总是会达到一致性的状态。(消息的补偿机制用的比较多。)
c. 2PC(二阶段提交协议)
ⅰ. 两阶段提交(2PC)
2PC两阶段是指:提交事务的流程分为两个阶段
- 准备阶段(PreparePhase)
- 提交阶段(Commit Phase)
支持两阶段提交的数据有:oracle 和 MySQL
准备阶段
事务管理器给每个参者发送一个Prepare的消息。
每个数据库参与者在本地执行事务,并且写入到本地的 undo / redo 日志中,此时事务还没有提交,
- Undo 日志是记录修改前的数据,用于数据库回滚,
- Redo 是记录修改后的数据,用于提交事务后写入数据库文件
提交阶段
如果事务管理其收到参与者的执行失败或者超时消息。直接会给每个参与者发送回滚( Rollback ) 消息,
否则发送提交消息。
参与者根据事务管理器的指令执行提交或者回滚操作。并释放事务处理过程中使用的锁资源,
注意:必须是最后阶段释放锁资
ⅱ. 两阶段提交的角色
- 中央协调器
- 本地资源管理器
两阶段正常提交事务
阶段 1:准备阶段
准备阶段有如下三个步骤:
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
- 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
- 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交。
阶段 2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚 ( rollback ) 消息;否则,发送提交 ( commit ) 消息。
参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。
(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程
两阶段异常中断事务
情况 1,当所有参与者均反馈 yes,提交事务,如上图:
- 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
- 参与者执行 commit 请求,并释放整个事务期间占用的资源。
- 各参与者向协调者反馈 ack ( 应答 ) 完成的消息。
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交
情况 2,当任何阶段 1 一个参与者反馈 no,中断事务,如上图:
- 协调者向所有参与者发出回滚请求(即 rollback 请求)
- 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源
- 各参与者向协调者反馈 ack 完成的消息
- 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断
d. 3PC(三阶段提交协议)
ⅰ. 什么是三阶段提交协议
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),
是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制: 同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段: 保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC 把 2PC 的准备阶段再次一分为二,
这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
ⅱ. 执行流程
阶段一:CanCommit
- 3PC的CanCommit阶段其实和2PC的准备阶段很像。
- 协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
阶段二:PreCommit
Coordinator根据Cohort的反应情况来决定是否可以继续事务的PreCommit操作。
根据响应情况,有以下两种可能。
A. 假如 Coordinator 从所有的 Cohort 获得的反馈都是Yes响应,那么就会进行事务的预执行:
- 发送预提交请求:Coordinator 向 Cohort 发送 PreCommit 请求,并进入 Prepared 阶段。
- 事务预提交:Cohort接收到PreCommit请求后,会执行事务操作,并将 undo 和 redo 信息记录到事务日志中。
- 响应反馈:如果 Cohort 成功的执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
B. 假如有任何一个 Cohort 向 Coordinator 发送了 No 响应,或者等待超时之后,Coordinator 都没有接到 Cohort 的响应,那么就中断事务:
- 发送中断请求:Coordinator 向所有 Cohort 发送 abort 请求。
- 中断事务:Cohort收到来自Coordinator的abort请求之后(或超时之后,仍未收到Cohort的请求),执行事务的中断。
阶段三:DoCommit
该阶段进行真正的事务提交,也可以分为以下两种情况:
执行提交
- 发送提交请求Coordinator接收到Cohort发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有Cohort发送doCommit请求。
- 事务提交Cohort接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
- 响应反馈事务提交完之后,向Coordinator发送ACK响应。
- 完成事务Coordinator接收到所有Cohort的ACK响应之后,完成事务。
中断事务
Coordinator没有接收到Cohort发送的ACK响应
(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
ⅲ. 三阶段提交协议和两阶段提交协议的不同
对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制
(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。
在2PC的准备阶段和提交阶段之间,插入预提交阶段,
使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。
PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。
ⅳ. 三阶段提交协议的缺点
如果进入PreCommit后,Coordinator发出的是abort请求,假设只有一个Cohort收到并进行了abort作,
而其他对于系统状态未知的Cohort会根据3PC选择继续Commit,此时系统状态发生不一致性。
ⅴ. 相比二阶段提交
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,
此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据
不一致。
e. 本地消息表(异步确保)
它是一种:最终一致性的策略
ⅰ. 简介
本地消息表,最早是有 ebay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
如下图:
具体 kafka 的方式
- 消息生成方法需要额外创建一个消息表并记录消息发送的状态也就是说它们要存储在一个数据库中,然后消息会经过 MQ 发送到消息消费方,如果消息发送失败,会重试发送。消息和业务数据要在一个事务里提交。
- 也就是说它们要存储在一个数据库中,然后消息会经过 MQ 发送到消息消费方,如果消息发送失败,会重试发送。
- 消息消费方需要处理这个消息,并完成自己的业务逻辑,此时如果本地事务处理成功,表明已经处理成功了如果处理失败,那么就会重试执行,如果业务方法多次执行失败,则可以给生产者发送一个业务补偿消息,通知生产法进行回滚等操作。
- 生产者和消费者定时扫描本地消息表,把还没处理完成的消息或者失败的消息在发送一遍
ⅱ. 知识小结
这种场景遵循BASE理论:采用的是最终一些行,编写这种本地消息表的业务,即不会像 2PC 那样复杂的实现,也不会像 TCC 那样可能出
现不能确认和回滚的情况。
- 本地消息表的优点:是一种非常经典的实现,避免了分布式事务没实现了最终一致性。
- 本地消息表的缺点:是与具体业务场景绑定,耦合性很强,消息数据和雨雾数据都在同一个数据库,占用业务系统资源。业务系统在使用关系型数据库的情况下,消息服务性能会收到关系型数据库并发性能的局限。
f. TCC 补偿事务
TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond
Distributed Transactions:an
Apostate’s Opinion》的论文提出。
TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现:
- Try 操作作为一阶段,负责资源的检查和预留。
- Confirm 操作作为二阶段提交操作,执行真正的业务。
- Cancel 是预留资源的取消。
TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。
处理流程
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步
骤,库存服务和订单服务分别在不同的服务器节点上。
ⅰ. Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,
这个阶段主要完成:
- 完成所有业务检查( 一致性 ) 。
- 预留必须业务资源( 准隔离性 ) 。
- Try 尝试执行业务。
TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操
作(Try)而展开。
因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果
撤销。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建
订单,订单状态为待确认。
ⅱ. Confirm / Cancel 阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作
这里使用的资源一定是 Try 阶段预留的业务资源。
在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段
Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。
ⅲ. 方案总结
TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,
有以下优点:
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。