学了微服务架构,不会分布式事务,等于没学?

架构之道与术

共 2237字,需浏览 5分钟

 ·

2021-08-29 16:49

市面上各种讲述微服务框架的教程(SpringCloud, Dubbo),但这些框架解决的是应用层的网络通信问题,存储层的数据一致性问题还是需要自己去解决。


集中式时代,所有数据装一个DB里面,通过单机事务来保证数据一致性。改成微服务之后,DB不可避免的要拆库拆表,所以分布式事务就变成了一个无法逃避的问题。


解决分布式事务的方法很多,2PC、TCC、全局事务协调器。。。这里仔细讨论2个最常见、最实用的办法:最终一致性 + 对账。


从一个最简单的例子入手:

微服务A要更新自己的DB,同时调用微服务B的一个接口更新微服务B的数据。


最终一致性的思路大家基本上都知道,就是微服务A更新完自己的DB之后,通过消息中间件给微服务B发一条消息。但实现细节上却有很多“坑”:


方案1:最终一致性的第1种实现方式

1) 微服务A增加一张消息表,微服务A不再直接给消息中间件发送消息,而是把消息写入到这张消息表中,把自身的DB操作和写入消息表这两个操作放在一个数据库事务里,保证两者的原子性。

2)微服务A准备一个后台程序,源源不断地把消息表中的消息传送给消息中间件。发成功了,从DB消息表里面删除;失败了,不断尝试重传。因为网络的2将军问题,微服务A发送给消息中间件的消息超时了,微服务A会再次发送该消息,直到消息中间件返回成功。所以,微服务A发送给消息中间件的消息可能重复,但不会丢失,顺序也不会打乱。


微服务B对消息的消费要解决下面两个问题:

问题1:丢失消费。微服务B从消息中间件取出消息(此时还在内存里面),如果处理了一半,微服务B宕机并再次重启,此时这条消息未处理成功,怎么办?

答案是通过消息中间件的ACK机制,凡是没有发送ACK的消息,微服务B重启之后消息中间件会再次推送。


问题2: 重复消费。重复消费有2个场景:

场景1: 即使微服务B把消息处理成功了,但是正要发送ACK的时候宕机了,消息中间件以为这条消息没有处理成功,微服务B再次重启的时候又会收到这条消息,微服务B就会重复消费这条消息

场景2: 上面说的,微服务A重复发送,导致微服务B的重复消费。


解决重复消费的办法就是“幂等”。关于实现幂等的N种办法,参见作者另外一篇文章。


这种方案有一个缺点:系统A需要增加消息表,同时还需要一个后台任务,不断扫描此消息表,会导致消息的处理和业务逻辑耦合,额外增加业务方的开发负担。


为此,业界有了第2种实现最终一致性的办法。


方案2:最终一致性 - 事务消息

以阿里开源的RocketMQ为例,它不是提供一个单一的“发送”接口,而是把消息的发送拆成了两个阶段,Prepare阶段(消息预发送)和Confirm阶段(确认发送)。具体使用方法如下:

步骤1:微服务A调用Prepare接口,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。

步骤2:微服务A做业务逻辑,更新自己的DB

步骤3:微服务A调用Comfirm接口,确认发送消息。此时消息中间件才会把消息给消费方进行消费。


显然,这里有两种异常场景:

场景1:步骤1成功,步骤2成功,步骤3失败或超时,怎么处理?

场景2:步骤1成功,步骤2失败或超时,步骤3不会执行。怎么处理?

这就涉及RocketMQ的关键点:RocketMQ会定期扫描所有的预发送但还没有确认的消息,回调发送方,询问这条消息是要发出去,还是取消。发送方根据自己的业务数据,知道这条消息是应该发出去(DB更新成功了),还是应该取消(DB更新失败)。

对比最终一致性的两种实现方案会发现,RocketMQ最大的改变其实是把“扫描消息表”这件事不让业务方做,而是让消息中间件完成。

至于消息表,其实还是没有省掉。因为消息中间件要询问发送方事务是否执行成功,还需要一个“变相的本地消息表”,记录事务执行状态和消息发送状态。

对于消费方微服务B,实现方式没有变化,需要解决自己上面的丢失消费 + 重复消费问题。


最后抛个问题:Kafka里面也实现了事务消息,能否拿来用在这个场景,有什么缺陷?


方案3:同步调用 + 对账

微服务A更新自己的DB,同步调用微服务B的接口,如果2者都成功,皆大欢喜;调用接口失败,开启对账。


(1)即时对账:

失败之后,往消息中间件扔一个异常消息。用一个后台程序消费消息,重新调用微服务B,直到2个微服务数据补齐。

但这个办法不能100%保证,给消息中间件发消息也很能失败


事后对账:

用一个后台程序,调用微服务A的查询接口,分批次查询数据,再调用微服务B的查询接口。比对,不一致,用A的数据补齐B的数据。

这里又分为增量对账、全量对账。

增量,可以按数据更新时间,每次取最近的一批数据对;

全量,每次都对所有的。

一般策略是:频繁的增量对,比如几分钟一次;偶尔全量对,比如每天凌晨做一次。


即时对账是保证及时性,事后对账保证不漏,做兜底。2者是配合关系,不是取代。


最后做一个总结:最终一致性方案是放弃了及时性,保证数据最终一致。而同步更新 + 对账,99%情况下数据是强一致,1%异常情况下,对账补齐。


最后留个问题:还没来得及对账补齐之前,数据被下游其它服务读了,导致下游服务出现业务逻辑错误,怎么解决?


浏览 50
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报