一、基础实战
(一)MQ的作用:异步、解耦、流量削峰填谷
(二)MQ应用场景
传统的金融项目一般使用IBMMQ(收费),比如某丰银行项目。ActiveMQ已经成为历史,因为现在很少使用。RabbitMQ(小项目使用较多)、Kafka(多用于大数据领域,支持大数据量的高并发吞吐量)、RocketMQ(支持10w/s的QPS)现在使用较多。
(三)RabbitMQ的AMQP
1.交换机的类型
direct(直连交换机),fanout(广播交换机),topic(主题交换机),headers(和direct一样不实用可忽略)。
2.RabbitMQ消息可靠性分析
为了保证RabbitMQ消息不丢失,需要确保图片中①②③④四个环节都是ok的。
(1)发布者确认机制:确保消息生产者将消息成功发送到RabbitMQ服务器端
RabbitMQ支持发布确认(Publisher Confirm)机制,即消息生产者将消息发送到RabbitMQ服务器后,如果服务器端将消息成功存储到队列中则会返回一个确认消息给消息生产者,否则会返回一个否定消息Nack(Negative Acknowledgement)给消息生产者。
(2)备份队列:确保消息在交换机中不会丢失
RabbitMQ支持备份队列(Alternate Exchange)机制,即消息到达队列之前,先将消息发送到备份队列中。如果主队列无法接收消息,RabbitMQ会将消息发送到备份队列中。备份队列通常是一个交换机,可以在创建队列时通过x-dead-letter-exchange属性来指定备份队列。
(3)持久化:确保消息在队列中不会丢失
在发送消息时,可以设置消息属性delivery_mode为2,表示该消息需要被持久化,即将消息保存到磁盘中,即使RabbitMQ服务器宕机也能够保证消息不回丢失。可以在创建队列时将队列的durable持久化属性设置为True,表示该队列也需要被持久化,以便在RabbitMQ服务器宕机后能够重新创建队列和绑定。
(4)消费者确认机制:确保消费者成功消费消息
在RabbitMQ中,消费者通过basic.ack命令向RabbitMQ服务器确认已经消费了某条消息。如果消费者在处理消息时发生错误或者宕机,RabbitMQ服务器会重新将消息发送给其他消费者。在确认消息消费成功之前,RabbitMQ会将消息保存在内存(队列)中,只有收到消费者的确认消息后才会删除内存中的消息。
除了RabbitMQ本身会自动确认消息是否消费成功之外,还可以手动确认消息是否消费成功。
(四)Kafka:topic、partition(分区)
1.Kafka可靠性分析
Kafka 通过多种机制来确保消息不丢失,包括副本机制、ISR(In-Sync Replicas)机制、ACK 机制等。
(1)副本机制
Kafka 通过副本机制来确保消息不会丢失。在 Kafka 中,每个分区都可以配置多个副本,每个副本保存分区的完整副本,当一个副本宕机时,Kafka 会自动将副本切换到其他可用的副本上。因此,即使其中一个副本宕机,也能够保证消息不会丢失。
(2)ISR 机制
在 Kafka 中,副本分为 Leader 副本和 Follower 副本。Leader 副本负责处理消息,Follower 副本只是简单地复制 Leader 副本的数据。当 Follower 副本落后于 Leader 副本时,Kafka 会将 Follower 副本从 ISR 中移除。只有当 Follower 副本与 Leader 副本的差距不大时,才会将 Follower 副本重新加入 ISR,确保消息不会丢失。
(3)ACK 机制
在 Kafka 中,生产者发送消息时可以指定 acks 参数,表示生产者等待的确认数。acks 参数有三个取值:
acks=0 表示生产者不等待确认消息,直接将消息发送到 Kafka 集群。这种方式可能会导致消息丢失,不建议使用。
acks=1 表示生产者在 Leader 副本收到消息后,就将消息视为发送成功。如果 Leader 副本在发送消息后立即宕机,消息可能会丢失。如果 Follower 副本成功复制了消息,但 Leader 副本在宕机前没有来得及将消息写入磁盘,则这条消息将会丢失。
acks=all 表示生产者在所有 ISR 副本都确认接收到消息后,才将消息视为发送成功。这种方式可以最大程度地确保消息不会丢失,但是会降低消息发送的性能。
通过上述机制的使用,可以最大程度地确保 Kafka 中的消息不会丢失。需要根据实际场景选择合适的参数配置来平衡消息发送的性能和可靠性。
(五)RocketMQ:topic、queue(队列)
1.RocketMQ可靠性分析
RocketMQ 通过多种机制来确保消息不丢失,包括刷盘机制、消息拉取机制、ACK 机制等。
(1)刷盘机制
RocketMQ 中的消息分为内存消息和磁盘消息,内存消息在 Broker 内存中进行读写,磁盘消息则保存在磁盘上。RocketMQ 支持同步刷盘和异步刷盘两种方式,通过刷盘机制可以确保消息在 Broker 宕机时不会丢失。在同步刷盘模式下,消息写入磁盘时,会等待磁盘的写入完成才返回写入成功的响应。在异步刷盘模式下,消息写入磁盘后立即返回写入成功的响应,但是不等待磁盘写入完成。
(2)ACK 机制
在RocketMQ中,Producer发送消息后,Broker会返回ACK确认信号,表示消息已经成功发送。如果Broker没有收到ACK 确认信号,就会尝试重新发送该消息,直到消息被确认为止。
RocketMQ采用主从复制机制,每个消息队列都有一个主节点和多个从节点,主节点负责消息的写入和读取,从节点负责备份数据。当主节点宕机时,从节点会自动接管主节点的工作,确保消息不会丢失。
(3)消息存储机制
RocketMQ 默认使用双写模式来存储消息,即将消息同时写入内存和磁盘中,然后再将内存中的消息异步刷盘到磁盘中。这种方式可以保证消息的可靠性,即使系统宕机,也能够尽可能地保证消息不会丢失。
除此之外,RocketMQ 还提供了多种机制来保证消息不丢失,例如事务消息、延迟消息、顺序消息等,这些机制可以根据业务需求进行选择和使用。
需要注意的是,为了确保消息的可靠性,RocketMQ 的发送消息的速度可能会受到一定的限制,需要在消息可靠性和性能之间进行权衡。
2.RocketMQ生产架构
(六)如何防止消息重复消费:消费端幂等性处理
1.什么是幂等性?
针对消息消费端来说,幂等性是指针对相同的输入多次调用处理函数得到的结果不变,也就是不管生产者发送多少次消息给消息消费端,对业务结果不会产生不期望的结果。
例如SQL语句update stat_table set count= 10 where id =1不管执行多少次,count字段的值都是10,那么执行SQL的操作就是幂等性操作。
再看另外一个SQL的执行就不是幂等性操作:update stat_table set count= count +1 where id= 1,因为每次执行SQL的结果都不一样。
2.如何实现幂等性?
(1)MVCC
多版本并发控制,一种乐观锁的实现。在生产者发送消息进行数据更新时需要带上数据的版本号,消费者去消费消息时需要比对数据的版本号,版本号不一致的操作无法成功。例如博客点赞次数自动加1的接口:
public boolean addCount(Long id, Long version);
update blogTable set count= count+1,version=version+1 where id=321 and version=123
每个version只有一次执行成功的机会,如果执行失败必须重新获取数据的最新版本号再次发起更新操作。
(2)去重表
利用数据库的特性来实现幂等,常用的一个思路就是在表上建立唯一性索引,保证某一类数据一旦执行完成,后续同样的请求将不再重复处理(利用一张日志表来记录已经处理成功的消息的id,如果新到的消息的id已经包含在日志表中,那么新到的这条消息就不在处理了)。
以电商平台为例,电商平台订单的id就是最适合的token。当用户下单时,会经历过很多个环节,比如生成订单、减库存、减优惠卷等等。请求经历每一个环节时先检查该订单id是否已经执行过这一步骤,对未执行的请求,执行操作并缓存执行结果,对已经执行过的id,则直接返回之前的执行结果,不做任何操作。这样以来,可以最大程度上减少重复执行的问题,缓存起来的执行结果也能用于事务控制等等。
二、高级实战
(一)什么是延时消息,各种MQ的实现?
1.场景
(1)买电影票
购票->选座位(选完座位后系统会锁定座位)->支付(限定5分钟内完成支付,否则释放座位)。
处理方案:锁定座位后,推送一条只有5分钟有效期的延时消息。应用中消费者进行业务处理时5分钟内没有完成支付系统将释放座位给其他用户购买。
如果不实用MQ,而是定时扫描数据库来解决需要延时处理的业务场景,这种方案性能差(不能保证即时性)、不灵活(系统的业务中可能还有其他不同的需要延时处理的场景)。
(2)实现
①通过RabbitMQ的死信队列/消息实现
基于死信交换机和两个消息队列实现的。先将消息发送到过期队列中,如果过一段时间后消息仍然没有被消费,那么消息将会进入死信队列中。一旦监控到死信队列中有消息说明消息没有及时被消费,这个时候可以做进一步处理。
②通过RocketMQ延时消息来实现
RocketMQ发送消息的方法有很多重载方法,可以根据业务中需要的延时时间精确度来选择不同的发送延时消息的方法,因为这些多个发送延时消息的重载方法的区别就是它们可以通过不同参数来决定延时消息的时间单位和精度。
RocketMQ延时消息的原理:
生产者一旦发送了带有延时属性的消息,消息则不会立即进入目标主题,而是先进入一个临时主题(SCHEDULE_TOPIC_XXXX)。然后通过定时任务每秒运行一次去检查消息是否到达超时时间,如果消息到达超时时间会立即进入目标主题。
其中,SCHEDULE_TOPIC_XXXX主题有18个队列,每个队列对应一个延时消息等级,因为消息的延时等级有18个不同的时间。延时消息是放在队列中的,队列可以保证消息的顺序性,在定时任务遍历队列的时候只需要判断第一次消息是否过期,如果没有过期,那么其后面的消息肯定没有过期。
③Kafka自身没有延时消息的实现
(二)如何保证消息的顺序性?
最好生产者是单线程,如果是多线程并发生成消息就无法保证顺序性;
一个主题应该只有一个Queue;
消费者只能启动一个。
1.RabbitMQ消息存放在Queue队列中,因此自带顺序性
一条一条消费,一条消息消费完成后会从队列中删除。
(三)如何解决消息堆积问题?
RocketMQ和RabbitMQ可以通过后台管理界面监控消息是否出现堆积。
1.消费端
检查消费端代码,排查消息堆积是否是消息消费端的原因,如果是则需要代码解决。
2.突发流量
代码没有问题,数据库也没有锁的问题,这个时候需要排查是否是突发流量造成问题。假如有100w消息出现堆积,此时需要通过动态扩容机制来解决问题,具体来说是让多个消费者并发消费。
(四)RocketMQ的分布式事务
两阶段提交+消费者尽最大可能+失败补偿(利用消息队列的死信消息进行补偿)。
三、原理与源码
(一)Kafka的ISR机制:(In Sync Replica)动态复制方案
Kafka中存在副本机制,一个Broker可能有多个副本Broker,每个Broker里面有进行分区p0、p1、p2、p3…。Broker推荐使用手动创建。
Kafka中存在ACKS确认机制。
1.acks=0
只要发送确认就可以了,只要消息到Kafka就认为这个消息已经写入Kafka。优点:速度快。缺点:有可能会消息丢失。
2.acks=1
至少有一个Leader写入到分区数据文件(因为RocketMQ采用内存映射方式,所以不需要保证数据一定存入磁盘)
3.acks=all
等到所有的同步副本都收到消息,Kafka才会确认消息写入成功。
如果要确保消息不丢失。那么最好要等到所有副本中的消息同步到Follower节点。
(二)零拷贝原理以及MQ的运用
零拷贝技术并不是说不涉及数据拷贝,而是指减少数据传输过程中的不必要数据拷贝次数。Kafka、RocketMQ、Nginx中都用到了零拷贝技术,通过文件映射的方式将磁盘和内存进行映射,修改内存中值后磁盘中的值会自动更改。RocketMQ在启动的时候把对应的文件做了内存映射,可以减少一次CPU拷贝。
1.请求到达操作系统后其内部处理机制
2.传统网络传输(本地磁盘数据发送到远程服务器)
(1)流程执行:四次拷贝
数据从磁盘DMA拷贝到内核文件缓冲区->内核缓冲区的数据经过CPU拷贝到达用户空间内存缓冲区->数据在内存中修改后再经过一次CPU拷贝到达内核空间的Socket缓冲区->内核空间Socket缓冲区数据再经过一次DMA拷贝到达远程服务的网卡。
(2)代码示例
①Server端实现
②Client端实现(4次拷贝):传统io读写+网络传输
3.零拷贝读写客户端(模仿Kafka中2次拷贝的sendfile方式)
(1)零拷贝和传统网络传输方式效率对比
可以发现零拷贝方式发送相同字节数需要的时间会短很多,文件越大其对比效果越明显。
4.RocketMQ中零拷贝技术:通过Mmap文件映射方式的sendfile方法
(1)sendfile的零拷贝:发送文件描述符
Kafka中使用fileChannel.transferTo方式实现零拷贝,RocketMQ中使用文件映射方式具体来说是调用sendfile方法实现。transferTo方式内部本质上是封装了sendfile方式。