消息队列是什么
百度百科上关于“消息队列”的定义是这样的
“消息队列(Message Queue)”是在消息的传输过程中保存消息的容器。
消息队列(Message Queue,简称MQ),指保存消息的一个容器,本质是个队列。
不管是什么类型的消息队列,消息队列的本质都是:一发一存一消费
生产者先将消息投递一个叫做「队列」的容器中,然后再从这个容器中取出消息,最后再转发给消费者,仅此而已。
消息队列最原始的模型包含了两个关键词:消息和队列。
1、消息:就是要传输的数据,可以是最简单的文本字符串,也可以是自定义的复杂格式(只要能按预定格式解析出来即可)。
2、队列:大家应该再熟悉不过了,是一种先进先出数据结构。它是存放消息的容器,消息从队尾入队,从队头出队,入队即发消息的过程,出队即收消息的过程。
为什么使用消息队列
大家比较熟知的消息队列引用场景有:
- 系统解耦
- 异步通信
- 流量削峰
除此之外,还有延迟通知、最终一致性保证、顺序消息、流式处理等等。
使用消息队列还可以:
屏蔽异构平台的细节 发送方、接收方系统之间不需要了解双方,只需认识消息。
复用 一次发送多次消费。
可靠一次保证消息的传递。如果发送消息时接收者不可用,消息队列会保留消息,直到成功地传递它。
提供路由发送者无需与接收者建立连接,双方通过消息队列保证消息能够从发送者路由到接收者,甚至对于本来网络不易互通的两个服务,也可以提供消息路由。
举个实际的例子说明一下,用户参加一个优惠券活动,需要输入活动推广码,然后系统需要给用户发放优惠券和发送短信、邮件等通知用户参加活动成功。
解耦:使用消息队列后用户输入推广码后将事件发送到消息队列,之后直接记录下用户使用推广码的记录即可,发券和发送触达则有相应的系统去消息队列获取事件进行处理即可,减少了系统依赖,实现了系统解耦。
异步: 改造后相当于发券和发触达这些后续步骤全部变成了异步执行,能减少输入推广码登记的时间,提升了系统的吞吐量。
削峰:消息队列转储消息,可以作为“漏斗”进行限流保护。
使用消息队列需要注意的问题
-
高可用 项目中使用消息队列,都是得集群/分布式的。
-
数据丢失 消息队列中的数据需要存在别的地方,这样才尽可能减少数据的丢失
-
数据一致性 使用分布式事务,把所有关联操作放在一个事务里
-
重复消费 保证消息消费的幂等性
-
顺序消费
如何保证消息的顺序性?
RocketMQ的topic内的队列机制,可以保证存储满足FIFO(First Input First Output 先进先出),剩下的只需要消费者顺序消费即可。RocketMQ仅保证顺序发送,顺序消费由消费者业务保证。
Kafka 保证消息顺序性
写入一个 partition中的数据一定是有顺序的。
生产者在写的时候,可以指定一个 key,比如订单id作为key,那么订单相关的数据,一定会被分发到一个 partition中,此时这个 partition中的数据一定是有顺序的。Kafka 中一个 partition 只能被一个消费者消费。消费者从partition中取出数据的时候 ,一定是有顺序的。
消息可靠性怎么保证?
消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。
-
生产者发送消息 JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试,在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。
-
MQ丢失
RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了。可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。当我们选择同步刷盘之后,如果刷盘超时会给返回FLUSH_DISK_TIMEOUT。
Kafka也可以通过配置做到:acks=all 只有参与复制的所有节点全部收到消息,才返回生产者成功。除非所有的节点都挂了,消息才会丢失。
- 消费者丢失 RocketMQ默认是需要消费者回复
ack确认,而kafka需要手动开启配置关闭自动offset。消费方不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工来处理了。(Kafka没有这些)
保证 MQ 重复消费幂等性
所谓消息幂等就是当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响。
思路:
- 拿数据要写库,首先检查下主键,如果有数据,则不插入,进行一次update
- 如果是写 redis,就没问题,反正每次都是 set ,天然幂等性
- 生产者发送消息的时候带上一个全局唯一的id,消费者拿到消息后,先根据这个id去 redis里查一下,之前有没消费过,没有消费过就处理,并且写入这个 id 到 redis,如果消费过了,则不处理。
- 基于数据库的唯一键
常用的业务幂等性保证方法
利用数据库的唯一约束实现幂等:比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等
去重表:本质也是根据数据库的唯一性约束来实现。思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚
利用redis的原子性:每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中
多版本(乐观锁)控制:此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1
状态机机制:此方案多用于更新且业务场景存在多种状态流转的场景
token机制:生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。
消息队列如何选型
目前在市面上比较主流的消息队列中间件主要有,Kafka、ActiveMQ、RabbitMQ、RocketMQ 等这几种。
RocketMQ是阿里开源的,定位是非日志的可靠消息传输。例如:订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等。
Kafka是世界范围级别的消息队列标杆,定位是系统间的数据流管道,实时数据处理。例如:常规的消息系统、网站活性跟踪,监控数据,日志收集、处理等。
RocketMQ
RocketMQ实现原理
RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的:
- Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
- Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
- Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
Borker的Master和Slave之间是怎么同步数据的呢?
RocketMQ 底层用了 DLedger,用 Raft 同步日志,从原理上保证了不会脑裂。
通过broker主从机制实现了高可用。
- 在Master broker收到消息后,会被标记为uncommitted状态,然后会把消息发送给所有的slave
- slave在收到消息之后返回ack响应给master
- master在收到超过半数的ack之后,把消息标记为committed
- 发送committed消息给所有slave,slave也修改状态为committed
RocketMQ为什么速度快?
因为使用了顺序存储、Page Cache和异步刷盘。
- 我们在写入commitlog的时候是顺序写入的,这样比随机写入的性能就会提高很多
- 写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的PageCache
- 最后由操作系统异步将缓存中的数据刷到磁盘
RocketMQ延迟队列怎么实现?
RocketMQ延迟队列的核心思路是:
所有的延迟消息由producer发出之后,都会存放到同一个topic(SCHEDULE_TOPIC_XXXX)下,不同的延迟级别会对应不同的队列序号。
当延迟时间到之后,由定时线程读取转换为普通的消息存的真实指定的topic下,此时对于consumer端此消息才可见,从而被consumer消费。
注意:RocketMQ不支持任意时间的延时,只支持以下几个固定的延时等级
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h >2h";