分布式事务解决(java,Spring Cloud版)方案(一)

本文围绕分布式事务展开,先介绍事务的定义、四大特性ACID,接着阐述分布式事务概念及应用场景,如电商支付。列举三种常见解决方案,重点讲解基于可靠消息最终一致性方案,包括第一阶段上游服务操作及异常处理、消息事务二阶段流程,最后提及不同控制力度的特点及适用场景。

分布式事务解决(java,Spring Cloud版)方案(一)

什么是事务

事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。

事务的四大特性ACID

说到事务,就不得不提一下事务著名的四大特性。

1.原子性atomicity
原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
2. 一致性 consistency
一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
3. 隔离性 isolation
事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
4. 持久性 durability
持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

什么是分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说:分布式事务就是为了保证不同数据库的数据一致性。

分布式事务应用场景

最经典的场景就是电商支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,再对买家进行积分奖励。这些操作必须在一个事务里执行,要么全部成功,要么全部失败。但是随着微服务化或者SOA服务化,分离出了订单服务、用户服务、库存积分和积分服务。对于订单服务,有专门的数据库存储订单信息,用户服务也有专门的数据库存储用户信息,库存服务也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。

分布式事务解决方案

目前使用较多的分布式事务解决方案有以下三种:
一、结合MQ消息中间件实现的可靠消息最终一致性
二、TCC补偿性事务解决方案
三、最大努力通知型方案

第一种方案:可靠消息最终一致性,需要业务系统结合MQ消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制MQ的消息状态
第二种方案:TCC补偿性,分为三个阶段try-confirm-cancel。每个阶段做不同的处理。
try阶段主要是对业务系统进行检测及资源预留
confirm阶段是做业务提交,通过try阶段执行成功后,再执行该阶段。默认如果try阶段执行成功,confirm就一定能成功。
cancel阶段是回对业务做回滚,在try阶段中,如果存在分支事务try失败,则需要调用cancel将已预留的资源进行释放。
第三种方案:最大努力通知型,这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。

基于事务消息的MQ方案是目前公认的较为理想的分布式事务解决方案,各大电商都在应用这一方案。种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。

本文解决方案是基于可靠消息的最终一致性

所谓消息服务是基于消息中间件的两阶段提交。本质上是将本地事务和发消息放在一个分布式事务里,要么保证本地操作成功,并且对外发送消息成功,要么都失败。开源的MQ产品(RocketMQ,RabbitMQ等)支持这一特性。具体实现如下:

第一阶段上游服务执行本地业务并向MQ中间件发送消息


1.上游服务(A系统)项消息中间件发送预备消息
2.消息中间件保存预备消息并返回
3.A执行本地事务
4.A发送提交消息给MQ中间件
对于以上的四个步骤,都有可能出现异常,出现异常时,如何处理呢?
步骤1.整个事务都失败,不会执行A的本地操作

 /**发送前暂存消息*/
        dbCoordinator.setMsgPrepare(bizName);
        Object returnObj = null;
        /** 执行业务函数 */
        try {
            returnObj = joinPoint.proceed();
        } catch (Exception e) {
            dbCoordinator.deleteMsgPrepare(bizName);
            log.error("业务执行失败,业务名称:" + bizName);
            throw e;
        }

步骤2.整个事务都失败,不会执行A的本地操作
步骤3.这个时候是要回滚预备消息,这时就需要在A服务实现一个消息中间件的回调接口,消息中间件会不断的执行这个回调接口,检查A服务的事务是否执行成功,如果失败了,就需要回滚预备消息,成功执行本地事务则不需要回滚预备消息。
步骤4.这个时候,A服务的本地事务是执行成功的,此时MQ中间件就不能回滚A服务事务,通过MQ的回调接口,MQ就能检查A事务执行成功,这时候,其实不需要A发送提交信息,MQ中间件能自己对消息进行提交,从而完成整个消息服务。

  } catch (Exception e) {
            log.error("第四阶段消息ID:{}发送异常,此时本地事务已经执行完成,向RabbitMQ发送消息必须成功", bizName, e);
            mqTransation4Bug2ReSend.reSendMsg4Bug(bizName);

        }

消息事务的二阶段

消息事务的整个执行流程如下:
在这里插入图片描述
基于MQ消息中间件的二阶段提交事务,往往用在高并发的场景下,将分布式事务拆分为(A服务的本地事务+向MQ消息中间件发送消息)+ B服务的本地事务,其中B服务的操作是由MQ消息来驱动的。只要发送消息成功,那么A服务的本地事务一定成功。这时候消息一定发出来了,这时候B服务会受到消息去执行本地事务,如果本地事务执行失败,消息会重新投递,知道B服务执行成功。这样就变相的执行了A服务和B服务的分布式事务。
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏。

项目代码连接

参考第二篇博客:https://blog.youkuaiyun.com/schcilin/article/details/90756074

总结

分布式事务,本质上是对多个不同数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,例如消息事务+最终一致性,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值