为了保障分布式环境下数据强一致性,须要引入分布式事务,而分布式事务因为网络环境的不确定性,天生就很难实现。具体能够见上一篇。
分布式下,我想要强一致性
为了保障分布式事务的正确性,目前互联网畛域有几种风行的解决方案,然而大部分都没有像 XA 事务一样造成规范的工业标准。然而这些计划在某些特定的行业或者业务场景下却失去了越来越多的开发者的认可。
防止分布式事务
此计划提倡尽量避免分布式事务,不仅仅是因为分布式事务的难度,更是因为实现分布式事务须要更多的高级人才。如果一个操作设计到事务操作,而这些事务操作能够利用单机事务来解决,举荐首选单机事务。
当然,是否能够防止分布式事务还要看具体业务,在微服务流行的当下,更多的还要看畛域的划分规范,如果两个微服务能够合并成一个微服务,肯定水平上在畛域划分规范承受范畴之内,能够思考利用合并的形式来防止分布式服务。
举一个很简略的栗子:一个用户根本信息服务和用户资产服务(比方:用户经验值),当用户批改材料的时候给用户加奉献值这个业务场景下,因为波及到用户材料批改和加奉献值两个不同服务的操作,这个时候就能够思考将两个服务合并为一个服务,用单机的数据库事务来代替分布式事务。
在能够防止分布式事务的状况下,首选防止分布式事务
二阶段提交
二阶段(2PC)提交计划是基于 X /OpenDTP 标准规范的,最大的毛病在于它在第一阶段须要锁定资源,会大大降低零碎的性能,大型的互联网利用并不举荐这种计划,那种对性能不敏感的企业级利用能够尝试应用。
在 asp.net 中,微软曾经提供了分布式事务的治理类型:TransactionScope,它依赖 DTC(Distributed Transaction Coordinator)服务实现事务一致性。当它包裹的代码中如果设计到多个不同物理地位的数据库的时候,它会主动降级为分布式事务,应用起来十分不便。
using (TransactionScope ts = new TransactionScope())
{数据库 A 操作 ();
数据库 B 操作 ();
数据库 C 操作 ();
ts.Complete();}
TCC
TCC 实质上是一种编程模型,它提倡的是弥补操作,所以个别状况下它会有重试机制,它约定参加事务的每个业务方都须要提供三个接口,具体情况请查看上一篇文章。因为 TCC 的接口重试个性,所以提供的提交和勾销接口必须实现幂等性。
2PC 次要是针对数据库操作,而 TCC 次要是针对业务层面来进行操作,这在性能上比 2PC 要高很多,例如一个提交订单的场景,商品服务须要扣除库存,而订单零碎须要创立订单,代码相似以下,请不要纠结命名和参数:
// 订单服务
public interface IOrderService
{
// 创立一个不可见的订单,返回订单号
Task<string> CreateOrder();
// 依据订单号提交订单,使订单可见
Task<int> SubmitOrder(string orderNo);
// 依据订单号勾销订单
Task<int> CancleOrder(string orderNo);
}
// 商品服务
public interface IProductService
{
// 依据商品 id,锁定库存, 返回锁定的 id
Task<int> LockProductStock(int productId);
// 依据锁定的库存 id,提交事务,扣除商品库存
Task<int> SubmitLockStock(int lockId);
// 依据锁定的库存 id,勾销事务,商品库存回滚
Task<int> CancleLockStock(int lockId);
}
其实 TCC 实现过程中,还有很多细节。比方:当提交事务阶段,有一个节点因为网络起因或者 down 机提交失败,该怎么办呢?这个时候咱们要在本地引入本地音讯机制,或者叫做业务流动管理器,把每个业务参加分布式事务的每个操作都记录下来,当某个过程的某个节点操作失败,无论是主动发动重试,还是手动重试都能够达到最终数据的一致性。
基于音讯的事务
基于音讯的分布式事务实现的是最终一致性,它是基于 BASE 实践的一个解决方案,最早由 eBay 提出并施行,它采纳了音讯队列来辅助实现事务管制流程,核心思想是将须要分布式解决的工作通过 MQ 分发给每个业务去异步执行,如果工作失败,则能够发动零碎主动重试或者人工重试的纠正流程。
还是以上边的创立订单和扣减库存为栗子:
- 首先调用订单服务的创立订单接口创立订单,如果创立胜利,则发送须要扣减库存的音讯(也能够看做创立订单胜利的音讯)到 MQ。
- 商品服务监听扣减库存音讯队列,如果收到扣减库存音讯,则执行扣减库存操作,如果操作胜利,则回复 MQ 删除该音讯。如果没有操作胜利,则筹备接管同样音讯的下次投递。
这个流程看似很完满,其实有很多破绽。
- 创立订单是第一步操作,能够看做是单纯的单机操作,这个并没有问题,然而接着发送 MQ 音讯这一步须要和创立订单保障事务性,因为会产生创立订单胜利,发送 mq 音讯失败的状况。如果不能用技术手段来保障这两步的事务,也能够采纳引入本地音讯的计划,在创立订单的时候,用订单数据库来保障订单创立胜利和创立订单音讯表的一致性。而后发送 mq 胜利之后,批改订单音讯表的状态为发送胜利,如果发送 mq 音讯失败,则启用另外一个线程或者过程进行重试。
- 商品服务扣减库存相似,扣减库存这个操作和回复 mq 音讯这两个操作也能够利用本地音讯表的形式来解决一致性问题。当收到扣减库存音讯的时候,扣减库存和增加音讯胜利解决记录能够利用数据库的事务来保障一致性,如果回复音讯队列 ack 失败,就算是有反复音讯,也能够依据本地的生产音讯表来过滤反复音讯
基于音讯的分布式解决方案还有一个劣势,如果一个事务的业务参与方十分多,音讯的发送可能会非常复杂,须要十分审慎的设计。比方以上订单的栗子,当初引入了优惠券服务,在订单创立胜利,须要同时扣减库存和优惠券,如果优惠券扣减失败,须要同时回滚库存和勾销订单,这也只是三个业务参与方,如果是四个,五个呢?当然这在业务中兴许并不常见。
基于音讯的分布式事务解决方案,因为引入了重试机制,也须要接口在实现的时候反对幂等性。但从开发的角度,这种计划要比 tcc 以及 2pc 都要有劣势,把每个零碎之间的耦合度降到了最低,而且每个业务方的实现技术能够非常灵活,无论是采纳 java 还是 c# 活着是 golang 都无所谓。
当然市面上基于音讯的分布式解决方案各式各样,但总体来说都属于最终一致性计划。如果引入音讯通道 MQ 的不稳定性,那还须要在各个业务方引入查问机制来确保音讯的 ack 机制。举个栗子:如果商品服务曾经失常扣减库存,因为 mq 问题,始终不能失常 ack。这个时候订单服务是否会被动查问商品服务是否曾经失常扣库存?这个时候整个架构可能就非当初这个样子了,这个要是扯起来又是一篇文章了
更多精彩文章
- 分布式大并发系列
- 架构设计系列
- 趣学算法和数据结构系列
- 设计模式系列