当前位置: 首页 > 后端技术 > Java

架构师必备的分布式事务解决方案

时间:2023-04-01 22:28:45 Java

为保证分布式环境下的数据强一致性,需要引入分布式事务,而分布式事务由于网络环境的不确定性,本来就难以实现。详见上一篇文章。在分布式环境中,我想要强一致性。为了保证分布式事务的正确性,目前互联网领域有几种比较流行的解决方案,但大多都没有像XA事务那样形成标准的工业规范。但是,这些解决方案在某些特定的行业或业务场景中已经被越来越多的开发者所认可。避免分布式事务该方案提倡尽量避免分布式事务,不仅因为分布式事务的难度大,还因为分布式事务的实现需要更多的高级人才。如果一个操作被设计成事务性操作,而这些事务性操作可以使用单机事务来解决,建议优先选择单机事务。当然,能不能避免分布式事务,要看具体的业务。在微服务盛行的当下,更多取决于领域划分标准。如果两个微服务可以合并为一个微服务,在一定程度上,领域划分在标准接受的范围内,可以考虑使用合并的方式来避免分布式服务。举个很简单的例子:一个用户的基础信息服务和用户资产服务(比如:用户体验值),当用户修改数据时,给用户一个贡献值。在这个业务场景中,由于涉及到用户数据修改和增加贡献值两个不同服务的操作,此时可以考虑将两个服务合并为一个服务,用单机数据库事务代替分布式事务。在可以避免分布式事务的情况下,最好避免分布式事务的两阶段提交。两阶段(2PC)提交方案基于X/OpenDTP标准规范。最大的缺点是需要在第一阶段锁定资源,会大大降低系统性能,不推荐大型互联网应用使用此方案,对性能不敏感的企业级应用可以尝试使用它。在asp.net中,微软提供了一种分布式事务管理类型:TransactionScope,它依赖于DTC(DistributedTransactionCoordinator)服务来完成事务的一致性。当它封装的代码中设计了多个不同物理位置的数据库时,会自动升级为分布式事务,使用起来非常方便。使用(TransactionScopets=newTransactionScope()){数据库操作();数据库B操作();数据库C操作();ts.完成();}TCCTCC本质上是一种编程模型,提倡补偿操作,所以一般情况下它都会有重试机制,它规定每个参与交易的业务方需要提供三个接口,详见上一篇文章。由于TCC的接口重试特性,提供的提交和取消接口必须做到幂等。2PC主要是做数据库操作,而TCC主要是做业务操作,性能比2PC高很多。比如提交订单的场景,商品服务需要扣除库存,订单系统需要创建订单。代码类似下面,请不要纠结命名和参数://OrderservicepublicinterfaceIOrderService{//创建一个隐形订单并返回订单号TaskCreateOrder();//提交根据订单号下单,使订单可见TaskSubmitOrder(stringorderNo);//根据订单号取消订单TaskCancelOrder(stringorderNo);}//商品服务公共接口IProductService{//根据商品id,锁定库存并返回锁定的idTaskLockProductStock(intproductId);//根据锁定的库存id,提交交易并扣除商品库存TaskSubmitLockStock(intlockId);//根据锁定的库存id,取消交易,商品库存回滚TaskCancelLockStock(intlockId);}其实TCC实现的过程中还有很多细节。例如:提交交易时,节点由于网络原因或宕机导致提交失败,怎么办?这时候就需要引入本地消息机制,或者说业务活动管理器,来记录参与分布式事务的各个业务的每一次操作。重试或手动重试可以达到最终的数据一致性。基于消息的事务。基于消息的分布式事务实现最终一致性。它是基于BASE理论的解决方案。它最先由eBay提出并实施。它使用消息队列来协助执行事务控制过程。核心思想是将需要分布式处理的任务通过MQ分发到各个业务异步执行。如果任务失败,系统可以自动重试或手动重试修正过程。还是拿上面的创建订单和扣除库存举个栗子:首先调用订单服务的创建订单接口创建订单,如果创建成功,发送需要扣除库存的消息(可以也算是订单创建成功的消息)给MQ。商品服务监听库存扣减消息队列。如果收到库存扣减消息,则执行库存扣减操作。如果操作成功,则回复MQ删除消息。如果没有任何操作成功,则准备接收下一次传递的同一消息。这个过程看似完美,其实漏洞百出。创建订单是第一步。可以看作是一个简单的单机操作。这样是没有问题的,但是下一步发送MQ消息需要和创建订单是事务性的,因为创建订单会成功,而发送MQ消息会失败。.如果无法通过技术手段保证两步交易,也可以采用引入本地消息的方案。创建订单时,使用订单数据库来保证订单创建成功和订单消息表的一致性。然后在发送mq成功后,修改order消息表的状态为发送成功,如果发送mq消息失败,则启用另一个线程或进程重试。与商品服务的扣库存类似,扣库存和回复mq消息的操作也可以使用本地消息表来解决一致性问题。当收到减库存消息时,可以通过数据库事务来保证减库存成功处理记录和增加消息的一致性。如果回复消息队列ack失败,即使有重复消息,也可以根据本地消费消息表进行处理。基于消息的分布式解决方案在过滤重复消息方面还有一个缺点。如果一个事务中有很多业务参与者,消息的发送可能会非常复杂,需要精心设计。比如上述订单的栗子,现在已经推出了优惠券服务。订单创建成功后,需要同时扣除库存和优惠券。如果抵扣券失败,需要回滚库存,同时取消订单。这才三商参与方,如果是四家,那五家呢?当然这在商业中可能并不常见。基于消息的分布式事务方案,由于引入了重试机制,在实现的时候也需要接口支持幂等性。但是从开发的角度来看,这个方案比tcc和2pc更有优势。它最大限度地降低了各个系统之间的耦合度,各个业务端的实现技术可以非常灵活,不管是java还是c#,golang都无所谓。当然,市场上有各种基于消息的分布式方案,但总的来说都属于最终一致性方案。如果引入消息通道MQ的不稳定性,还需要在各个业务端引入查询机制,保证消息的ack机制。举个栗子:如果货物和服务已经正常的从库存中扣除,由于mq的问题,还是无法正常的ack。这个时候订单服务会主动查询商品服务是否正常扣库存?这时候整个结构可能就不是现在这个样子了。这个要是拉上来就另当别论了。如果这篇文章对你有帮助,别忘了给我三连,点赞、转发、评论,我们下期见!获取方式:点赞、评论、关闭~学习更多JAVA知识技能,关注并私信博主(666)