###真的需要分布式事务?
因为我们需要各个资源数据一致性。对,看起来合情合理,我们需要,而分布式事务恰好解决这个问题,但是分布式事务提供的是强一致性。试问下,我们真的需要强一致性吗?大多数业务场景都能容忍短暂的不一致,只是不同的业务对不一致的时间窗口要求不同罢了,现实生活中的餐馆买面条,他给你的是单号,而不是面条。
爱因斯坦说过:我们无法用我们制造问题的思维方式去解决我们的制造的问题。
本地消息表(业务耦合)
这种实现方式的思路,其实是源于 ebay,后来通过支付宝等公司的布道,在业内广泛使用。其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。如果不考虑性能及设计优雅,借助关系型数据库中的表即可实现。
以跨行转账为例。
第一步:扣款 1万,通过本地事务保证了凭证消息插入到消息表中。(图中写业务即理解为扣除1万)
第二步,通知对方银行账户上加 1万了。那问题来了,如何通知到对方呢?
常采用两种方式:
采用时效性高的 MQ,由对方订阅消息并监听,有消息时自动触发事件
采用定时轮询扫描的方式,去检查消息表的数据。
两种方式其实各有利弊,仅仅依靠 MQ,可能会出现通知失败的问题。而过于频繁的定时轮询,效率也不是最佳的(90% 是无用功)。所以,我们一般会把两种方式结合起来使用。
解决了通知的问题,又有新的问题了。万一这消息有重复被消费,往用户帐号上多加了钱,那岂不是后果很严重?
仔细思考,其实我们可以消息消费方,也通过一个“消费状态表”来记录消费状态。在执行“加款”操作之前,检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地事务控制来更新这个“消费状态表”。这样子就避免重复消费的问题。
总结:上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
MQ(非事务消息,细化本地消息表方案)
通常情况下,在使用非事务消息支持的 MQ 产品时,我们很难将业务操作与对 MQ 的操作放在一个本地事务域中管理。通俗点描述,还是以上述提到的“跨行转账”为例,我们很难保证在扣款完成之后对 MQ 投递消息的操作就一定能成功。这样一致性似乎很难保证。
根据上述代码及注释,我们来分析下可能的情况:
操作数据库成功,向 MQ 中投递消息也成功,皆大欢喜(没问题)
操作数据库成功,但是向 MQ 中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚(没有问题)
操作数据库失败,不会向 MQ 中投递消息了(没问题)
操作数据库成功,mq发送成功,但是返回响应的时候网络异常,导致append操作抛出异常。(有问题)最终结果是事件被投递,数据库确被回滚。
操作数据库成功,result是true,这个时候服务挂了,没有投递消息给MQ。(有问题)最终结果是数据库操作成功,事件未被投递。
为了解决如上问题,引入本地消息表。
优化后伪代码:
那么我们来分析下消费者端面临的问题:
消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果
如何保证消息与业务操作一致,不丢失?
主流的MQ 产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些 MQ 可以自定义重试次数)。
如何避免消息被重复消费造成的问题?
保证消费者调用业务的服务接口的幂等性
通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖 MQ 产品提供该特性)
总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果 MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下,不建议在交易业务中直接使用。
外部消息事件(消息与业务解耦)
外部事件表:
1、 业务服务在事务提交前,通过实时事件服务向事件系统请求发送事件,事件系统只记录事件并不真正发送;
2、 业务服务在提交后,通过实时事件服务向事件系统确认发送,事件得到确认后事件系统才真正发布事件到消息代理;
3、 业务服务在业务回滚时,通过实时事件向事件系统取消事件;
4、 如果业务服务在发送确认或取消之前停止服务了怎么办呢?;
事件系统的事件恢复服务会定期找到未确认发送的事件向业务服务查询状态,根据业务服务返回的状态决定事件是要发布还是取消该方式将业务系统和事件系统
独立解耦,都可以独立伸缩。但是这种方式需要一次额外的发送操作,并且需要发布者提供额外的查询接口介绍完了可靠事件投递再来说一说幂等性的实现,有些事件本身是幂等的,有些事件却不是。
优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。
MQ(事务消息)
举个例子,Bob 向 Smith 转账,那我们到底是先发送消息,还是先执行扣款操作?
好像都可能会出问题。如果先发消息,扣款操作失败,那么 Smith 的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith 收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?
下面以阿里巴巴的 RocketMQ 中间件为例,分析下其设计和实现思路。
RocketMQ 第一阶段发送 Prepared 消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ 会定期扫描消息集群中的事物消息,这时候发现了 Prepared 消息,它会向消息发送者确认,Bob 的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:
业务补偿模式
补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。
例子:
一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。
(1)上海-北京6月19日9点的某某航班
(2)某某酒店住宿3晚
(3)北京-上海6月22日17点火车
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。
为了降低开发的复杂性和提高效率,协调服务实现为一个通用的补偿框架。补偿框架提供服务编排和自动完成补偿的能力。
要实现补偿过程,我们需要做到两点:
首先要确定失败的步骤和状态,从而确定需要补偿的范围。
在上面的例子中我们不光要知道第3个步骤(预订火车)失败,还要知道失败的原因。如果是因为预订火车服务返回无票,那么补偿过程只需要取消前两个步骤就可以了;但是如果失败的原因是因为网络超时,那么补偿过程除前两个步骤之外还需要包括第3个步骤。
其次要能提供补偿操作使用到的业务数据。
比如一个支付微服务的补偿操作要求参数包括支付时的业务流水id、账号和金额。理论上说实际完成补偿操作可以根据唯一的业务流水id就可以,但是提供更多的要素有益于微服务的健壮性,微服务在收到补偿操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等。
做到上面两点的办法是记录完整的业务流水,可以通过业务流水的状态来确定需要补偿的步骤,同时业务流水为补偿操作提供需要的业务数据。
对于一个通用的补偿框架来说,预先知道微服务需要记录的业务要素是不可能的。那么就需要一种方法来保证业务流水的可扩展性,
这里介绍两种方法:大表和关联表。
1、 大表顾明思议就是设计时除必须的字段外,还需要预留大量的备用字段,框架可以提供辅助工具来帮助将业务数据映射到备用字段中;
2、 关联表,分为框架表和业务表,技术表中保存为实现补偿操作所需要的技术数据,业务表保存业务数据,通过在技术表中增加业务表名和业务表主键来建立和业务数据的关联;
补充:
1、 微服务实现补偿操作不是简单的回退到业务发生时的状态,因为可能还有其他的并发的请求同时更改了状态一般都使用逆操作的方式完成补偿;
2、 补偿过程不需要严格按照与业务发生的相反顺序执行,可以依据工作服务的重用程度优先执行,甚至是可以并发的执行;
3、 有些服务的补偿过程是有依赖关系的,被依赖服务的补偿操作没有成功就要及时终止补偿过程;
4、 如果在一个业务中包含的工作服务不是都提供了补偿操作,那我们编排服务时应该把提供补偿操作的服务放在前面,这样当后面的工作服务错误时还有机会补偿;
5、 设计工作服务的补偿接口时应该以协调服务请求的业务要素作为条件,不要以工作服务的应答要素作为条件因为还存在超时需要补偿的情况,这时补偿框架就没法提供补偿需要的业务要素;