highlight: arduino-light
两阶段提交协议
原文链接:https://blog.csdn.net/fenglibing/article/details/92417739
两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器TC和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。
我们根据上面的图来看看主要流程:
1) 我们的应用程序(client)发起一个开始请求到TC(transaction);
2) TC先将prepare消息写到本地日志,之后向所有的业务系统发起prepare消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证的效果,如果没有本地日志(凭证),出问题容易死无对证;
3) 业务系统收到prepare消息后,执行具体本机事务,但不会进行commit,如果成功返回yes,不成功返回no。同理,返回前都应把要返回的消息写到日志里,当作凭证。
4) TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。
注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到commit,则提交,如果abort则回滚。如果是yes,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在prepare阶段Si就崩溃了,因此需要回滚。
现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/),来快速实现。)
分布式事务选型
https://blog.csdn.net/qq_42556214/article/details/105796048
分布式事务性能问题
不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?
1)两阶段提交涉及多次节点间的网络通信,通信时间太长!
2)事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!
正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。
使用消息队列来避免分布式事务
如果仔细观察生活的话,生活的很多场景已经给了我们提示。
比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。
为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。
还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。
那么我们如何可靠保存凭证(消息)有两种方法:
1)业务数据与消息耦合的方式:数据库
支付宝在完成扣款的同时,同时在数据库记录消息数据,消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。
Begin transaction
update A set amount=amount-10000 where userId=1;
insert into message(userId, amount,status) values(1, 10000, 1);
End transaction
commit;
上述事务能保证只要用户支付宝账户里被扣了钱,消息一定能保存下来。
1.支付宝减少余额,并插入Message1表
2.定时器扫描message表,请求余额宝,余额宝处理成功则将数据置为已处理
3.余额宝增加余额
假如在第一步处理失败 - 业务数据回滚 没影响
假如在第二步处理失败 - 余额宝处理成功,没有置为已处理,那么定时器仍然会去扫描这条消息
假如在第三步处理失败 - 不会返回成功响应,也不会将数据置为已处理那么定时器仍然会去扫描这条消息
2)业务与消息解耦方式:消息队列
上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。
a)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;
b)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;
c)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;
d)对于那些未确认的消息或者取消的消息,需要有一个定时任务去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。
优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。
RocketMq解决分布式事务方案
比如一个下订单扣库存的动作,这两个服务分别操作订单库和库存库,属于分布式事务范畴了,如果mq不支持事务,那么可能做法是:
```md //step1:开启本地事务
//step2:订单库新增一条记录
//step3:向mq发送订单消息,用于扣库存
//step4:提交事务/回滚事务 ```
该方案在正常情况下没有问题,但是一些异常情况下就有了问题:
1.如果step3执行后,在step4执行前jvm进程or服务器宕机,事务没有成功提交,订单数据库回滚没变化和但是库存数据库减少,导致两个库数据不一致。
2.由于消息是在事务提交之前提交,发送的消息内容是订单实体的内容,会造成在消费端进行消费时如果需要去验证订单是否存在时可能出现订单不存在,该问题也会存在,因为消费端速度很快的话。
对于生成订单(DB操作)和发送消息是一个事务内的动作,因此要保证要么全部成功,要么回滚,因此可以采用rocketmq的事务消息来解决。
rocketmq事务消息解决分布式事务,实现最终数据一致性,思想就是xa协议2pc。 ```md //step1:向mq发送事务订单消息 用于扣库存。此时事务消息不可见
//step2:开启本地事务,订单库新增一条记录
//step3:触发本地逻辑:判断本地事务是否执行成功。
本地事务执行成功提交事务并提交事务消息即可。此时事务消息可见
本地事务执行失败提交事务并回滚事务消息即可。此时事务消息不可见
假设出现异常,本地事务或者事务消息是提交还是回滚未知,此时可以通过事务回查机制判断本地事务是否执行成功。
本地事务执行成功提交事务并提交事务消息即可。此时事务消息可见
本地事务执行失败提交事务并回滚事务消息即可。此时事务消息不可见
```
事务回查相关参数:
transactionTimeout==60s即事务回查超时时间。
transactionCheckMax==15即最大回查次数。
listener是DefaultTransactionalMessageCheckListener。
功能:读取当前half的half queueoffset,然后从op half拉取32条消息保存到removeMap,如果half queueoffset处的消息在removeMap中, 则说明该prepare消息被处理过了,然后读取下一条prepare消息,如果prepare不在removeMap中,说明是需要回查的,此时broker作为client端,向服务端producer发送回查命令,最后由producer返回回查结果更新原prepare消息。 ```md 考虑几个事务消息的异常状态: 1.preprare消息发送成功,本地事务执行成功,但是producer宕机 该情况broker会进行回查事务状态,从而提交事务,发送消息给下游系统。 2.preprare消息发送成功,本地事务执行过程中producer宕机了 事务执行过程宕机了,那么数据库自动会回滚事务,事务就是没执行成功,因此broker回查从而删除preprae消息。 3.preprare消息发送成功,本地事务执行成功,但是因为broker宕机发送commit消息给broker失败(发送给prepare消息接收的那台broker), 启动该broker,broker回查到事务执行成功,从而提交消息,发送消息给下游系统进行消费,该情况会导致下游有长时间延迟才收到消息消费。 4.客户端消费消息失败了,怎么办? rocketmq给出的方案是人工解决,这样的情况不能多,如果多了,需要优化业务和代码,实际用rocketmq的事务消息,客户端消费失败情况是少的,比如扣库存动作,基本都是成功的。客户端消费失败的情况通常是通过对账根据业务情况解决。
使用rocketmq来保证分布式事务属于消息一致性方案,通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。 ``` 消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。
RocketMQ 中的分布式事务实现
在 RocketMQ 中的事务实现中,增加了事务反查的机制来解决事务消息提交失败的问题。
如果 Producer 也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。为了支撑这个事务反查机制,我们的业务代码需要实现一个反查本地事务状态的接口,告知RocketMQ 本地事务是成功还是失败。
在我们这个例子中,反查本地事务的逻辑也很简单,我们只要根据消息中的订单 ID,在订单库中查询这个订单是否存在即可,如果订单存在则返回成功,否则返回失败。
RocketMQ 会自动根据事务反查的结果提交或者回滚事务消息。
这个反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。
这种情况下,即使是发送事务消息的那个订单服务节点宕机了,RocketMQ 依然可以通过其他订单服务的节点来执行反查,确保事务的完整性。
综合上面讲的通用事务消息的实现和 RocketMQ 的事务反查机制,使用 RocketMQ 事务消息功能实现分布式事务的流程如下图
分布式事务性能
为了方便大家理解,我们再来举一个银行转账的示例(和上一个例子差不多):
比如,Bob向Smith转账100块。
在单机环境下,执行事务的情况,大概是下面这个样子:
当用户增长到一定程度,Bob和Smith的账户及余额信息已经不在同一台服务器上了,那么上面的流程就变成了这样:
这时候你会发现,同样是一个转账的业务,在集群环境下,耗时居然成倍的增长,这显然是不能够接受的。那如何来规避这个问题?
大事务 = 小事务 + 异步
将大事务拆分成多个小事务异步执行。这样基本上能够将跨机事务的执行效率优化到与单机一致。转账的事务就可以分解成如下两个小事务:
图中执行本地事务(Bob账户扣款)和发送异步消息应该保证同时成功或者同时失败,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢?
首先看下先发送消息的情况,大致的示意图如下:
存在的问题是:如果消息发送成功,但是扣款失败,消费端就会消费此消息,进而向Smith账户加钱。
先发消息不行,那就先扣款吧,大致的示意图如下:
存在的问题跟上面类似:如果扣款成功,发送消息失败,就会出现Bob扣钱了,但是Smith账户未加钱。
可能大家会有很多的方法来解决这个问题,比如:直接将发消息放到Bob扣款的事务中去,如果发送失败,抛出异常,事务回滚。这样的处理方式也符合“恰好”不需要解决的原则。
RocketMQ支持事务消息,下面来看看RocketMQ是怎样来实现的?
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。
细心的你可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?
RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。