在微服务架构中,随着服务的逐步拆分,数据库私有已经成为共识,这也导致所面临的分布式事务问题成为微服务落地过程中一个非常难以逾越的障碍,但是目前尚没有一个完整通用的解决方案。
其实不仅仅是在微服务架构中,随着用户访问量的逐渐上涨,数据库甚至是服务的分片、分区、水平拆分、垂直拆分已经逐渐成为较为常用的提升瓶颈的解决方案,因此越来越多的原子操作变成了跨库甚至是跨服务的事务操作。最终结果是在对高性能、高扩展性,高可用性的追求的道路上,我们开始逐渐放松对一致性的追求,但是在很多场景下,尤其是账务,电商等业务中,不可避免的存在着一致性问题,使得我们不得不去探寻一种机制,用以在分布式环境中保证事务的一致性。
引用自 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
见 https://www.infoq.cn/article/2018/08/rocketmq-4.3-release
见 https://www.txlcn.org/zh-cn/docs/preface.html
谈到分布式事务,首先要说的就是 2PC ( two phase commit )方案,如下图所示:
2PC 把事务的执行分为两个阶段,第一个阶段即 prepare 阶段,这个阶段实际上就是投票阶段,协调者向参与者确认是否可以共同提交,再得到全部参与者的所有回答后,协调者向所有的参与者发布共同提交或者共同回滚的指令,用以保证事务达到一致性。
2PC 是几乎所有分布式事务算法的基础,后续的分布式事务算法几乎都由此改进而来。
这里我们定义一个充值需求,后续我们在各个实现中看看如何为该需求实现分布式事务。
Order 和 Account 分别是独立的一个服务,充值完成后,要分别将订单 Order 设置为成功以及增加用户余额。
Seata ( Fescar ) is a distributed transaction solution with high performance and ease of use for microservices architecture.
阿里开源,其特点是用一个事务管理器,来管理每个服务的事务,本质上是 2PC (后文会解释) 的一种实现。
Seata 提供了全局的事务管理器。
代理 SQL 查询,实现事务管理,类似中间件。
用该方案实现需求的话,就是这样的:
Order 和 Account 都接入 Seata 来代理事务。
比起自己去实现 2PC,Seata 提供了简化方案,代码实例见 : Seata Samples
TCC(Try-Confirm-Concel) 模型是一种补偿性事务,主要分为 Try:检查、保留资源,Confirm:执行事务,Concel:释放资源三个阶段,如下图所示:
其中,活动管理器记录了全局事务的推进状态以及各子事务的执行状态,负责推进各个子事务共同进行提交或者回滚。同时负责在子事务处理超时后不停重试,重试不成功后转手工处理,用以保证事务的最终一致性。
每个子节点,要实现 TCC 接口,才能被管理。
优点:不依赖 local transaction,可以管理非关系数据库库的服务。
缺点:TCC 模式多增加了一个状态,导致在业务开发过程中,复杂度上升,而且协调器与子事务的通信过程增加,状态轮转处理也更为复杂。而且,很多业务是无法补偿的,例如银行卡充值。
tx-lcn LCN distributed transaction framework, compatible with dubbo, spring cloud and Motan framework, supports various relational databases https://www.txlcn.org
或者 Seata MT 模式
需要把 Oder.done 和 Account 的余额+ 操作都实现 tcc 接口。
可以看出,这样真的很麻烦,能用本地事务的还是尽量用本地事务。
以购物场景为例,张三购买物品,账户扣款 100 元的同时,需要保证在下游的会员服务中给该账户增加 100 积分。
由于数据库私有,所以导致在实际的操作过程中会出现很多问题,比如先发送消息,可能会因为扣款失败导致账户积分无故增加,如果先执行扣款,则有可能因服务宕机,导致积分不能增加,无论是先发消息还是先执行本地事务,都有可能导致出现数据不一致的结果。
事务消息的本质就是为了解决此类问题,解决本地事务执行与消息发送的原子性问题。
Apache RocketMQ™ is an open source distributed messaging and streaming data platform.
优点:对异步操作支持友好。
缺点:Producer 端要为 RMQ 实现事务查询接口,导致在业务开发过程中,复杂度上升。
// TODO
通过 MQ,来保障 Order 和 Acount 的两个操作要么一起成功,要么一起失败。
注意一个点,假设 Account 的余额+失败了,这里是无法回滚 Order 的操作的,Account 要保证自己能正确处理消息。
分布式事务=A 系统本地事务 + B 系统本地事务 + 消息通知;
准备:
A 系统维护一张消息表 log1,状态为未执行,
B 系统维护 2 张表,
未完成表 log2,
已完成表 log3,
消息中间件用两个 topic,
topic1 是 A 系统通知 B 要执行任务了,
topic2 是 B 系统通知 A 已经完成任务了,
假设出现网络中断和系统 Crash 等问题时,为了继续执行事务,需要进行重试。重试方式有:
优点:简单。
缺点:程序会出现执行到一半的状态,重试则要求每个操作需要实现幂等性。
注意:分布式系统实现幂等性的时候,记得使用分布式锁,分布式锁详细介绍见文末参考文章。
通过消息表,把断开的事务继续执行下去。
我们先对这些实现方案进行一个总结:
| 基础原理 | 实现 | 优势 | 必要前提 | | :--------- | ---------: | :----------------: | :----------------------: | | 2PC | Seata | 简单 | 关系型数据库 | | 2PC | TCC | 不依赖关系系数据库 | 实现 TCC 接口 | | 最终一致性 | 事务消息 | 高性能 | 实现事务检查接口 | | 最终一致性 | 本地消息表 | 去中心化 | 侵入业务,接口需要幂等性 |
各个方案有自己的优劣,实际使用过程中,我们还是需要根据情况来选择不同事务方案来灵活组合。
例如存在服务模块 A、B、C。A 模块是 mysql 作为数据源的服务,B 模块是基于 redis 作为数据源的服务,C 模块是基于 mongo 作为数据源的服务。若需要解决他们的事务一致性就需要针对不同的节点采用不同的方案,并且统一协调完成分布式事务的处理。
方案:将 A 模块采用 Seata 模式、B/C 采用 TCC 模式就能完美解决。
RocketMQ 4.3 正式发布,支持分布式事务
Seata
txlcn
分布式事务 CAP 理解论证 解决方案
再有人问你分布式锁,这篇文章扔给他
考拉 APP 系统开发小哥 Nick,专注系统开发和优化。