事务的概念
一个事务要么成功要么失败,不能有中间状态;一个事务一旦完成,后面的事务都要基于这个完成后的状态;未完成的实务不会相互影响,事务的中间状态不会被其他的事务感知到;事务一旦完成就是持久的。
分布式中事务
分布式事务简单来说就是一次大的操作由不同的小操作组成,这些小操作分布在不同的服务器,而且属于不同的应用。分布式事务是要保证这些事务要么全部成功要么全部失败,或者不严谨的说分布式事务的重点就是为了保证不同数据库的数据一致性。
比如一次支付的话买家和卖家一个扣钱,一个加钱,对于买家有个买家数据库,对于卖家有个卖家数据库,这个时候就要使用分布式的事务实现数据一致性。
但是,各节点之间由于相互独立,无法确切地知道其经节点中的事务执行情况,所以多节点之间很难保证ACID,尤其是原子性。
分布式事务解决方案
两段式提交
两段式提交是基于XA协议的,XA是一个分布式事务协议
XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA引入的事务管理器充当全局事务中的“协调者”角色。事务管理器控制着全局事务,管理事务生命周期,并协调资源
- 准备阶段 又称投票阶段。在这一阶段,协调者也就是事务管理器会询问所有参与者是否准备好提交,参与者如果已经准备好提交则回复Prepared,否则回复Non-Prepared。
- 提交阶段 又称执行阶段。协调者如果在上一阶段收到所有参与者回复的Prepared,则在此阶段向所有参与者发送commit指令,所有参与者立即执行commit操作;否则协调者向所有参与者发送rollback指令,参与者立即执行rollback操作。
两段式提交缺点就是性能不够好,但是能保证强一致性。
比如A是协调者,BCD是参与者。
首先是投票阶段:
(1)A先问BCD,准备好了没有;
(2)B说好了;
(3)c说好了;
(4)d迟迟不回复,此时对于这个事务,ABC会处于阻塞状态,无法继续进行;
如果都准备好了就到了提交阶段:
(1)协调者A发给BCD提交指令;
(2)bcd收到指令开始执行提交,有一个没执行的都会由A组织回滚。
不仅要锁住参与者的所有资源,而且要锁住协调者资源,开销大。
本地消息表
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。
消息生产方,需要额外建一个本地消息表,用来记录消息的发送状态。提交到消息表的操作和当前要操作的业务数据放在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过消息队列发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,会发生给生产方消息,通知生产方改变本地消息表里事务的状态。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍查缺补漏。
缺点:需要设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。
第三方消息中间件+最终一致性
在高并发的场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,B收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功。如果B一直执行不成功,那一致性会被破坏。
消息系统的设计,回避不了两个问题:消息的顺序问题,重复问题。
消息有序指的是可以按照消息的发送顺序来消费。例如:一笔订单产生了 3 条消息,分别是订单创建、订单付款、订单完成。消费时,要按照顺序依次消费才有意义。与此同时多笔订单之间又是可以并行消费的。
由于网络的问题,消息重复发送,会导致严重的问题。解决重复问题(消费端收到两条一样的消息怎么办):①业务逻辑保持幂等性;②保证每个消息都有唯一的编号,并有去重表对重复消息做筛选。
如何解决重复问题以及顺序问题后面再说。
支持事务的消息中间件
Apache开源的RocketMQ中间件能够支持一种事务消息机制,确保本地操作和发送消息的异步处理达到本地事务的结果一致。
第一阶段,RocketMQ在执行本地事务之前,会先发送一个Prepared消息,并且会持有这个消息的接口回查地址。
第二阶段,执行本地事物操作。
第三阶段,确认消息发送,通过第一阶段拿到的接口地址URL执行回查,并修改状态,如果本地事务成功,则修改状态为已提交,否则修改状态为已回滚。
其中,如果第三阶段的确认消息发送失败后,RocketMQ会有定时任务扫描集群中的事务消息,如果发现还是处于prepare状态的消息,它会向消息发送者确认本地事务是否已执行成功。 RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。
这样就保证了消息的发送与本地事务同时成功或同时失败。
再回到上面转账的例子,如果用户A的账户余额已经减少,且消息已经发送成功,作为消费者用户B开始消费这条消息,这个时候就会出现消费失败和消费超时两个问题,解决超时问题的思路就是一直重试,直到消费端消费消息成功,整个过程中有可能会出现消息重复的问题,就需要采用幂等方案来进行处理。
失败 rollback
rollback 丢失
上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。
那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。
系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。
当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。
该接口会返回三种结果:
- 提交
若获得的状态是“提交”,则将该消息投递给系统B。
- 回滚
若获得的状态是“回滚”,则直接将条消息丢弃。
- 处理中
若获得的状态是“处理中”,则继续等待。
消息丢失的重试
如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。
当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。
如果重试3次之后仍然投递失败,那么这条消息就需要人工干预。