分布式事务--seata
官网:http://seata.io/en-us/docs/overview/what-is-seata.html
分布式事务的产生
分库分表、SOA
可随着业务量的不断增长,单体架构渐渐扛不住巨大的流量,此时就需要对数据库、表做 分库分表处理,将应用 SOA 服务化拆分。也就产生了订单中心、用户中心、库存中心等,由此带来的问题就是业务间相互隔离,每个业务都维护着自己的数据库,数据的交换只能进行 RPC 调用。
当用户再次下单时,需同时对订单库 、库存库 、用户库 进行操作,可此时我们只能保证自己本地的数据一致性,无法保证调用其他服务的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。

Seata 优势
实现分布式事务的方案比较多,常见的比如基于 XA 协议的 2PC、3PC,基于业务层的 TCC,还有应用消息队列 + 消息表实现的最终一致性方案,还有今天要说的 Seata 中间件,下边看看各个方案的优缺点。
2PC
基于 XA 协议实现的分布式事务,XA 协议中分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、MYSQL 这些数据库都实现了 XA 接口,而事务管理器则作为一个全局的调度者。两阶段提交(2PC),对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。

可 2PC的缺点也是显而易见,它是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的 刚性事务。所以它比较适⽤于执⾏时间确定的短事务,整体性能比较差。
一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。

3PC
三段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的阻塞问题,上边提到两段提交,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞锁定资源。
2PC 中只有协调者有超时机制,3PC 在协调者和参与者中都引入了超时机制,协调者出现故障后,参与者就不会一直阻塞。而且在第一阶段和第二阶段中又插入了一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。
TCC
所谓的 TCC 编程模式,也是两阶段提交的一个变种,不同的是 TCC 为在业务层编写代码实现的两阶段提交。TCC 分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。
以下单扣库存为例,Try 阶段去占库存,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
消息事务(最终一致性)
消息事务其实就是基于消息中间件的两阶段提交,将本地事务和发消息放在同一个事务里,保证本地操作和发送消息同时成功。下单扣库存原理图:

-
订单系统向
MQ发送一条预备扣减库存消息,MQ保存预备消息并返回成功ACK -
接收到预备消息执行成功
ACK,订单系统执行本地下单操作,为防止消息发送成功而本地事务失败,订单系统会实现MQ的回调接口,其内不断的检查本地事务是否执行成功,如果失败则rollback回滚预备消息;成功则对消息进行最终commit提交。 -
库存系统消费扣减库存消息,执行本地事务,如果扣减失败,消息会重新投,一旦超出重试次数,则本地表持久化失败消息,并启动定时任务做补偿。
基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况。
Seata
Seata 也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 等事务模式,这里重点介绍 AT模式。既然 Seata 是两段提交,那我们看看它在每个阶段都做了点啥?下边我们还以下单扣库存、扣余额举例

先介绍 Seata 分布式事务的几种角色:
-
Transaction Coordinator(TC): 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交。 -
Transaction Manager™: 事务管理者,业务层中用来开启/提交/回滚一个整体事务(在调用服务的方法中用注解开启事务)。 -
Resource Manager(RM): 资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction),管理分支事务与TC进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。
Seata 实现分布式事务,设计了一个关键角色 UNDO_LOG (回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 UNDO_LOG 表中,以便业务异常能随时回滚。
工作过程
举个例子来说明它。
商务表:product
| 场地 | 类型 | 钥匙 |
|---|---|---|
| ID | 大整数(20) | PRI |
| 名称 | varchar(100) | |
| 自从 | varchar(100) |
AT模式下分支事务的sql:
update product set name = 'GTS' where name = 'TXC';
阶段1
过程:
- 解析sql:知道sql类型是更新操作,表名是product,where条件是name = 'TXC'等等。
- 查询更新前的数据(Named before image):为了定位要更新的数据,通过上面的where条件生成查询语句。
select id, name, since from product where name = 'TXC';
得到“之前的图像”:
| ID | 名称 | 自从 |
|---|---|---|
| 1 | TXC | 2014 |
- 执行更新sql:更新name等于'GTS'的记录。
- 查询更新后的数据(以图片命名):通过更新前图片数据的主键定位记录。
elect id, name, since from product where id = 1;
得到后图:
| ID | 名称 | 自从 |
|---|---|---|
| 1 | GTS | 2014 |
- 插入回滚日志:构建回滚日志前后的图像,以及SQL语句相关信息,然后插入到表中
UNDO_LOG。
{ "branchId": 641789253, "undoItems": [{ "afterImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "GTS" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "beforeImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "TXC" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "sqlType": "UPDATE" }], "xid": "xid:xxx" }
- 在本地提交之前,事务向TC提交申请,为表product中主键为1的记录获取全局锁。
- 提交本地事务:在同一个本地事务中提交 PRODUCT 表的更新和 UNDO_LOG 表的插入。
- 将步骤 7 的结果报告给 TC。
第 2 阶段 - 回滚案例
- 收到 TC 的回滚请求后,开始一个本地事务,执行如下操作。
- 通过 XID 和分支 ID 检索 UNDO LOG。
- 验证数据:将UNDO LOG中更新后的图像数据与当前数据进行比较,如果有差异,则表示数据已经被当前事务之外的操作改变了,应该在不同的策略中处理,我们将在其他中详细描述文档。
- 根据UNDO LOG中的前图和业务SQL的相关信息生成回滚SQL语句。
update product set name = 'TXC' where id = 1;
- 提交本地事务,将本地事务的执行结果(Branch事务的回滚结果)报告给TC。
第 2 阶段 - 提交案例
- 收到 TC 的提交请求后,将请求放入工作队列,立即向 TC 返回成功。
- 在队列中做异步工作的阶段,UNDO LOGs被批量删除。

浙公网安备 33010602011771号