温馨提示: 本文阅读时长在20-30分钟左右
阅读本文章之前,希望大家先暂停几分钟,回想一下自己对mq的理解,想想自己了解哪些mq?mq的用途有哪些?等等问题,带着问题来阅读相信大家或多或少都会有写收获的。
一、关于MQ
MQ介绍
MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。
MQ的作用
消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
解耦:一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。
异步:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。
削峰:高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。
MQ的缺点
1、系统可用性降低。依赖服务越多,服务越容易挂掉。需要考虑MQ瘫痪的情况
2、系统复杂性提高。需要考虑消息丢失、消息重复消费、消息传递的顺序性
3、业务一致性。主业务和从属业务一致性的处理
消息中间件常用协议
AMQP
AMQP即(Advanced Message Queuing Protocol)高级消息队列协议,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。
优点:可靠、通用
MQTT
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统
STOMP
STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。
优点:命令模式(非topic\queue模式)
XMPP
XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。
优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大
消息中间件模式分类
点对点
PTP点对点:使用queue作为通信载体
说明:
消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。
消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。 Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
发布/订阅
Pub/Sub发布订阅(广播):使用topic作为通信载体
说明:
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
queue实现了负载均衡,将producer生产的消息发送到消息队列中,由多个消费者消费。但一个消息只能被一个消费者接受,当没有消费者可用时,这个消息会被保存直到有一个可用的消费者。
topic实现了发布和订阅,当你发布一个消息,所有订阅这个topic的服务都能得到这个消息,所以从1到N个订阅者都能得到一个消息的拷贝。
主流MQ
目前市面上主流MQ分为Kafka、RocketMQ、RabbitMQ和ActiveMQ,其中前面三个的市场份额最大。
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
主要协议 | OpenWire、STOMP、REST、XMPP、AMQP | AMQP协议 | 自己定义的一套协议 | 基于TCP自定义协议 |
单机吞吐量 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 10万级,RocketMQ也是可以支撑高吞吐的一种MQ | 10万级别,这是kafka最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
时效性 | ms级 | 微秒级,这是rabbitmq的一大特点,延迟是最低的 | ms级 | 延迟在ms级以内 |
可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 高,分布式架构 | 高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 高 | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置,消息可以做到0丢失 |
功能支持 | MQ领域的功能极其完备 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
优劣势总结 | 偶尔会有较低概率丢失消息 而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本 | erlang语言开发,性能极其好,延时很低;吞吐量到万级,MQ功能比较完备 而且开源提供的管理界面非常棒,用起来很好用 社区相对比较活跃,几乎每个月都发布几个版本分 在国内一些互联网公司近几年用rabbitmq也比较多一些 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障 而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码 | kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展 同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略 |
针对个人而言,MQ的选型:
Kafka配置起来相对麻烦些,并且要处理分区和zookeeper,然而一般我会用到读取日志,比如说用cancal组件通过kafka读取binlog日志,去实现数据同步。用kafka新建consumer订阅时,如果不需要历史数据,一定要设置offset,不然会把默认保存7天的历史消息推送给你。
RocketMQ用起来的话,配置确实很简单,只需配置topic和tag,针对吞吐量大的场景,可以选择该MQ,毕竟现在阿里系的技术方案都还算是靠谱。同时支持事务,但是事务处理的话,我建议还是用Seata这些方案来处理,回归MQ的本质作用。同时无管理界面,这点不太友好。
RabbitMQ用起来的话,配置也相对简单,确定好Exchange、Qunue之前的关系,中间用Routingkey作为关联绑定就能用起来,消息的可视化界面比较人性化。并发高、延迟低、其他综合起来比较平均。消费时需要显式的返回ack状态。
二、背景及目标
背景
1、现有系统的mq消息没有落地,消息丢失也不方便补偿
2、消息状态无法跟踪
3、针对延迟消息,虽然有RocketMQ支持,但是针对开源版本只能做到固定时间刻度的延迟,无法做到任意时间的延迟,同时也需要做二开(专业版购买可以支持任意时刻)
4、消息的节点配置没有做到统一管理,维护起来较麻烦
5、针对业务状态节点完成后执行的衍生任务也只能通过程序来编码实现,不方便解构,例如(注册完,给用户发券、发短信等关联场景)
目标
1、mq消息的链路监控和跟踪补偿
2、任意时间的延迟消费
3、多级关联任务的管理和处理
三、名词解释
死信队列
死信队列用于处理无法被正常消费的消息,即死信消息。
DLX,dead-letter-exchange
1、消息什么时候变为死信(dead-letter)
- 消息被否定接收,消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。
- 消息在队列里得时间超过了该消息设置的过期时间(TTL)。
- 消息队列到达了它的最大长度,之后再收到的消息。
2、死信队列的原理
当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的"x-dead-letter-exchange"参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。
延时插件
1、RabbitMQ Delayed Message Plugin是一个rabbitmq的插件
2、安装好插件后只需要声明一个类型type为"x-delayed-message"的exchange,并且在其可选参数下配置一个key为"x-delayed-type",值为交换机类型(topic/direct/fanout)的属性。
3、声明一个队列绑定到该交换机
4、在发送消息的时候消息的header里添加一个key为"x-delay",值为过期时间的属性,单位毫秒。
5、代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。
6、启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机
四、延迟消费技术方案(基于rabbitMQ)
1、基于RabbitMq的延迟消费,RabbitMq有2种方式来实现延迟消息,死信队列和延时插件,目前该方案中用的是延迟插件的方式,死信队列当初在测试的时候发现消息会变得无序,所以就选择了延迟插件的方式。
2、插件安装方式:https://blog.csdn.net/zhenghongcs/article/details/106700446
MQ改造业务图:
该方案分,业务触发、MQ状态机、消息监听和消息补偿
业务触发:就相当于producer,就是业务触发使用mq的地方
MQ状态机:状态机包含了4小块,消息接收模块、核心消息队列模块(1个核心即时消息队列、1个20天内延迟消息队列和超过20天的延迟队列)、数据库记录模块、以及最后的消费队列模块。
消息监听:真正意义上的消息消费模块,consumer
消息补偿:定时任务会去刷状态机模块中的表的数据,发现异常数据即时补偿刷到对应的队列中去。
作业流程
1、配置节点
将需要mq的地方配置节点,主要记录节点名称、消息类型、延迟时间、是否是关键节点、exchange,queue,routingkey等信息配置到表中
2、producer调用
业务方调用mq接收服务,判断是否是已记录的节点,如果满足,就记录入库,同时根据配置的信息放入消息队列中
3、消息队列
即时消息队列:存放即时消息
20天以内延迟队列:存放消息小于20天的消息
超过20天延迟队列:存放超过20天的延迟消息
注:为啥是20天为界,因为rabbitmq的delay时间参数是int型,并且是毫秒数,换算下来最大支持24天的消息,这里就取20天,留个buffer。
4、状态机内3个公共队列接收
分三个不同的Listener,分别监听不同的队列消息
如果是即时消息,那么就直接发送消息出去,并更新消息在库中状态
如果是20天内延迟消息队列消息接收,接收消息后发送出去,并更新消息在库中状态
如果是超过20天延迟消息,这个时候先判断消息的剩余延迟时间是否小于20天,如果还大于20天的话,那就再继续添加到超过20天的队列中,如果剩余的延迟时间小于20天,那么就放入小于20天的延迟队列中。如此往复。
附图
表设计
CREATE TABLE `t_node_config` (
`id` int(10) NOT NULL,
`node_name` varchar(60) DEFAULT NULL COMMENT '节点名称',
`node_code` varchar(60) DEFAULT NULL COMMENT '节点CODE',
`pid` int(10) DEFAULT NULL,
`delay_flag` tinyint(10) DEFAULT NULL COMMENT '消息类型 0-即时 1-延迟',
`delay_time` int(20) DEFAULT NULL COMMENT '延迟处理时间(S)',
`key_node_flag` varchar(255) DEFAULT NULL COMMENT '是否是关键节点 0-是 1-否',
`remark` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`delay_type` varchar(255) DEFAULT NULL COMMENT '延迟类型 1-小于20天 2-大于20天',
`delay_start_time` int(11) DEFAULT NULL COMMENT '延迟开始时间',
`delay_end_time` int(11) DEFAULT NULL COMMENT '延迟结束时间',
`delay_day` int(10) DEFAULT NULL COMMENT '延迟天,delay_type为2时使用',
`exchange_key` varchar(45) DEFAULT NULL COMMENT '交换机',
`qunue_key` varchar(45) DEFAULT NULL COMMENT '队列',
`routing_key` varchar(45) DEFAULT NULL COMMENT '关联key',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='节点配置';
CREATE TABLE `t_node_record` (
`id` int(10) NOT NULL,
`node_id` int(10) DEFAULT NULL,
`node_code` varchar(60) DEFAULT NULL,
`sequence` varchar(60) DEFAULT NULL COMMENT '消息唯一序列',
`status` tinyint(5) DEFAULT NULL COMMENT '状态 0-未消费 1-已消费',
`data` varchar(2000) DEFAULT NULL COMMENT '消息内容',
`delete_flag` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='消息记录表';
注:上面的例子是用的RabbitMQ做的案例,核心思想是可以在不同MQ产品之间套用。