概述
在微服务开发中咱们常常会引入消息中间件实现业务解耦,执行异步操作,当初让咱们来看看应用消息中间件的益处和弊病。
首先须要必定是应用音讯组件有很多益处,其中最外围的三个是:解耦、异步、削峰。
- 解耦:客户端只有讲申请发送给特定的通道即可,不须要感知接管申请实例的状况。
- 异步:将音讯写入音讯队列,非必要的业务逻辑以异步的形式运行,放慢响应速度。
- 削峰:消息中间件在音讯被生产之前始终缓存音讯,音讯解决端能够依照本人解决的并发量从音讯队列中缓缓解决音讯,不会一瞬间压垮业务。
当然消息中间件并不是银弹,引入音讯机制后也会有如下一些弊病:
- 潜在的性能瓶颈:音讯代理可能会存在性能瓶颈。侥幸的是目前支流的消息中间件都反对高度的横向扩大。
- 潜在的单点故障:音讯代理的高可用性至关重要,否则零碎整体的可靠性将受到影响,侥幸的是大多数消息中间件都是高可用的。
- 额定的操作复杂性:音讯零碎是一个必须独立装置、配置和运维的零碎组件,减少了运维的复杂度。
这些弊病咱们借助消息中间件自身提供的扩大、高可用能力能够解决,然而要真正用好消息中间件咱们还须要关注可能会遇到的一些设计难题。
解决并发和程序音讯
在生产环境中为了进步音讯解决的能力以及应用程序的吞吐量,个别会将消费者部署多个实例节点。那么带来的挑战就是 如何确保每个音讯只被解决一次,并且是依照他们的发送程序来解决的。
例如:假如有 3 个雷同的接管方实例从同一个点对点通道读取音讯,发送方按程序公布了 Order Created
、Order Updated
和 Order Cancelled
这 3 个事件音讯。简略的音讯实现可能就会共事讲每个音讯给不同的接管方。若因为网络问题导致提早,音讯可能没有依照他们收回时的程序被解决,这将导致奇怪的行为,服务实例可能在另一个服务器解决 Order Created
音讯之前解决 Order Cancelled
音讯。
Kafka 应用的解决方案是应用分片(分区)通道。整体解决方案分为三个局部:
- 一个主题通道由多个分片组成,每个分片的行为相似一个通道。
- 发送方在音讯头部指定分片键如 orderId,Kafka 应用分片键将音讯调配给特定的分片。
- 将接管方的多个实例组合在一起,并将他们视为雷同的逻辑接管方(消费者组)。kafka 将每个分片调配给单个接收器,它在接管方启动和敞开时重新分配分片。
如上图所示,每个 Order 事件音讯都将 orderId 作为其分片键。特定订单的每个事件都公布到同一个分片。而且该分片中的音讯始终由同一个接管方实例读取,因而这样就可能保障按程序解决这些音讯。
解决反复音讯
引入音讯架构必须要解决的另一个挑战是解决反复音讯。在现实状况下,音讯代理应该只传递一次音讯,但保障音讯有且仅有一次的消息传递的老本通常很高。相同,很多音讯组件承诺至多保障胜利传递一次音讯。
在失常状况下,音讯组件只会传递一次音讯。然而当客户端、网络或音讯组件故障可能导致音讯被屡次传递。假如客户端在解决音讯后发送确认音讯前,他的数据库解体了,这时音讯组件将再次发送未确认的音讯,在数据库重新启动时向该客户端发送。
解决反复音讯有以下两种不同的办法:
- 编写幂等音讯处理程序
- 跟踪音讯并抛弃反复项
编写幂等音讯处理器
如果利用程序处理音讯的逻辑是满足幂等的,那么反复音讯就是有害的。程序的幂等性是指,即便这个利用被雷同输出参数多次重复调用时,也不会产生额定的成果。例如:勾销一个曾经勾销的订单,就是一个幂等性操作。同样,创立一个曾经存在的订单操作也必是这样。满足幂等的音讯处理程序能够被释怀的执行屡次,只有音讯组件在传递音讯时放弃雷同的音讯程序。
然而可怜的是,应用程序通常不是幂等的。或者你当初正在应用的音讯组件在从新传递音讯时不会保留排序。反复或无序音讯可能会导致谬误。在这种状况下,你须要编写跟踪音讯并抛弃反复音讯的音讯处理程序。
跟踪音讯并抛弃反复音讯
思考一个受权消费者信用卡的音讯处理程序。它必须为每个订单仅执行一次信用卡受权操作。这段应用程序每次调用时都会产生不同的成果。如果反复音讯导致音讯处理程序屡次执行该逻辑,则应用程序的行为将不正确。执行此类利用程序逻辑的音讯处理程序必须通过检测和抛弃反复音讯而让它成为幂等的。
一个简略的解决方案是音讯接管方应用 message id 跟踪他已解决的音讯并抛弃任何反复项。例如,在数据库表中存储它生产的每条音讯的 message id。
当接管方解决音讯时,它将音讯的 message id 作为创立和变更业务实体的事务的一部分记录在数据表里。如上图所示,接管方将蕴含 message id 的行插入 PROCESSED_MESSAGE 表。如果音讯是反复的,则 INSERT 将失败,接管方能够抉择抛弃该音讯。
另一个解决方案是音讯处理程序在应用程序表,而不是专门表中记录 message id。过后用具备受限事务模型的 NoSQL 数据库时,此办法特地有用,因为 NoSQL 数据库通常不反对将针对两个表的更新作为数据库事务。
解决事务性音讯
服务通常须要在更新数据库的事务中公布音讯,数据库更新和音讯发送都必须在事务中进行,否则服务可能会更新数据库而后在发送音讯之前解体。
如果服务不以原子形式执行者两个操作,则相似的故障可能使零碎处于不统一状态。
接下来咱们看一下罕用的保障事务音讯的两种解决方案,最初再看看古代音讯组件 RocketMQ 的事务性音讯解决方案。
应用数据库表作为音讯队列
如果你的应用程序正在应用关系型数据库,要保证数据的更新和音讯发送之间的事务能够间接应用 事务性发件箱模式,Transactional Outbox。
此模式应用数据库表作为长期音讯队列。如上图所示,发送音讯的服务有个 OUTBOX 数据表,在进行 INSERT、UPDATE、DELETE 业务操作时也会给 OUTBOX 数据表 INSERT 一条音讯记录,这样能够保障原子性,因为这是基于本地的 ACID 事务。
OUTBOX 表充当长期音讯队列,而后咱们在引入一个音讯中继(MessageRelay)的服务,由他从 OUTBOX 表中读取数据并公布音讯到音讯组件。
音讯中继的实现能够很简略,只须要通过定时工作定期从 OUTBOX 表中拉取最新未公布的数据,获取到数据后将数据发送给音讯组件,最初将实现发送的音讯从 OUTBOX 表中删除即可。
应用事务日志公布事件
另外一种保障事务性音讯的形式是基于数据库的事务日志,也就是所谓的数据变更捕捉,Change Data Capture,简称 CDC。
个别数据库在数据产生变更的时候都会记录事务日志(Transaction Log),比方 MySQL 的 binlog。事务日志能够简略的了解成数据库本地的一个文件队列,它次要记录按工夫程序产生的数据库表变更记录。
这里咱们利用 alibaba 开源的组件 canal 联合 MySQL 来阐明下这种模式的工作原理。
更多操作阐明能够参考官网文档:https://github.com/alibaba/canal
canal 工作原理
- canal 模仿 MySQL slave 的交互协定,把本人伪装成一个 MySQL 的 slave 节点,向 MySQL master 发送 dump 协定;
- MySQL master 收到 dump 申请,开始推送 binary log 给 slave (即 canal);
- canal 解析 binary log 对象(原始为 byte 流),而后能够将解析后的数据间接发送给音讯组件。
RocketMQ 事务音讯解决方案
Apache RocketMQ 在 4.3.0 版中曾经反对分布式事务音讯,RocketMQ 采纳了 2PC 的思维来实现了提交事务音讯,同时减少一个弥补逻辑来解决二阶段超时或者失败的音讯,如下图所示。
RocketMQ 实现事务音讯次要分为两个阶段:失常事务的发送及提交、事务信息的弥补流程。
整体流程为:
- 失常事务发送与提交阶段
1、生产者发送一个半音讯给 MQServer(半音讯是指消费者临时不能生产的音讯)
2、服务端响应音讯写入后果,半音讯发送胜利
3、开始执行本地事务
4、依据本地事务的执行状态执行 Commit 或者 Rollback 操作 - 事务信息的弥补流程
1、如果 MQServer 长时间没收到本地事务的执行状态会向生产者发动一个确认回查的操作申请
2、生产者收到确认回查申请后,查看本地事务的执行状态
3、依据查看后的后果执行 Commit 或者 Rollback 操作
弥补阶段次要是用于解决生产者在发送 Commit 或者 Rollback 操作时产生超时或失败的状况。
在生产者应用 RocketMQ 发送事务音讯的时候咱们也会借鉴第一种计划即自建一张事务日志表,而后在执行本地事务的时候同时生成一条事务日志记录,让本地事务与日志事务在同一个办法中,同时增加 @Transactional
注解,保障两个操作事务是一个原子操作。这样如果事务日志表中有这个本地事务的信息,那就代表本地事务执行胜利,须要 Commit,相同如果没有对应的事务日志,则示意没执行胜利,须要 Rollback。