同步和异步通讯
微服务之间通讯有同步和异步俩种方式
- 同步通讯: 需要实时响应,例如 打电话
- 异步通讯: 不需要实时响应,如发邮件
同步通讯
Fegin 服务调用就属于同步方式,虽然调用可以实时获取到结果,但是存在下面的问题:
同步调用的优点:
- 时效性较强,可以立即获得结果
同步调用的缺点:
- 耦合度高
- 性能和吞吐能力下降
- 额外的资源消耗
- 级联失败的问题
异步通讯
异步调用则可以避免上述问题:
我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。
在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。
订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
异步通讯的优点:
- 吞吐量提升:无需等待订阅者处理完成,响应更快速
- 故障隔离:服务没有直接调用,不存在级联失败问题
- 调用间没有阻塞,不会造成无效的资源占用
- 耦合度极低,每个服务都可以灵活插拔,可替换
- 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
异步通讯的缺点:
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
RabbitMQ
基础知识
RabbitMQ 是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,他是使用 Erlang语言来编写的,并且是基于AMQP 协议的。
扩展知识点拓展知识点
AMQP 协议
AMQP(Advanced Message Queueing Protocol)定义:具有现代特征的二进制协议。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
AMQP 协议模型:
解释:
Publisher 推送消息前先与Server建立连接,找到Virtual host,然后将消息推送至Exchange交换机。而交换机与Message Queue有绑定关系(一个交换机相当于一个独立的虚拟机,而这个虚拟机内的各种独立的应用就相当于一个Queue,这个Queue与交换机绑定),Consumer通过绑定的对队列,而交换机也绑定了队列。发送者将消息发送给交换机,这样就能完成消息的推送了
消息队列:
总结
- 追求可用性:
KafakaRocketMQRabbitMQ - 追求可靠性:
RabbitMQRocketMQ - 追求吞吐能力:
RocketMQKafka - 追求消息低延迟:
RabbitMQKafka
基本架构
解析
- publisher:生产者
- consumer:消费者
- exchange个:交换机,负责消息路由
- queue:队列,存储消息
- virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离
五种消息模型
基本消息队列
涉及三种角色
publisher: 消息发布者,将消息发送到队列 queuequeue: 消息队列,负责接收并缓存消息consumer: 订阅队列,处理队列中的消息
工作消息队列
简单点说就是将多个消费者绑定到一个队列,共同消费队列中的消息。可以解决消息堆积的问题
多个消费者绑定到一个队列后,同一个消息只会被一个消费者处理
通过设置prefetch 参数可以控制消费者预取的消息数量
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
发布订阅三种模型
订阅模型:会多一个角色 exchange
publisher: 生产者,就是发送消息的程序,但不再是发送到队列中,而是发送到 交换机exchange: 交换机,一方面,接收生产者发送的消息,另一方面,处理消息,例如:递交到某个队列又或者将消息丢弃
-
fanout:广播,将消息递交到所有绑定 该交换机的队列direct: 定向,将消息递交给符合 路由键routing key的队列topic: 通配符,将消息递交给符合 路由模式routing pattern的队列
consumer: 消费者,订阅队列queue: 队列,接收并且缓存消息
注意:
交换机只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与交换机绑定,或者没有符合路由规则的队列,那么消息会丢失
广播模式
消息发送的流程:
- 可以有多个队列
- 每个队列都要绑定到交换价
- 生产者发送的消息,只能发送到交换机,交换机 负责决定 转发消息到哪个队列,生产者无法决定
- 交换机吧消息发送给绑定过的所有队列
- 订阅队列的消费者 都能拿到消息
交换机的作用:
- 接收 生产者 发送的消息
- 将消息按规则 路由 到与其绑定的队列
- 不能缓存消息,路由失败 则 消息 丢失
- 广播交换机,会将消息路由到每个绑定的队列中
定向模式
队列与交换机 通过一个路由键(routing key) 绑定, 消息的发送方在 向交换机 exchange 发送消息时,必须指定消息的 routing key, 交换机不再把消息交给每个绑定的队列,而是根据 消息的 routing key 判断,只有队列的 routing key 与消息的 routing key 完全一致,才会接收到消息。
通配符模式
routing key 一般都是一个或者多个单词组成,多个单词之间以 . 分割
通配符:
#: 匹配一个或者多个词*: 匹配一个词
解释:
- Queue1:绑定的是china.# ,因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weather
- Queue2:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news
传统模式痛点
- 痛点一: 复杂的业务系统,一次用户请求可能会同步调用 N 个系统的接口,需要等待所有的接口都返回了,才能真正的获取执行结果。这种同步接口调用的方式总耗时是比较长的。非常影响用户的体验。特别是在网络不稳定的情况下,极其融通器出现接口超时的问题。
- 痛点二: 系统之间的耦合性太高,如果调用的任何一个子系统出现异常,整个请求都会异常,大大降低了系统的稳定性。
- 痛点三: 当用户突增的时候,一时间所有的请求到到数据库,可能会导致数据库无法承受巨大的压力,导致响应变慢甚至直接挂掉。
MQ 的优点
- 异步: 同步接口调用导致响应时间长的问题,使用消息队列之后,将同步调用改成异步,可以显著提高系统的响应速度
- 解耦: 子系统之间的耦合性太大的问题,使用消息队列之后,我盟只需要依赖 消息队列,避免各个系统将的强耦合问题
- 消峰: 出现请求峰值的时候,导致系统不稳定的问题,使用消息队列后,能够起到消峰的作用
MQ 与 多线程实现异步的区别
- CPU 的消耗: 多线程异步存在 CPU 的竞争,可能影响业务的执行,而且 MQ 不消耗 CPU
- 可靠性: MQ 可以保证可靠性,消息会持久化到硬盘中,二多线程不能
- 消峰 或者 消息堆积的能力: 当业务系统处于高并发的时候,MQ 可以将消息堆积到
Broker实例中,儿多线程会创建大量线程,导致服务器处于高负荷状态,可能出发拒绝策略。
MQ 如何避免消息堆积的问题
产生的原因
生产者投递消息的速率 > 消费者消费的速率
解决方法
- 提高消费者消费的速率 例如:采用消费者集群
- 消费者采用批量获取消息形式,减少网络传输的次数
MQ 如何避免消息丢失
confirm机制的原理
rabbitmq 通过confirm机制 来通知消息是否持久化成功
- 消息生产者将消息发送给 MQ,如果接收成功,MQ 会返回一个 ACK消息消息给生产者
- 如果消息接收不成功,MQ 会返回一个
nack消息给生产者
思考
如果生产者每发一条消息,都要 MQ 持久化到磁盘中,然后再发起 ack 或者 nack 的回调,这样的话,是不是 MQ 的吞吐量就会受很大的限制,因为持久化的这个动作是很慢的。
所以,MQ 持久化到磁盘的实现,是同步异步调用处理的。例如:等到有几千条消息的时候,会一次性的刷盘到磁盘上面。而不是每一条保存一次。
那么,confirm机制其实就是一个异步监听的机制,是为了保证系统的高吞吐量。但是这样就导致了还是不能够 100% 保证消息不丢失,因为及时加上了 confirm机制,消息在 MQ 内存中还没有刷盘到磁盘就宕机了,这些内存中的消息就会丢失掉。
数据库事务机制
生产者投递消息之前,可以先在本地数据库建一张消息表,先把消息持久化到 数据库中,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息队列中。
confirm机制 去监听消息是否发送成功? 如果成功则,删除数据库中此条消息,如果不成功,则重发或者延时发送。。。。。 根据具体业务走。
小小知识点:
ACK 消息
确认消息也称为ACK消息,是在计算机网上中通信协议的一部分,是设备或是进程发出的消息,回复已收到数据。
例如在传输控制协议(TCP,Transmission Control Protocol)中就有用ACK来告知创建链接时有收到SYN数据包、使用链接时有收到数据包,或是在中止链接有收到FIN数据包。
在ARQ(自动重传请求)协议中也有用到确认消息,确认帧会配合收到的帧进行编号,然后送回发送端,发送端可以知道是否有遗漏的数据包。
ACK字符是一些通信协议下用来做确认消息的字符,也有通信协议使用其他字符。
也有些通信协议将ACK集成在其字段中,例如控制局域网(CAN)中就有应答(ACK)比特,设备收到数据后需在此字段回应,若没有回应,即为通信错误。
NAK 消息 或者 NACK 消息
否定应答(称为NAK;或称为NACK;或称为Negative-Acknowledgment)翻译为否定应答或者非应答。这种协议消息在数字通信中被使用。其作用是作为一种确认数据收到的应答,但表明有小错误存在的一种消息信号。
许多通信协议时基于ACK (Acknowledgement,确认)为基础的。这意味着这些通信协议正确地收到消息,传输控制协议(TCP,Transmission Control Protocol)是一个基于ACK协议的例子。
其它基于NAK的意味着他们只对那些有出错或有问题的信号做出反应。可靠多播协议就是一个例子。当接收器侦测出有丢失的数据包时,会发出一个NAK。
在多点系统中,若在轮询时,设备尚未就序,也会用NAK来回应。
最后,还有其它一些协议同时利用NAK和ACK的。双同步(Bisync)和用在节能以太网路的自适应链路速率(Adaptive Link Rate)就是这个应用的例子。
像NAK字元就是一个用来传递否定应答的控制字符。 [1]
MQ 保证消息顺序一致性问题
顺序性的意义:
消息队列中的若干消息如果是对同一个数据进行操作,这些操作具有前后的关系,必须要按前后的顺序执行,否则就会造成数据异常。
例如:
比如通过mysql binlog进行两个数据库的数据同步,由于对数据库的数据操作是具有顺序性的,如果操作顺序搞反,就会造成不可估量的错误。比如数据库对一条数据依次进行了 插入->更新->删除操作,这个顺序必须是这样,如果在同步过程中,消息的顺序变成了删除->插入->更新,那么原本应该被删除的数据,就没有被删除,造成数据的不一致问题。
RabbitMQ 保证消息顺序性
所有消息需要投递到同一个 MQ 服务器,同一个分区模型 queue 中存放,最终被同一个消费者消费。
核心原理:设定相同的消息key,根据相同的消息key 计算hash 存放在同一个分区
MQ 消息幂等性
幂等性:就是在相同条件下对一个业务的操作,不管操作多少次,结果都保持一致
那么什么情况下会 需要 出现消息的重复消费呢?
消息重复消费
消息非重复消费可能出现在生产者,也可能出现在 MQ 或者 消费者
这里说的重复消费问题是指 同一个数据被执行了俩次,不单单指 MQ 中一条消息被消费了俩次,也可能是 MQ 中存在俩条一模一样的消费。
- 生产者: 生产者可能会重复推送一条数据到 MQ 中,为什么会出现这种情况呢?也许是一个接口被重复调用了多次,也可能是生产者的重试机制导致重复推送了一次消息
- MQ: 消费者消费完一条数据响应
ack 信号时,MQ 突然宕机了。导致 MQ 以为消费者还未消费该消息,MQ 恢复后再次推送了这条消息,导致了重复消费 - 消费者: 消费者已经消费完了一条信息,正准备但是还未给 MQ 发送
ACK 信号时,此时消费者宕机了,那么当服务重启后,MQ 以为消费者还没有消费该消息,再次推送了该条消息。
幂等性如何保证
那么如何解决消息重复消费的问题呢?
状态判断法
消费者消费数据后吧消费数据记录到 Redis中,下次消费时先到 Redis 中查看是否存在该消息,存在则代表消息已经消费过了。直接丢弃消息。
业务判断法
通常数据消费后都需要插入到数据库中,使用数据库的唯一性约束防止重复消费。每次消费直接尝试插入数据,如果提示违反了唯一性约束,则直接丢弃消息。一般都是通过这个业务判断的方法就可以高效的避免消息的重复消费。