事务消息的实现方式选择
微服务架构本质上就是分布式服务化架构,微服务架构的流行,让分布式事务问题日益突出!尤其是在订单业务、资金业务等系统核心业务流程中,一定要有可靠的分布式事务解决方案来保证业务数据的可靠性和准确性。在所有的分布式事务解决方案中,“事务消息”是其中性能最好的一种。
在分布式环境下,由于网络上数据传输的不确定性,导致了消息发送和投递的不可靠性。然而考虑到只要消息数据持久化了,一般来说最终一定会被消费,就算消费方完全挂了,后续把挂掉的服务重新启动后,消息还是会被消费,不会丢失,可以保证最终一致性。因此在设计事务消息方案时,一般聚焦于如何保证“发送一致性”,即产生消息的业务动作与消息发送的一致。也就是说,如果业务操作成功,那么由这个业务操作所产生的消息一定要成功投递出去,否则就丢消息。
下面将介绍事务消息的三种技术方案。为方便介绍,这里先假设一个业务场景,我们在下单购买商品的时候,需要在下单完成购买后,确保商品送货到家,即订单操作和送货操作要保持一致性,但并不要求强一致性,只需要满足最终一致性即可。以这个场景为例,下单买了商品,在一段时间内商家开始发货并送货到家即可,不要求刚下单就发货。
按照微服务的架构,上述场景涉及到订单服务和货运服务,并且设计到订单库和货运记录库两个数据库,所以不能用本地事务解决问题。接下来我们结合这个业务场景来描述整个事务消息方案,包括 本地消息服务、半独立消息服务、独立消息服务 三种变体。其中可以把订单服务看作主动方应用系统,货运服务看作被动方应用系统。
方案一:本地消息服务
- 下单,将订单业务数据存入订单库中
- 存储该订单对应的消息数据并进行消息发送
a. 将消息数据写入订单库中,1和2.a为一个本地事务,要么都成功,要么都失败。
b. 如果消息写入成功,将消息发送给消息服务
c. 在收到发送成功的确认后从数据库删除已发送消息 - 货运服务从消息服务中收到订单消息
- 货运服务根据订单消息中的商品和送货地址等信息进行业务操作
异常情况分析
异常情况 | 最终结果(主动方) | 最终结果(消息服务) | 最终结果(被动方) | 一致性 |
---|---|---|---|---|
业务操作失败 | 消息存储和消息发送不会发生 | 无 | 未收到消息 | 一致 |
消息存储失败 | 业务操作由于事务回滚而撤销 | 无 | 未收到消息 | 一致 |
消息发送失败未到达消息服务 | 无 | 消息恢复系统定时去向主动方应用查询超时未发出消息,对超时未发送消息进行消息投递,并从主动方数据库删除消息 | 收到消息(可能顺序错乱) | *不一致 |
消息发送超时但实际上消息已发送到消息服务 | 无 | 同上 | 收到消息(可能重复消息) | *不一致 |
消息发送成功但从数据库删除已发送消息失败 | 无 | 同上 | 收到消息(可能重复消息) | *不一致 |
消息恢复系统发送消息成功但从从主动方数据库删除消息失败 | 无 | 同上 | 收到消息(可能重复消息) | *不一致 |
消息恢复系统发送消息超时但消息实际上已发送到消息服务 | 无 | 同上 | 收到消息(可能重复消息) | *不一致 |
* 重复消息或顺序错乱在弱一致性要求下,也可以算作是一致的,而且在被动方应用引入去重手段可以消除重复消息的影响(即实现幂等消费)
优点
- 消息直接从应用发送到消息服务,时效性比较高;如做异步发送,对主动方应用的性能影响更低。
- 消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。除MQ外,也可以应用于邮件、短信等其他类型的消息服务。
- 方案轻量,容易实现。
缺点
- 主动方应用需要提供消息查询接口。
- 消息数据与业务数据同库,占用业务系统资源。
- 不能保证消息投递的强一致性要求
方案二:独立消息服务
- 用户下单,主动方应用预发送消息给消息服务子系统。
a.将预发送消息发给消息服务
b. 消息服务子系统存储预发送的消息,消息状态为“待确认”。
c. 返回存储预发送消息的结果。 - 如果第1步操作是成功的,则执行业务操作,否则不执行。
- 业务操作结束后,根据业务操作结果向消息服务发送提交或回滚消息。
- 消息服务收到提交消息后,将对应消息置为“待发送”状态,并发送到消息中间件,而后从消息库中删除;如收到回滚消息,则直接从消息库中删除。
- 被动方应用消费消息并进行相应的业务处理。
异常情况分析
异常情况 | 最终结果(主动方) | 最终结果(消息服务) | 最终结果(被动方) | 一致性 |
---|---|---|---|---|
预发送消息未到达消息服务 | 预发送消息失败,业务操作未执行 | 无 | 未收到消息 | 一致 |
消息服务返回确认预发送消息成功,但主动方未收到确认消息 | 从主动方应用角度来看预发送消息超时,发送失败,业务操作未执行 | 消息服务定期确认业务操作结果,删除对应业务未执行的预发送消息 | 未收到消息 | 一致 |
消息服务没有收到主动方应用的业务操作结果确认消息(提交或回滚),但对应业务操作已成功 | 无 | 消息服务定期确认业务操作结果,因为业务已操作成功,所以更新对应消息状态为“待发送”,执行消息投递 | 收到消息(可能顺序错乱) | * 不一致 |
消息服务没有收到主动方应用的业务操作结果确认消息(提交或回滚),且对应业务操作未执行或回滚 | 无 | 消息服务定期确认业务操作结果,因为业务未操作成功,所以删除对应消息 | 未收到消息 | 一致 |
* 顺序错乱在弱一致性要求下,也可以算作是一致的
优点
- 消息数据不必入业务库,节省了业务库容量
缺点
- 一次消息发送需要两次请求(预发送和确认消息),影响主动方应用的性能。
- 主动方应用需要提供业务操作状态查询接口。
- 消息服务的实现复杂度高
- 不能保证消息投递的强一致性要求
方案三:半独立消息服务
- 下单,在一个事务中完成订单数据和对应消息的入库
a. 将订单数据写入订单库中。
b. 将消息数据也写入订单库,但在一个单独的消息表中 - Kafka Connector 从数据库的事务日志(“预写日志”,WAL)中捕获消息表中的任何变化
- Kafka Connector 的消息路由将消息表中的数据变化转换为 Kafka 消息并路由转发到相应的 Topic 上
- 被动方应用从 Kafka 消费消息并进行相应的业务处理。
异常情况分析
异常情况 | 最终结果(主动方) | 最终结果(消息服务) | 最终结果(被动方) | 一致性 |
---|---|---|---|---|
业务操作失败 | 消息存储不会发生 | 无 | 未收到消息 | 一致 |
消息存储失败 | 业务操作由于事务回滚而撤销 | 无 | 未收到消息 | 一致 |
Kafka Connect 进程崩溃 | 无 | Kafka Connect 集群的其他节点上将重启相应数据库抽取任务,同时从上一进程记录的最后一个偏移量处恢复,这意味着重启的任务可能会生成一些重复消息 | 收到消息(可能重复消息) | *不一致 |
Kafka Connect 向 Kafka发送消息超时,消息实际上已发送成功 | 无 | Kafka Connect 会向 Kafka 重试发送消息,同时在当前这批消息未发送成功前,不会发送下一批消息 | 收到消息(可能重复消息) | *不一致 |
* 重复消息在弱一致性要求下,也可以算作是一致的,而且在被动方应用引入去重手段可以消除重复消息的影响(即实现幂等消费)
优点
- 对应用的侵入较低,除保存待发送消息外,不需要再添加其他操作
- 能保证较强的消息的一致性,可能出现重复消息,但不会乱序
- 实现复杂度较低,基于现有的Kafka Connect,只需开发一个相应的消息路由功能
- 性能高,应用不需要直接发消息。经实测,对业务操作的性能影响在 20% 以内,且业务事务越大,影响率越低。
缺点
- 消息数据与业务数据同库,占用业务系统资源。
- 暂时只支持 Kafka
方案评估和选择
根据对上述事务消息方案的架构描述和简单的优缺点说明后,我们根据需要关注的质量属性点,进行360度环评,综合挑选当下最适合的方案。
质量属性 | 本地消息 | 独立消息 | 半独立消息 |
---|---|---|---|
性能 | 高 | 中,要做两次消息发送 | 高 |
复杂度 | 中,需要开发消息恢复系统 | 高,自研消息服务复杂度很高 | 低,只需在现有的 Kafka Connect 基础上做简单的扩展,增加消息路由功能即可 |
应用侵入 | 中,需要提供消息查询接口 | 高,既要添加预发送消息操作,又要添加业务处理结果查询接口 | 低,只需存储待发送消息 |
一致性保证 | 低,不保证消息顺序并且可能出现重复消息 | 低,不保证消息顺序 | 中,可能出现重复消息 |
通过上述比较,相对而言“半独立消息”方案最优,原因有:
- 排除“独立消息”方案的主要原因是:复杂度和人力投入太高了,实际上开源已有具备事务消息功能的消息服务 RocketMQ,与其花费巨大资源自研开发一套类似的系统,不如直接使用开源方案;当然即便是 RocketMQ 也由于两次消息发送及其半消息内部实现机制的原因,存在性能不高的问题。
- 排除“本地消息”方案的主要原因是:无法保证消息顺序不错乱,这在某些应用场景下基本是致命的;另外,对应用的侵入导致应用开发工作量增加
- “半独立消息”性能高,复杂度低,可靠性也高
最后说明一下,既然提到了RocketMQ,为什么没有就直接采用这个开源方案呢?这里的考虑是(除了上述所述原因外)站在一个数据架构的角度来看,我们希望:
- 消息系统不仅仅是承载应用系统间消息交互的基础设施,还是一个实时事件流平台,在这个平台上可以进行实时流处理;
- 另一方面,我们希望这个事件流平台,能成为各种应用、大数据分析系统、数据存储系统间的一个汇流平台,所有这些系统都能接入这个中央流平台,然后很容易的进行信息交换
而RocketMQ的主要问题是:
- 它目前只具备消息发布和订阅功能,不能在上面进行实时流处理,而 Kafka 已经有 Kafka Stream 和 KSQL 来支持这方面的功能
- 围绕 RocketMQ 建立的软件生态系统,目前还没有建立起来,许多第三方系统需要定制开发来接入 RocketMQ,这种人力投入在绝大部分公司都是难以达成的