巩固基础,砥砺前行 。
只有不断重复,才能做到超越自己。
能坚持把简单的事情做到极致,也是不容易的。
面试题
项目上用过消息队列吗?用过哪些?当初选型基于什么考虑的呢?
面试官心理分析
第一,你知不知道你们系统里为什么要用消息队列这个东西?
不少候选人,说自己项目里用了Redis、MQ,但是其实他并不知道自己为什么要用这个东西。其实说白了,就是为了用而用,或者是别人
设计的架构,他从头到尾都没思考过。
没有对自己的架构问过为什么的人,一定是平时没有思考的人,面试官对这类候选人印象通常很不好。因为面试官担心你进了团队之后只会木头木脑的干呆活儿,不会自己思考。
第二,你既然用了消息队列这个东西,你知不知道用了有什么好处&坏处?
你要是没考虑过这个,那你盲目弄个MQ进系统里,后面出了问题你是不是就自己溜了给公司留坑?你要是没考虑过引入一个技术可能存
在的弊端和风险,面试官把这类候选人招进来了,基本可能就是挖坑型选手。就怕你干 1年挖一堆坑,自己跳槽了,给公司留下无穷后
第三,既然你用了MQ,可能是某一种MQ,那么你当时做没做过调研?
你别傻乎乎的自己拍脑袋看个人喜好就瞎用了一个MQ,比如Kafka,甚至都从没调研过业界流行的MQ到底有哪几种。每一个MQ的优点
和缺点是什么。每一个MQ没有绝对的好坏,但是就是看用在哪个场景可以扬长避短,利用其优势,规避其劣势。
如果是一个不考虑技术选型的候选人招进了团队,leader交给他一个任务,去设计个什么系统,他在里面用一些技术,可能都没考虑过选
型,最后选的技术可能并不一定合适,一样是留坑。
为什么使用消息队列
其实就是问问你消息队列都有哪些使用场景,然后你项目里具体是什么场景,说说你在这个场景里用消息队列是什么?
面试官问你这个问题,期望的一个回答是说,你们公司有个什么业务场景,这个业务场景有个什么技术挑战,如果不用MQ可能会很麻烦,但
是你现在用了MQ之后带给了你很多的好处。
先说一下消息队列常见的使用场景吧,其实场景有很多,但是比较核心的有3个:解耦、异步、削峰
消息队列有什么优缺点
缺点有以下几个:
系统可用性降低
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,ABCD 四个系统还好好的,没啥问题,你偏
加个MQ进来,万一MQ挂了咋整?MQ一挂,整套系统崩溃,你不就完了?如何保证消息队列的高可用,可以点击这里查看。
系统复杂度提高
硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头,问题一大堆,痛
苦不已。
一致性问题
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果C
系统写库失败了,咋整?你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好
之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的。
Kafka、ActiveMQ、RabbitMQ、RocketMQ对比
ActiveMQ:JMS规范,支持事务、支持XA协议,没有生产大规模支撑场景、官方维护越来越少
RabbitMQ:erlang语言开发、性能好、高并发,支持多种语言,社区、文档方面有优势,erlang语言不利于java
程序员二次开发,依赖开源社区的维护和升级,需要学习AMQP协议、学习成本相对较高
以上吞吐量单机都在万级
kafka:高性能,高可用,生产环境有大规模使用场景,单机容量有限(超过64个分区响应明显变长)、社区更新
吞吐量单机百万
rocketmq:java实现,方便二次开发、设计参考了kafka,高可用、高可靠,社区活跃度一般、支持语言较少
吞吐量单机十万
Kafka的PuIl和Push分别有什么优缺点
1.pul/表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自己控制,根据自己的消息处理能力来进
行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空
2.push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自己的能力来消费消息,推过来
多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者压力大等问题
Kafka、ActiveMQ、RabbitMQ、RocketMQ有什么优缺点?
综上,各种对比之后,有如下建议:
::一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,
所以大家还是算了吧,我个人不推荐用这个了。|
后来大家开始用RabbitMQ,但是确实erlang语言阻止了大量的Java工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是
确实人家是开源的,比较稳定的支持,活跃度也高。
不过现在确实越来越多的公司会去用RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前RocketMQ已捐给Apache,但GitHub上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用RocketMQ,否则回去老老实实用RabbitMQ吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ
是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用Kafka是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这
个领域的事实性规范。
部署是单机还是集群呢?你们高可用是怎么保证的呢?
如果有人问到你MQ的知识,高可用是必问的。上一讲提到,MQ会导致系统可用性降低。所以只要你用了MQ,接下来问的一些要点肯定就
是围绕着MQ的那些缺点怎么来解决了。
要是你傻乎乎的就干用了一个MQ,各种问题从来没考虑过,那你就杯具了,面试官对你的感觉就是,只会简单使用一些技术,没任何思考,马上对你的印象就不太好了。这样的同学招进来要是做个20k薪资以内的普通小弟还凑合,要是做薪资20k+的高工,那就惨了,让你设计个系统,里面肯定一堆坑,出了事故公司受损失,团队一起背锅。
这个问题这么问是很好的,因为不能问你Kafka的高可用性怎么保证?ActiveMQ的高可用性怎么保证?一个面试官要是这么问就显得很没水
平,人家可能用的就是RabbitMQ,没用过Kafka,你上来问人家Kafka干什么?这不是摆明了刁难人么。
所以有水平的面试官,问的是MQ的高可用性怎么保证?这样就是你用过哪个MQ,你就说说你对那个MQ的高可用性的理解。
RabbitMQ的高可用性
RabbitMQ是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以RabbitMQ为例子讲解第一种 MQ的高可用性怎么实
现。
RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式
单机模式,就是Demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式。
普通集群模式(无高可用性)
普通集群模式,意思就是在多台机器上启动多个RabbitMQ实例,每台机器启动一个。你创建的queue,只会放在一个RabbitMQ 实例上,但是每个实例都同步queue的元数据(元数据可以认为是queue的一些配置信息,通过元数据,可以找到queue所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。
镜像集群模式(高可用性)
这种模式,才是所谓的RabbitMQ的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的queue,无论是元数据还是queue 里的消息都会存在于多个实例上,就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue 的全部数据的意思。然后每次你写消息到queue的时候,都会自动把消息同步到多个实例的queue上。
那么如何开启这个镜像集群模式呢?其实很简单,RabbitMQ有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个queue的完整数据,别的consumer都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就没有扩展性可言了,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue。你想,如果这个queue的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?
有遇到过重复消费的问题吗?怎么解决的呢?
其实这是很常见的一个问题,这俩问题基本可以连起来问。既然是消费消息,那肯定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是 MQ领域的基本问题,其实本质上还是问你使用消息队列郊何保证幂等性,这个是你架构里要考虑的一个问题。
回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你先大概说一说可能会有哪些重复消费的问题。
首先,比如RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是MQ自己保证的,是由我们开
发来保证的。挑一个Kafka 来举个例子,说说怎么重复消费吧。
Kafka 实际上有个offset 的概念,就是每个消息写进去,都有一个offset,代表消息的序号,然后consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的offset来继续消费吧”。
但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。
这会导致consumer有些消息处理了,但是没来得及提交offset,尴尬了。重启之后,少数消息会再次消费一次。
举个栗子。
有这么个场景。数据 1/2/3依次进入Kafka,Kafka会给这三条数据每条分配一个offset,代表这条数据的序号,我们就假设分配的offset 依次是152/153/154。消费者从Kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 offset=153的这条数据,刚准备去提交offset 到Zookeeper,此时消费者进程被重启了。那么此时消费过的数据1/2的offset并没有提交,Kafka 也就不知道你已经消费了
offset=153这条数据。那么重启之后,消费者会找Kafka说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传
递过来。由于之前的offset 没有提交成功,那么数据1/2会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。
注意:新版的Kafka 已经将offset的存储从Zookeeper 转移至 Kafka brokers,并使用内部位移主题
如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据1/2在数据库里插入了2次,那么数据就错啦。
其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性。
举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?
但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。
一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。
幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
所以第二个问题来了,怎么保证消息队列消费的幂等性?
其实还是得结合业务来思考,我这里给几个思路:
。比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update一下好吧。
比如你是写Redis,那没问题了,反正每次都是set,天然幂等性。
比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单 id之类的东西,然后你这里消费到了之后,先根据这个id 去比如Redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。当然,如何保证MQ的消费是幂等性的,在实际应用中需要结合具体的业务来看。
消息队列有哪些作用
1.解耦:使用消息队列来作为两个系统直接的通讯方式,两个系统不需要相互依赖了
2.异步:系统A给消费队列发送完消息之后,就可以继续做其他事情了
3.流量削峰:如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,有消费者自己控制消费速度
简述RabbitMq的交换机类型
交换器分发会先找出绑定的队列,然后再判断routekey,来决定是否将消息分发到某一个队列中
Channel channel = connection.createchannel();
//在rabbitmq中创建一个信道
channe1.exchangeDeclare(“exchangeName”,“direct”);//创建一个type为direct的交换器
channe1.queueDeclare(“queueName”);
//创建一个队列channe1.queueBind(“queueName”,“exchangeName”,channe1.queueBind(“queueName”,“exchangeName”,
“zhangsna”);
“Tisi”);
//绑定并设置路由键//绑定并设置路由键channel.queueBind(“queueName”,“exchangeName”,“wangwu”);//绑定并设置路由键
fanout:扇形交换机,不再判断routekey,直接将消息分发到所有绑定的队列
direct:判断routekey的规则是完全匹配模式,即发送消息时指定的routekey要等于绑定的routekey
topic:判断routekey的规则是模糊匹配模式
header:绑定队列与交换器的时候指定一个键值对,当交换器在分发消息的时候会先解开消息体里的headers数据,然后判断里面是否有所设置的键值对,如果发现匹配成功,才将消息分发到队列中;这种交换器类型在性能上相对来说较差,在实际工作中很少会用到
简述RabbitMQ的架构设计
Broker:rabbitmq的服务节点
Queue:队列,是RabbitMQ的内部对象,用于存储消息。RabbitMQ中消息只能存储在队列中。生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(轮询)给多个消费者进行消费,而不是每个消费者都收到所有的消息进行消费。(注意:RabbitMQ不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由Key绑定多个队列,由多个消费者来订阅这些队列的方式。
Exchange:交换器。生产者将消息发送到Exchange,由交换器将消息路由到一个或多个队列中。如果路由不
到,或返回给生产者,或直接丢弃,或做其它处理。
RoutingKey:路由Key。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。这个路由Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时通过指定RoutingKey来决定消息流向哪里。
Binding:通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就可以指定
如何正确的路由到队列了。
交换器和队列实际上是多对多关系。就像关系数据库中的两张表。他们通过BindingKey做关联(多对多关系表)。在
投递消息时,可以通过Exchange和RoutingKey(对应BindingKey)就可以找到相对应的队列。
信道:信道是建立在Connection之上的虚拟连接。当应用程序与Rabbit Broker建立TCP连接的时候,客户端紧接著可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的D。RabbitNTQ处理的每条AMQP指令都是通过信道完成的。信道就像电缆里的光纤束。一条电缆内含有许多光纤束,允许所有的连接通过多条光线束进行传输和接收。
RabbitMQ如何确保消息发送?消息接收?
发送方确认机制:
信道需要设置为confirm 模式,则所有在信道上发布的消息都会分配一个唯一ID.
一旦消息被投递到queue(可持久化的消息需要写入磁盘),信道会发送一个确认给生产者(包含消息唯一ID)
如果 RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(未确认)消息给生产者。
所有被发送的消息都将被confirm(即 ack)或者被nack一次。但是没有对消息被confirm 的快慢做任何保证,并
且同一条消息不会既被confirm又被nack
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调
方法会被触发。
Confirmca11back接口:只确认是否正确到达Exchange 中,成功到达则回调
Returncallback接口:消息失败返回时回调
接收方确认机制:
消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存
(或者磁盘,持久化消息)中移去消息。否则,消息被消费后会被立即删除。
消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ
才能安全地把消息从队列中删除。
RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者
连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很长。保证数据的最终一致性;
如果消费者返回ack之前断开了链接,RabbitMQ会重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
rabbitmq的普通集群原理
元数据:
•队列元数据:队列名称和它的属性
交换器元数据:交换器名称、类型和属性
,绑定元数据:一张简单的表格展示了如何将消息路由到队列
vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性
为什么只同步元数据:
。存储空间,每一个节点都保存全量数据,影响消息堆积能力
性能,消息的发布者需要将消息复制到每一个集群节点
客户端连接的是非队列数据所在节点:则该节点会进行路由转发,包括发送和消费
集群节点类型:
。磁盘节点:将配置信息和元信息存储在磁盘上
内存节点:将配置信息和元信息存储在内存中。性能优于磁盘节点。依赖磁盘节点进行持久化
RabbitMQ要求集群中至少有一个磁盘节点,当节点加入和离开集群时,必须通知磁盘节点(如果集群中唯一的磁盘节点崩溃了,则不能进行创建队列、创建交换器、创建绑定、添加用户、更改权限、添加和删除集群节点)。如果唯一磁盘的磁盘节点崩溃,集群是可以保持运行的,但不能更改任何东西。因此建议在集群中设置两个磁盘节点,只要一个可以,就能正常操作。
死信队列是什么?延时队列是什么?
1.死信队列也是一个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试
2.延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单
有遇到过消息丢失吗?可靠性怎么保证呢?
这个是肯定的,用MQ有个基本原则,就是数据不能多一条,也不能少一条,不能多,就是前面说的重复消费和幂等性问题。不能少,就是说
这数据别搞丢了。那这个问题你必须得考虑一下。
如果说你这个是用MQ来传递非常核心的消息,比如说计费、扣费的一些消息,那必须确保这个MQ传递过程中绝对不会把计费消息给弄丢。
数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从RabbitMQ来分析一下吧。
生产者弄丢了数据
生产者将数据发送到RabbitMQ的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
此时可以选择用RabbitMQ提供的事务功能,就是生产者发送数据之前开启RabbitMQ事务 channel.txSelect(),然后发送消息,如果消
息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务
channel.txRollback(),然后重试发送消息;如果
收到了消息,那么可以提交事务 channel.txCommit()
try{
通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
//开启事务
channel.txSelect();//这里发送消息
channel.basicPublish(exchange,routingKey,MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes())
// 模拟出现异常
int result =1/0;
|/ 提交事务
channel.txCommit();
}catch (IOException | TimeoutException e)
//捕捉异常,回滚事务
channel.txRollback();
}
但是问题是,RabbitMQ事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能。
所以一般来说,如果你要确保说写RabbitMQ的消息别丢,可以开启confirm 模式,在生产者那里设置开启
confirm模式之后,你每次写
的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ会给你回传一个ack消息,告诉你说这个消息ok了。如果RabbitMQ没能处理这个消息,会回调你的一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
事务机制和confirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm 机制是异步的,你发送
个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步回调你的一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用 confirm机制的。
已经在transaction事务模式的channel是不能再设置成confirm 模式的,即这两种模式是不能共存的。
客户端实现生产者 confirm 有3种方式:
1.普通confirm 模式:每发送一条消息后,调用 waitForConfirms()方法,等待服务器端confirm,如果服务端返回false或者在一段时间
内都没返回,客户端可以进行消息重发。
channel.basicPublish(ConfirmConfig.exchangeName, ConfirmConfig.routingkey, MessageProperties. PERSISTENT_TEXT_PLAIN, Cor if (!channel.waitForConfirms()){
|/消息发送失败
}
2.批量confirm 模式:每发送一批消息后,调用 waitForConfirms()方法,等待服务端confirm。
channel.confirmSelect();
for (int i= 0;i< batchCount;++i) {
channel.basicPubLish(ConfirmConfig.exchangeName, ConfirmConfig.routingKey, MessageProperties. PERSISTENT_TEXT_PLAIN,
if (!channel.waitForConfirms())f
//消息发送失败
}
3.异步confirm模式:提供一个回调方法,服务端confirm了一条或者多条消息后客户端会回调这个方法。
SortedSet confirmset = Collections.synchronizedSortedtet(new TreeSet());
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener()f
public void handleAck(long deliveryTag, boolean multiple) throws IOException (
if(multiple){
confirmset.headSet(deliveryTag + 1).clear();
}else {
confirmset.remove(deliveryTag);
}
public void handleNack(long deliveryTag, boolean multiple) throws I0Exception System.out.println(“Nack, SeqNo:”+ deliveryTag + “, multiple:”+ multiple)if (multiple){
confirmSet.headSet(deliveryTag + 1).clear();
1 else {
confirmset.remove(deliveryTag);
}
while (true){
Long nextSeqNo = channel.getNextPubLishSeqNo();
channel.basicPublish(ConfirmConfig.exchangeName,ConfirmConfig.routingKey,MessageProperties.PERSISTENT_TEXT_PLAIN,
confirmSet.add(nextSeqNo);}
RabbitMQ弄丢了数据
就是RabbitMQ自己弄丢了数据,这个你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。
设置持久化有两个步骤:
创建queue的时候将其设置为持久化。这样就可以保证RabbitMQ持久化queue的元数据,但是它是不会持久化queue里的数据的。第二个是发送消息的时候将消息的 deliveryMode 设置为2。就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行,RabbitMQ哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里的数据。
注意,哪怕是你给RabbitMQ开启了持久化机制,也有一种可能,就是这个消息写到了RabbitMQ中,但是还没来得及持久化到磁盘上,结果
不巧,此时RabbitMQ挂了,就会导致内存里的一点点数据丢失。
所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化
到磁盘之前,RabbitMQ挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
消费端弄丢了数据
RabbitMQ如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。
这个时候得用RabbitMQ提供的ack 机制,简单来说 就是你必须关闭RabbitMQ的自动 aick
,可以通过一个api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack一把。这样的话,如果你还没处理完,不就没有ack了?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer 去处理,消息是不会丢的。
为了保证消息从队列中可靠地到达消费者,RabbitMQ提供了消息确认机制。消费者在声明队列时,可以指定noAck参数,当
noAck=false,RabbitMQ会等待消费者显式发回ack信号后,才从内存(和磁盘,如果是持久化消息)中移去消息。否则,一旦消息被消费
者消费,RabbitMQ会在队列中立即删除它。
大量消息在mq里积压了几个小时了还没解决
几千万条数据在MQ里积压了七八个小时,从下午4点多,积压到了晚上11点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复consumer的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。::一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟就是18万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概1小时的时间才能恢复过来。
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉。
·新建一个topic,partition是原来的10倍,临时建立好原先10倍的queue 数量。
然后写一个临时的分发数据的consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建
立好的10倍数量的queue。
接着临时征用10倍的机器来部署consumer,每一批consumer 消费一个临时queue的数据。这种做法相当于是临时将queue资源和
consumer 资源扩大10倍,以正常的10倍速度来消费数据。
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的consumer 机器来消费消息。
MQ中的消息过期失效了
假设你用的是RabbitMQ,RabbtiMQ是可以设置过期时间的,也就是TTL。如果消息在queue中积压超过一定的时间就会被RabbitMQ给清
理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。
这个情况下,就不是说要增加consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据给他补回来。也只能是这样了。
假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再
补一次。
MQ都快写满了
如果消息积压在mq里,你很长时间都没有处理掉,此时导致mq都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
对于RocketMQ,官方针对消息积压问题,提供了解决方案。
1.提高消费并行度
绝大部分消息消费行为都属于10密集型,即可能是操作数据库,或者调用RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量,通过增加消费并行度,可以提高总的清费吞吐量,但是并行度增加到一定程度,反而会下降。所以,应用必须要设置合理的并行度。如下有几种修改消费并行度的方法:
同一个ConsumerGroup下,通过增加Consumer 实例数量来提高并行度(需要注意的是超过订阅队列数的Consumer 实例无效)。可以通过
加机器,或者在已有机器启动多个进程的方式。提高单个Consumer的消费并行线程,通过修改参数consumeThreadMin.
consumeThreadMax实现。
2.批量方式消费
某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量,例如订单扣款类应用,一次处理一个订单耗时1s,一次处理 10个订单可能也只耗时2s,这样即可大幅度提高消费的吞吐量,通过设置consumer 的consumeMessageBatchMaxSize 返个参数,默认是1,即一次只消费一条消息,例如设置为N,那么每次消费的消息数小于等于N。
3.跳过非重要消息
发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息。例如,当某个队列的消息
数堆积到100000条以上,则尝试丢弃部分或全部消息,这样就可以快速追上发送消息的速度。示例代码如下:
public ConsumeConcurrentlyStatus consumeMessage(
Listmsgs
ConsumeConcurrentlyContext context)long offset = msgs.get(0).getQueue0ffset();String maxoffset =
msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET);Long diff = Long.parseLong(max0ffset)- offset;
if (diff >100000)
//TODO 消息堆积情况的特殊处理
return ConsumeConcurrentyStatus.CONSUME SUCCESS
// TODO正常消费过程
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
4.优化每条消息消费过程举例如下,某条消息的消费过程如下:
•根据消息从DB查询【数据1]
根据消息从DB查询【数据2】复杂的业务计算•向DB插入【数据 3]向DB插入【数据 4】
这条消息的消费过程中有 4次与 DB的交互,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时25ms,所以如果能把4次DB交互优化为2次,那么总耗时就可以优化到15ms,即总体性能提高了40%。所以应用如果对时延敏感的话,可以把DB部署在SSD硬盘,相比于SCSI磁盘,前者的RT会小很多。
如果让你写一个消息队列,该如何进行架构设计?说一下你的思路。
其实聊到这个问题,一般面试官要考察两块:
你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。
看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个Spring 框架你会怎么做?如果让你来设计一个Dubbc框架你会怎么做?如果让你来设计一个MyBatis 框架你会怎么做?
其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一
些开源的技术把一个系统设计出来的思路说一下就好。
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker->topic->partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?
顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是kafka的思路。
其次你考虑一下你的mq的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的kafka的高可用保障机制。多副本->leader&
follower->broker 挂了重新选举leader即可对外服务。
能不能支持数据0丢失啊?可以的,参考我们之前说的那个kafka数据零丢失方案。
mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题
可以刷掉一大批人,因为大部分人平时不思考这些东西。
RocketMQ的事务消息是如何实现的
a.生产者订单系统先发送一条[lf消息到Broker,half消息对消费者而言是不可见的
b.再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
c,并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
d.一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
e.如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理
为什么RocketMQ不使用Zookeeper作为注册中心呢?
根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能保证服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,
期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决
这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。
RocketMQ的底层实现原理
RocketMQ由NameServer集群、Producer集群、Consumer集群、Broker集群组成,消息生产和消费的大致原理如下:
1.Broker在启动的时候向所有的NameServer注册,并保持长连接,每30s发送一次心跳
2.Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
3.Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
消息队列如何保证消息可靠传输
消息可靠传输代表了两层意思,既不能多也不能少。
1.为了保证消息不多,也就是消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息
2.首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自己的原因,如果要避
免出现问题,就需要在消费端做控制
3.要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产
者重复发送消息的问题
4.消息不能少,意思就是消息不能丢失,生产者发送的消息,消费者一定要能消费到,对于这个问题,就要考虑两个方面
5.生产者发送消息时,要确认broker确实收到并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给broker
6.broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到一条消息后,如
果确认没问题了,就可以给broker发送一个ack,broker接收到ack后才会删除消息
kafka高性能高吞吐的原因
1、磁盘顺序读写:保证了消息的堆积
顺序读写,磁盘会预读,预读即在读取的起始地址连续读取多个页面,主要时间花费在了传输时间,而这个时
间两种读写可以认为是一样的。
随机读写,因为数据没有在一起,将预读浪费掉了。需要多次寻道和旋转延迟。而这个时间可能是传输时间的
许多倍。
2、零拷贝:避免CPU将数据从一块存储拷贝到另外一块存储的技术
传统的数据复制:
1、读取磁盘文件数据到内核缓冲区
2、将内核缓冲区的数据copy到用户缓冲区
2、将用户缓冲区的数据copy到socket的发送缓冲区
3、将socket发送缓冲区中的数据发送到网卡、进行传输
零拷贝:
磁盘文件->内核空间读取缓冲区->网卡接口->消费者进程
3、分区分段+索引
Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度
4、批量压缩:多条消息一起压缩,降低带宽
5、批量读写
6、直接操作page cache,而不是JVM、避免GC耗时及对象创建耗时,且读写速度更高,进程重启、缓存也不会丢失
Kafka在什么情况下会出现消息丢失及解决方案?
1)消息发送
1、ack=0,不重试
producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、ack=l, leader crash
producer发送消息完,只等待1ead写入成功就返回了,1eader crash了,这时fo11ower没来及同步,消息丢失。
3、unclean.leader.election.enable 配置true
允许选举ISR以外的副本作为1eader,公导致数据丢失,默认为fa1se。producer发送异步消息完,只等待1ead写入成功就返回了,leader crash了,这时ISR中没有fo1lower,leader从OSR中选举,因为OSR中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=a11 / -1,tries >1,unclean.leader.election.enable:false
producer发送消息完,等待fo11ower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。
不允许选举ISR以外的副本作为leader。
2、配置:min.insync.replicas>1
副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是
NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。
min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异常。
3、失败的offset单独记录
producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行单独处理。
2)消费
先commit再处理消息。如果在处理消息的时候异常了,但是offset 已经提交了,这条消息对于该消费者来说就是丢失
了,再也不会消费到了。
3)broker的刷盘
减小刷盘间隔
Kafka消息高可靠解决方案
消息发送:
•ack:0、不重试,1、lead写入成功就返回了,all/-1、等待ISR同步完再返回
unclean.leader.election.enable:false,禁止选举ISR以外的follower为leader
tries>1,重试次数
min.insync.replicas>1:同步副本数,没满足该值前、不提供读写服务、写操作会异常
消费:
手工提交offset
broker:减小刷盘间隔
事务消息
Kafka中zk的作用
/brokers/ids:临时节点,保存所有broker节点信息,存储broker的物理地址、版本信息、启动时间等,节点名称
为brokerID,broker定时发送心跳到zk,如果断开则该brokerID会被删除
/brokers/topics:临时节点,节点保存broker节点下所有的topic信息,每一个topic节点下包含一个固定的partitions节点,partitions的子节点就是topic的分区,每个分区下保存一个state节点、保存着当前leader分区和ISR的brokerlD,state节点由leader创建,若leader宕机该节点会被删除,直到有新的leader选举产生、重新生成state节点
/consumers/[group_id]/owners/[topic]/[broker_id-partitidn_id]:维护消费者和分区的注册关系
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]:分区消息的消费进度Offset
client通过topic找到topic树下的state节点、获取leader的brokerlD,到broker树中找到broker的物理地址,但是
client不会直连zk,而是通过配置的broker获取到zk中的信息
Kafka的性能好在什么地方
kafka不基于内存,而是硬盘存储,因此消息堆积能力更强
顺序写:利用磁盘的顺序访问速度可以接近内存,kafka的消息都是append操作,partition是有序的,节省了磁
盘的寻道时间,同时通过批量操作、节省写入次数,partition物理上分为多个segment存储,方便删除
传统:
。读取磁盘文件数据到内核缓冲区
。将内核缓冲区的数据copy到用户缓冲区
。将用户缓冲区的数据copy到socket的发送缓冲区·将socket发送缓冲区中的数据发送到网卡、进行传输零拷贝:
。直接将内核缓冲区的数据发送到网卡传输
使用的是操作系统的指令支持
kafka不太依赖jvm,主要理由操作系统的pageCache,如果生产消费速率相当,则直接用pageCache交换数据,
不需要经过磁盘10
Kafka是pull?push?优劣势分析
pul/模式:
·根据consumer的消费能力进行数据拉取,可以控制速率
·可以批量拉取、也可以单条拉取
。可以设置不同的提交方式,实现不同的传输语义
缺点:如果kafka没有数据,会导致consumer空循环,消耗资源
解决:通过参数设置,consumer拉取数据为空或者没有达到一定数量时进行阻塞
push模式:不会导致consumer循环等待
缺点:速率固定、忽略了consumer的消费能力,可能导致拒绝服务或者网络拥塞等情况
Kafka为什么比RocketMQ的吞吐量要高
Kafka的生产者采用的是异步发送消自机制,当发送一条消息时,消息并没有发送到Broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送给Broker。这种做法减少了网络io,从而提高了消息发送的吞吐量,但是如果消息生产者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了性能却降低了可靠性。
Kafka的PuII和Push分别有什么优缺点
1.pul表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自己控制,根据自己的消息处理能力来进
行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空
2.push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自己的能力来消费消息,推过来
多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者压力大等问题
简述kafka的rebalance机制
consumer group中的消费者与topic下的partion重新匹配的过程
何时会产生rebalance:
•consumer group中的成员个数发生变化
•consumer消费超时
·groupi阅的topic个数发生变化
groupi阅的topic的分区数发生变化
coordinator:通常是partition的leader节点所在的broker,负责监控group中consumer的存活,consumer维持
到coordinator的心跳,判断consumer的消费超时
•coordinator通过心跳返回通知consumer进行rebalance
•consumer请求coordinator加入组,coordinator选举产生leader consumer
leader consumerMcoordinator获取所有的consumer,发送syncGroup(分配信息)给到coordinator
coordinator通过心跳机制将syncGroup下发给consumer
,完成rebalance
consumer请求coordinator加入组,coordinator选举产生leader consumer
leader consumer从coordinator获取所有的consumer,发送syncGroup(分配信息)给到coordinator
coordinator通过心跳机制将syncGroup下发给consumer
。完成rebalance
leader consumer监控topic的变化,通知coordinator触发rebalance
如果C1消费消息超时,触发rebalance,重新分配后、该消息会被其他消费者消费,此时C1消费完成提交offset、
导致错误
解决:coordinator每次rebalance,会标记一个Generation给到consumer,每次rebalance该Generation会
+1,consumer提交offset时,coordinator会比对Generation,不一致则拒绝提交
kafka的副本同步机制
LEO:下一条待写入位置
firstUnstableOffset:第一条未提交数据
LastStableOffset:最后一条已提交数据
LogStartoffset:起始位置
isolation.level=read_committed:只能消费到LastStableOffset,read_committed可以消费到HW的上一条
一个partition对应的ISR中最小的LEO作为分区的HW,consumer最多只能消费到HW所在的位置
leader收消息后会更新本地的LEO,leader还会维护follower的LEO即remote LEO,follower发出fetch同步数据请求时(携带自身的LEO)、leader会更新remote LEO,更新分区的HW,然后将数据响应给follower、follower更新自身HW(取响应中的HW和自身的LEO中的较小值),LEO+1
ISR:如果一个follower落后leader不超过某个时间阈值,那么则则ISR中,否则将放在OSR中。
同步副本时,follower获取leader的LEO和LogStartOffset,与本地对比、如果本地的LogStartOffset超出了
leader的值,则超过这个值的数据删除,再进行同步,如果本地的小于leader的、则直接同步
kafka架构设计
Consumer Group:消费者组,消费者组内每个消费者负责消费不同分区的数据,提高消费能力。逻辑上的一个
订阅者。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个Topic。
Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个Broker上,每个Partition是一个有序的队列。一个Topic 的每个Partition都有若干个副本(Replica),一个Leader和若干个Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是Leader。Follower负责实时从Leader中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个Follower 还会成为新的Leader。
使用MQ如何保证分布式事务的最终一致性?
分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败.
最终一致性:与之对应的就是强一致性
MQ中要保护事务的最终一致性,就需要做到两点
1、生产者要保证100%的消息投递。 事务消息机制
2、消费者这一端需要保证幂等消费。 唯—ID+ 业务自己实现幂等
分布式MQ的三种语义:
at least once
at most once
exactly once
RocketMQ并不能保证exactly once。商业版本当中提供了exactly once的实现机制。
kafka:在最新版本的源码当中,提供了exactly once的demo。
RabbitMQ:erlang天生就成为了一种屏障。
如何保证消息消费的幂等性?
其实就是要方式消费者重复消费消息的问题。
所有MQ产品并没有提供主动解决幂等性的机制,需要由消费者自行控制
RocketMQ:给每个消息分配了个MessagelD。这个MessagelD就可以作为消费者判断幂等的依据。这种方式不太建议.
最好的方式就是自己带一个有业务标识的ID,来进行幂等判断。OrderlD
统—ID分配.
如何保证消息不丢失?
1、哪些环节会造成消息丢失?
2、怎么去防止消息丢失。
2.1生产者发送消息不丢失
kafka:消息发送+回调
RocketMQ:1、消息发送+回调。2、事务消息。
2.2MQ主从消息同步不丢失
2.3MQ消息存盘不丢失
RabbitMQ:1、消息发送+回调
2、手动事务:channel.txSelectO)开启事务,channel.txCommitO)提交事务,channel.txRollback()回滚事务。这种方
式对channel是会产生阻塞的,造成吞吐量下降。
3、Publisher Confirm。整个处理流程跟RocketMQ的事务消息,基本是一样的。
2.2 MQ主从消息同步不丢失
RocketMQ:1、普通集群中,同步同步、异步同步。异步同步效率更高,但是有丢消息的风险。同步同步就不会丢消息。
2、Dledger集群-两阶段提交:
RabbitMQ:普通集群:消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。
镜像集群:镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高。
Kafka:通常都是用在允许消息少量丢失的场景。
2.3 MQ消息存盘不丢失
RocketMQ:同步刷盘 异步刷盘:异步刷盘效率更高,但是有可能丢消息。同步刷盘消息安全性更高,但是效率会降低。
RabbitMQ:将队列配置成持久化队列。新增的Quorum类型的队列,会采用Raft协议来进行消息同步。
2.4 MQ消费者消费消息不丢失
RocketMQ:使用默认的方式消费就行,不要采用异步方式。
如何保证消息的高效读写?
零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写。
传统文件复制方式:需要对文件在内存中进行四次拷贝
Java当中对零拷贝进行了封装,Mmap方式通过MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操
作。
Mmap适合比较小的文件,通常文件大小不要超过1.5G~29之间。
Transfile没有文件大小限制。
RocketMQ当中使用Mmap方式来对他的文件进行读写。commitlog。1G
在kafka当中,他的index日志文件也是通过mmap的方式来读写的。在其他日志文件当中,并没有使用零拷贝的方式。
如何保证消息的顺序?
全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。
生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列当中的消息。
RocketMQ中有完整的设计,但是在RabbitMQ和Kafka当中,并没有完整的设计,需要自己进行设计。
RabbitMQ:要保证目标exchange只对应一个队列。并且一个队列只对应一个消费者。
Kafka:生产者通过定制partition分配规则,将消息分配到同一个partition。Topic下只对应一个消费者。
如何进行产品选型?
Kafka
优点:吞吐量非常大,性能非常好,集群高可用。
缺点:会丢数据,功能比较单一。
使用场景:日志分析、大数据采集
RabbitMQ
优点:消息可靠性高,功能全面。
缺点:吞吐量比较低,消息积累会严重影响性能。erlang语言不好定制。
使用场景:小规模场景。
RocketMQ
优点:高吞吐、高性能、高可用,功能非常全面。
缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持java。
使用场景:几乎是全场景。
MQ有什么用?有哪些具体的使用场景?
MQ:MessageQueue,消息队列。队列是一种FIFO先进先出的数据结构。消息由生产者发送到MQ进行排队,然后由消费
者对消息进行处理。QQ、微信 就是典型的MQ场景。
MQ的作用主要有三个方面:
1、异步:
例子:快递。 快递员->菜鸟驿站<- 客户
作用:异步能提高系统的响应速度和吞吐量。
2、解耦:
例子:《Thinking in java》->编辑社
作用:服务之间进行解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。
另外,解耦之后可以实现数据分发。生产者发送一个消息后,可以由多个消费者来处理。
3、削峰:
例子:长江涨水->三峡大坝
作用:以稳定的系统资源应对突发的流量冲击。
MQ的缺点:
1、系统可用性降低:一旦MQ宕机,整个业务就会产生影响。高可用
2、系统的复杂度提高:引入MQ之后,数据链路就会变得很复杂。如何保证消息不丢失?消息不会重复调用?怎么保证消息
的顺序性?、、、、、
3、数据一致性:A系统发消息,需要由B、C两个系统一同处理。如果B系统处理成功、C系统处理失败,这就会造成数据一
致性的问题。
rabbitmq的镜像队列原理
GM负责消息的广播,所有的GM组成gm_group,形成链表结构,负责监听相邻节点的状态,以及传递消息到相邻
节点,master的GM收到消息时代表消息同步完成
mirror_queue_master/slave负责消息的处理,操作blockingQueue,Queue负责AMQP协议(commit、
rollback、ack等)
master处理读写
rabbitmq的死信队列、延迟队列原理
死信消息:
1.消息被消费方否定确认,使用 channe1.basicNack或 channe1.basicReject,并且此时requeue 属性被
设置为false。
2.消息在队列的存活时间超过设置的TTL时间。
3.消息队列的消息数量已经超过最大队列长度。
那么该消息将成为死信消息。如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该
消息将会被丢弃
为每个需要使用死信的业务队列配置一个死信交换机,同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的routekey,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic]
TTL:一条消息或者该队列中的所有消息的最大存活时间
如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费
则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。
只需要消费者一直消费死信队列里的消息
agruments.put(“x-dead-letter-exchange”,“dlx.exchange”);
channel.queueDeclare(aueueName, true, false, false, agruments);
channel.queueBind(queueName, exchangeName,routingkey);
channe1.exchangeDeclare(“d1x.exchange”,“Eopic”, true, false, nu11);
channel.queueDeclare(“d1x.queue”,true, false, false, nu11);
channe1.queueBind(“d1x.queue”,“d1x.exchange”,“#”);
rabbitmq可以直连队列么?
生产者和消费者使用相同的参数声明队列。重复声明不会改变队列
//生产者
channe1.queueDeclare(QUEUE_NAME,false, false, false, nu11)
7/发送10条消息,依次在消息后面网四1-10个点
for (int i = 6; i >0; i–)
String message = “helloworld
channel.basicpublish(”",QUEUE_NAME,nu11
message.getBytes
//消费者
channe1.queueDeclare (QUEUE_NAME,false, false, false, nu11)Qdeueingconsumer consumer = new Queueingconsumer(channe1);// 指定消费队列
channel.basicConsume (QUEUE_NAME,true,consumer)
while (true)
QueueingConsumer.Delivery delivery = consumer.nextDelivery/
string message
=new string(delivery.getBody());dowork(message);
channe1.queueDeclare(queue, durable,exclusive, autoDelete, arguments)
queue:队列名字
durable:队列持久化标志,true为持久化队列
exclusive:exclusive:排他队列,仅对创建的链接可见、链接中的channe1都可见,其他链接不能重复声明,链接
关闭队列会被自动删除
autoDelete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。
arguments:Map类型,队列参数设置
x-message-ttl:数字,消息队列中消息的存活时间,超过会被删除
x-expires:数字,队列自身的空闲存活时间,指定时间内没有被访问,就会被删除x-max-1ength和x-max-1ength-bytes:队列最大长度和空间,超出会删除老的数据x-dead-1etter-exchange和lx-dead-letter-routing-key:设置死信
x-max-priority:队列支持的优先级别,需要生产者在发送消息时指定,消息按照优先级从高到底分发给消费者
channel.basicpublish(exchange, routingkey, mandatory, immediate, basicproperties, body);
exchange:交换机名
routingkey:路由键
mandatory:为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用
basic.return方法将消息返还给生产者,channe1.addReturnListener添加一个监听器,当broker执行
basic.return方法时,会回调handleReturn方法,这样就可以处理变为死信的消息了;设为fa1se时,出现上述情形
broker会直接将消息扔掉;
immediate:3.0以前这个标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有
queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。3.0之后取消了该参数
basicproperties:消息的详细属性,优先级别、持久化、到期时间等,headers类型的exchange要用到的是其中的
headers字段。
body:消息实体,字节数组。
Queueingconsumer:一个已经实现好了的Consumer,相比于自己实现Consumer接口,这是个比较安全快捷的方式。该
类基于jdk的BlockingQueue实现,hand7eDelivery方法中将收到的消息封装成De1ivery对象,并存放到
BlockingQueue中,这相当于消费者本地存放了一个消息缓存队列。nextDe1ivery()方法底层调用的
BlockingQueue的阻塞方法take()。
channe1.basicconsume (queue, autoAck,consumer);
queue:队列名。
autoAck:自动应答标志,true为自动应答。
consumer:消费者对象,可以自己实现Consumer接口,建议使用QueueingConsumer。
rabbitmq的持久化机制
1、交换机持久化:exchange_declare创建交互机时通过参数指定
2、队列持久化:queue_declare创建队列时通过参数指定
3、消息持久化:new AMQPMessage创建消息时通过参数指定
append的方式写文件,会根据大小自动生成新的文件,rabbitmq启动时会创建两个进程,一个负责持久化消息的
存储,另一个负责非持久化消息的存储(内存不够时)
消息存储时会在ets表中记录消息在文件中的映射以及相关信息(包括id、偏移量,有效数据,左边文件,右边文
件),消息读取时根据该信息到文件中读取、同时更新信息
消息删除时只从ets删除,变为垃圾数据,当垃圾数据超出比例(默认50%),并且文件数达到3个,触发垃圾回收,锁定左右两个文件,整理左边文件有效数据、将右边文件有效数据写入左边,更新文件信息,删除右边,完成合并。当一个文件的有用数据等于0时,删除该文件。
写入文件前先写buffer缓冲区,如果buffer已满,则写入文件(此时只是操作系统的页存)
每隔25ms刷一次磁盘,不管buffer满没满,都将buffer和页存中的数据落盘
每次消息写入后,如果没有后续写入请求,则直接刷盘
RabbitMQ如何保证消息的可靠性传输
1、使用事务消息
2、使用消息确认机制
发送方确认:|
■ channel设置为confirm模式,则每条消息会被分配一个唯—id
。消息投递成功,信道会发送ack给生产者,包含了id,回调ConfirmCallback接口
如果发生错误导致消息丢失,发生nack给生产者。回调ReturnCallback接口
。ack和nack只有一个触发,且只有一次,异步触发。可以继续发送消息
接收方确认:
。声明队列时,指定noack=false,broker会等待消费者手动返回ack、才会删除消息,否则立刻删除
broker的ack没有超时机制,只会判断链接是否断开,如果断开、消息会被重新发送
RabbitMQ如何确保消息发送? 消息接收?
发送方确认机制:
信道需要设置为confirm 模式,则所有在信道上发布的消息都会分配一个唯一ID。
一旦消息被投递到queue(可持久化的消息需要写入磁盘),信道会发送一个确认给生产者(包含消息唯一ID)。
如果 RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(未确认)消息给生产者。
所有被发送的消息都将被confirm(即 ack)或者被nack一次。但是没有对消息被confirm的快慢做任何保证,并
且同一条消息不会既被confirm又被nack
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调
方法会被触发。
Confirmcallback接口:只确认是否正确到达Exchange中,成功到达则回调
Returncallback接口:消息失败返回时回调
接收方确认机制:
消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存
(或者磁盘,持久化消息)中移去消息。否则,消息被消费后会被立即删除。
消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ
才能安全地把消息从队列中删除。
RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者
连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很长。保证数据的最终一致性;
如果消费者返回ack之前断开了链接,RabbitMQ会重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,
需要去重)
RabbitMQ事务消息
通过对信道的设置实现
1.channel.txSelectO;通知服务器开启事务模式;服务端会返回Tx.Select-Ok
2. channel.basicPublish;发送消息,可以是多条,可以是消费消息提交ack
3.channel.txCommitO提交事务;
4.channel.txRollback(回滚事务;
消费者使用事务:
1.autoAck=false,手动提交ack,以事务提交或回滚为准;
2.autoAck=true,不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消
息移除了
如果其中任意一个环节出现问题,就会抛出loException异常,用户可以拦截异常进行事务回滚,或决定要不要重
复消息。
事务消息会降低rabbitmq的性能
RabbitMQ死信队列、延时队列
1.消息被消费方否定确认,使用channe1.basicNack或channe1.basicReject,并且此时requeue 属性被
设置为false。
2.消息在队列的存活时间超过设置的TL时间,
3.消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。“死信"消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会
被丢进死信队列中,如果没有配置,则该消息将会被丢弃
为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】
TTL:一条消息或者该队列中的所有消息的最大存活时间
如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费.
则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。
只需要消费者一直消费死信队列里的消息
RocketMQ如何保证不丢消息
生产者:
”同步阻塞的方式发送消息,加上失败重试机制,可能broker存储失败,可以通过查询确认
·异步发送需要重写回调方法,检查发送结果
•ack机制,可能存储CommitLog,存储ConsumerQueue失败,此时对消费者不可见
broker:同步刷盘、集群模式下采用同步复制、会等待slave复制完成才会返回确认
消费者:
•offset手动提交,消息消费保证幂等
RocketMQ事务消息原理
依赖于TransactionListener接口
executeLocalTransaction方法会在发送消息后调用,用于执行本地事务,如果本地事务执行成功,rocketmq
再提交消息
checkLocalTransaction用于对本地事务做检查,rocketmq依赖此方法做补偿
通过两个内部的topic来实现对消息的两阶段支持,
prepare:将消息(消息上带有事务标识)投递到一个名为RMS_SYS_TRANS_HALF_TOPIC的topic中,而不是投递
到真正的topic中。
commit/rollback:producer再通过TransactionListener的executeLocalTransaction方法执行本地事务,当producer的localTransaction处理成功或者失败后,producer会向broker发送commit或rollback命令,如果是commit,则broker会将投递到RMQ_SYS_TRANS_HALF_TOPIC中的消息投递到真实的topic中,然后再投递一个表示删除的消息到RMQ_SYS_TRANS_OP_HALF_TOPIC中,表示当前事务已完成;如果是rollback,则没有投递到真实topic的过程,只需要投递表示删除的消息到RMQ_SYS_TRANS_OP_HALF_TOPIC。最后,消费者和消费普通
的消息一样消费事务消息
第一阶段(prepare)失败:给应用返回发送消息失败
事务失败:发送回滚命令给broker,由broker执行消息的回滚
Commit或rollback失败:由broker定时向producer发起事务检查,如果本地事务成功,则提交消息事务,否
则回滚消息事务
事务状态的检查有两种情况:
commit/rollback:broker会执行相应的commit/rollback操作
如果是TRANSACTION_NOT_TYPE,则一段时间后会再次检查,当检查的次数超过上限(默认15次)则丢弃
消息
简述RocketMQ持久化机制
commitLog:日志数据文件,被所有的queue共享,大小为1G,写满之后重新生成,顺序写
consumeQueue:逻辑queue,消息先到达commitLog、然后异步转发到consumeQueue,包含queue在CommitLog 中的物理位置偏移量Offset,消息实体内容的大小和Message Tag的hash 值。大小约为600w个字节,写满之后重新生成,顺序写
indexFile:通过 key 或者时间区间来查找CommitLog中的消息,文件名以创建的时间戳命名,固定的单个
IndexFile大小为400M,可以保存2000W个索引
所有队列共用一个日志数据文件,避免了kafka的分区数过多、日志文件过多导致磁盘10读写压力较大造成性能瓶颈,rocketmq的queue只存储少量数据、更加轻量化,对于磁盘的访问是串行化避免磁盘竞争,缺点在于:写入是顺序写,但读是随机的,先读ConsumeQueue,再读CommitLog,会降低消息读的效率
消息发送到broker后,会被写入commitLog,写之前加锁,保证顺序写入。然后转发到consumeQueue
消息消费时先从consu
RocketMQ 架构设计
RocketMQ怎么实现顺序消息
RocketMQ 顺序消息原理
默认是不能保证的,需要程序保证发送和消费的是同一个queue,多线程消费也无法保证
发送顺序:发送端自己业务逻辑保证先后,发往一个固定的queue,生产者可以在消息体上设置消息的顺序
发送者实现MessageQueueSelector接口,选择一个queue进行发送,也可使用rocketmq提供的默认实现
SelectMessageQueueByHash:按参数的hashcode与可选队列进行求余选择
SelectMessageQueueByRandom:随机选择
mq:queue本身就是顺序追加写,只需保证一个队列统一时间只有一个consumer消费,通过加锁实现,
consumer上的顺序消费有一个定时任务、每隔一定时间向broker发送请求延长锁定
消费端:
pul/模式:消费者需要自己维护需要拉取的queue,一次拉取的消息都是顺序的,需要消费端自己保证顺序消费
push模式:消费实例实现自MQPushConsumer接口,提供注册监听的方法消费消息,
registerMessageListener、重载方法
MessageListenerConcurrently:并行消费
MessageListenerOrderly:串行消费,consumer会把消息放入本地队列并加锁,定时任务保证锁的同步
如何保证消息不被重复消费
幂等:一个数据或者一个请求,重复来多次,确保对应的数据是不会改变的,不能出错。
思路:
·如果是写redis,就没问题,反正每次都是set,天然幂等性
•生产者发送消息的时候带上一个全局唯一的id,消费者拿到消息后,先根据这个id去redis里查一下,之前有没
消费过,没有消费过就处理,并且写入这个id到redis,如果消费过了,则不处理。
•基于数据库的唯一键
让你设计一个MQ,你会如何设计?
两个误区: 1、放飞自我,漫无边际。 2、纠结技术细节,
好的方式:1、从整体到细节,从业务场景到技术实现。2、以现有产品为基础。RocketMQ
答题思路:MQ作用、项目大概的样子,
实现一个单机的队列数据结构。 高效、可扩展。
2、将单机队列扩展成为分布式队列。-分布式集群管理
B、基于Topic定制消息路由策略。-发送者路由策略,消费者与队列对应关系,消费者路由策略
4、实现高效的网络通信。-Netty Http
5、规划日志文件,实现文件高效读写。-零拷贝,顺序写。 服务重启后,快速还原运行现场。
6、定制高级功能,死信队列、延迟队列、事务消息等等。-贴合实际,随意发挥。
消息队列的优缺点,使用场景
优点:
1、解耦,降低系统之间的依赖
2、异步处理,不需要同步等待
.3、削峰填谷,将流量从高峰期引到低谷期进行处理
缺点:
1、增加了系统的复杂度,幂等、重复消费、消息丢失等问题的带入
2、系统可用性降低,mq的故障会影响系统可用
3、一致性,消费端可能失败
场景:日志采集、发布订阅等
ActiveMQ
消息队列(第一节)
1.消息队列产品有好多种,kafka、rabbitMQ 、rocketMQ 、activeMQ 等
在学习这些产品时,都需要从以下几个方面来着手
1)常用的API 如何发送接收消息
2)如何实现MQ高可用
3)MQ的集群和容错机制
4)MQ的持久化
5)MQ如何延迟和定时发送消息,如何保证消息有序
6)MQ的签收机制
7)这些MQ如何和Spring、SpringBoot 整合
8)这些消息队列有什么不同,使用场景有那些差异?
9)他们是用哪些语音开发的?
kafka(java、scale)、rabbitMQ(erlang)、rocketMQ(java)、activeMQ(java)
2.电商业务中的秒杀模块的操作:
读取订单、库存检查、库存冻结、余额查询、余额冻结、订单生成、余额扣减、库存扣减、生成流水、余额解冻、库从解冻
3. activeMQ 的两个端口 61616 后台端口,8161 web页面端口
4.查看后台程序是否存活
ps -ef|grep activemq | grep -v activemq
netstart -anp|grep 61616
lsof -i:61616
5.linux关闭防火墙命令
1)查看防火墙状态:
service iptables status
systemctl status firewalld
2)暂时关闭防火墙
systemctl stop firewalld
service iptables stop
3)永久关闭防火墙
systemctl disable firewalld
service iptables off
4)重启防火墙
systemctl enable firewalld
service iptables restart
5)查看版本
forewalld -cmd -version
6.消息队列工作流程
1)创建连接工厂
2)连接工厂创建连接,得到连接
3)连接创建session
4)session创建消息生产者或者消息消费者
5)消息生产者组装消息,并发送
activeMQ 使用场景(第二节)
问题引入
1.在什么情况下使用消息中间件?
2.为什么要使用消息中间件?
解耦 系统之间接口耦合太高
异步 同步操作太费时间,例如 注册发送邮件 XXX
消峰 双十一 春运等高并发场景
activeMQ 官网地址
activeMQ Java简单实现(第三节)
两种通讯方式
点对点(队列) ;
订阅发布(主题)
pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!--activemq所需要的jar包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<!-- 下面是通用jar包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
队列生产者
package com.ttzz.activemq;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* 创建生产者
* @Description:
* @author: tangzhong
* @date: 2021年3月15日 下午6:31:41
*/
public class ActiveMQProduceByQueue {
public static String url = "tcp://localhost:61616";
public static String queueName = "myQueue";
public static void main(String[] args) throws JMSException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地 Queue
Queue queue =session.createQueue(queueName);
//5. 创建生产者
MessageProducer messageProducer = session.createProducer(queue);
//6. 发送消息
for (int i = 0; i < 4; i++) {
TextMessage textMessage = session.createTextMessage("queue……"+ i );
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("OOKK");
}
}
队列消费者
有两种接收方式:
同步阻塞
异步非阻塞
package com.ttzz.activemq;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* 创建消费者
* @Description:
* @author: tangzhong
* @date: 2021年3月15日 下午6:32:57
*/
public class ActiveMQConsumerByQueue {
public static String url = "tcp://localhost:61616";
public static String queueName = "myQueue";
public static void main(String[] args) throws JMSException, IOException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地 Queue
Queue queue =session.createQueue(queueName);
//5. 创建消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
//使用同步阻塞的方式
// while(true) {
// TextMessage textMessage = (TextMessage) messageConsumer.receive();
// if(textMessage!=null) {
// System.out.println("****消费者接收到消息:"+textMessage.getText());
// } else {
// break;
// }
// System.out.println(textMessage.getText());
// }
//使用异步非阻塞的方式 监听器
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message arg0) {
TextMessage textMessage = (TextMessage) arg0;
if(textMessage!=null) {
try {
System.out.println("****消费者接收到消息:"+textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
System.in.read(); //保证控制台不关
//关闭资源
messageConsumer.close();
session.close();
connection.close();
System.out.println("OOKK2");
}
}
主题生产者
package com.ttzz.activemq;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQProduceByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地 Topic
Topic queue =session.createTopic(topicName);
//5. 创建生产者
MessageProducer messageProducer = session.createProducer(queue);
//6. 发送消息
for (int i = 0; i < 4; i++) {
TextMessage textMessage = session.createTextMessage("myTopic……"+ i );
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("OOKK");
}
}
主题消费者
package com.ttzz.activemq;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQConsumerByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException, IOException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地Topic
Topic queue =session.createTopic(topicName);
//5. 创建消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
//使用同步阻塞的方式
// while(true) {
// TextMessage textMessage = (TextMessage) messageConsumer.receive();
// if(textMessage!=null) {
// System.out.println("****消费者接收到消息:"+textMessage.getText());
// } else {
// break;
// }
// System.out.println(textMessage.getText());
// }
//使用异步非阻塞的方式 监听器
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message arg0) {
TextMessage textMessage = (TextMessage) arg0;
if(textMessage!=null) {
try {
System.out.println("****消费者接收到消息:"+textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
System.in.read(); //保证控制台不关
//关闭资源
messageConsumer.close();
session.close();
connection.close();
System.out.println("OOKK2");
}
}
消费者的三种情况
/**
* 1. 先生成,只启动一个消费者,第1个消费者能消费吗 ? 能
* 2. 先生成,先启动一个消费者,再启动一个消费者,第2个消费者能消费吗 ? no
* 3. 先启动两个消费者,然后再启动生成着,第二个消费者可以消费吗 ?Y 采用轮询的方式进行消费
*/
topic 简介
前提
1.先启动消费者 然后再启动生成者,只有订阅了,才能接收到订阅的消息
生成者
package com.ttzz.activemq;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQProduceByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地 Topic
Topic topic =session.createTopic(topicName);
//5. 创建生产者
MessageProducer messageProducer = session.createProducer(topic);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start();
//6. 发送消息
for (int i = 0; i < 4; i++) {
TextMessage textMessage = session.createTextMessage("myTopic……"+ i );
textMessage.setStringProperty("自定义消息的key", "自定义消息的value");
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("OOKK");
}
}
消费者
package com.ttzz.activemq;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicSubscriber;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQConsumerByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException, IOException {
// 1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
// 2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.setClientID("消费者2");
System.out.println("消费者2");
// 3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4. 创建目的地Topic
Topic topic = session.createTopic(topicName);
// 5. 创建消费者
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark..."); // 创建持久化的订阅
connection.start();
Message message = topicSubscriber.receive();
while (message != null) {
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage.getText());
message = topicSubscriber.receive();
}
session.close();
connection.close();
System.out.println("OOKK2");
}
}
先启动两个消费者,然后再启动生产者
消费者控制台
消费者1
消费者2
MQ界面
JMS 规范以及消息特性
JMS规范是什么
它是JavaEE体系中的一项Message Service
常用消息中间件比较
JMS组成和特点
JMS provider
实现jms接口的消息中间件
JMS Producer 、JMS Constomer
JMS Message 消息头
1)jms destination 消息目的地 队列或者主题
2)jms deviverymode 持久化方式
3)jms expiration 消息过期时间
4)jms 优先级 1到4是普通消息 5-9是加急消息
5)消息id 唯一识别每个消息的标识,是有MQ自己生成
消息头之destination
当然,也可以通过消息进行设置
四种重载:目的地,消息,优先级,存活时间,是否持久化
消息的目的地:队列 和 主题
持久性
消息的过期时间 默认是永不过期的
消息体
发送的消息类型有哪些:
StringMessage MapMessage ByteMessage StringMessage ObjectMessage 五中类型
要求:发送的消息体和接受的消息体要求类型一致。
要求:发送的消息体和接受的消息体要求类型一致。
自定义的消息属性
自定义的消息属性能有什么还用呢 ?
去重、识别、重点标注等
TextMessage textMessage = session.createTextMessage("myTopic……"+ i );
messageProducer.send(textMessage);
textMessage.setStringProperty("自定义消息的key", "自定义消息的value");
如何保证消息的可靠性???
消息的可靠性可以从以下四个方面来回答:
1)消息的持久性
2)消息的事务特性
3)消息的签收机制
4)消息持久化
(队列)消息的持久性
验证1:
设置消息为非持久性,然后生产消息,(服务器不关闭),再去消费消息
消息被正常消费
验证2:
设置消息为非持久性,然后生产消息,(服务器关闭),再去消费消息
生成出的消息
服务器关闭之后,再去消费消息
消息丢失,不能被消费。
刚刚生成的消息被丢失了
验证3:设置消息为持久性,然后生成消息。这个是刚刚生成的消息。
关闭消息服务器,再次重新启动消息,消息依旧存在
去消费消息。消息成功被消费
(topic)消息的持久性
对于topic消息,持久性没有多大意义,因为在topic模式中,需要先启动消费者,然后再启动生产者,如果设置了消息持久性,但是,还没有启动动消费者,则这些消息就会被丢失,不能被消费者消费
设置持久化的topic生成者
package com.ttzz.activemq;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQProduceByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException {
//1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
//2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4. 创建目的地 Topic
Topic topic =session.createTopic(topicName);
//5. 创建生产者
MessageProducer messageProducer = session.createProducer(topic);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start();
//6. 发送消息
for (int i = 0; i < 4; i++) {
TextMessage textMessage = session.createTextMessage("myTopic……"+ i );
textMessage.setStringProperty("自定义消息的key", "自定义消息的value");
messageProducer.send(textMessage);
}
//关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("OOKK");
}
}
topic 消费者
package com.ttzz.activemq;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicSubscriber;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ActiveMQConsumerByTopic {
public static String url = "tcp://localhost:61616";
public static String topicName = "myTopic";
public static void main(String[] args) throws JMSException, IOException {
// 1.获取工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(url);
// 2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
connection.setClientID("消费者1");
System.out.println("topic消费者1");
// 3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4. 创建目的地Topic
Topic topic = session.createTopic(topicName);
// 5. 创建消费者
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark..."); // 创建持久化的订阅
connection.start();
Message message = topicSubscriber.receive();
while (message != null) {
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage.getText());
message = topicSubscriber.receive();
}
session.close();
connection.close();
System.out.println("OOKK2");
}
}
验证1:启动一个消费者
Active Durable Topic Subscribers :处于激活状态的持久化的topic消费者
Offline Durable Topic Subscribers:处于离线状态的持久化的topic消费者
启动持久化的topic生成者:
topic消费者消费消息
消息服务器
验证2:将消费者1 关闭,启动消费者2
消费者1处于离线状态,启动生成者消费
消费者2能够正常消费消息
再次启动消费者1,消费者1也能正常消费消息
消息的事务特性
事务主要是针对生产者,签收主要针对消费者
//3.创建会话
// 第一个参数 是否开启开启事务
// 第二个参数 是签收模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
验证1:
事务设置为 false,执行发送消息,消息自动到服务器
因为设置了消息的持久性,关闭服务器,再次重启启动,该消息依旧存在
验证2:
事务设置为 true,执行发送消息,
看看服务器有没有收到消息。服务器中没有收到刚刚发送的消息。因为没有做消息的提交操作
提交事务
消息入队
事务对于多个消息同时发送,能够保证原子性
session.rollback();
为了验证的需要,需要重启的时候,删除持久化的消息
操作:在配置文件activemq.xml的broker字段添加deleteAllMessagesOnStartup=“true”
可以看到持久化的消息被删除,
生成者开启事务,将消息发送到消息服务器
看到消息
消费者消费掉消息
再次启动一个消费者,发现消息已经被消费,说明消息不能被重新消费
验证2:设置消费者开启事务,但是,没有提交事务【消息被重复消费】。第一次,消费者1正常消费消息,
但是在服务器看到:消息没有被消费
再次启动另一个消费者,发现消息可以被多次消费
一个有趣的现象
生成者以事务的方式将生成者发送到服务器
消费者开启事务进行消费,但是,没有提交事务。保证控制到不灭。再次启动2号消费者,发现不能重复消费???
如果(去掉: System.in.read();)设置消费者4秒之后,没有消息,自动关闭。则启动2号消费者,可以重复消费。
原因呢???哈哈哈哈
消息的签收
非事务的签收有三种
1)自动签收 Session.AUTO_ACKNOWLEDGE
2)手动签收 Session.CLIENT_ACKNOWLEDGE
3)允许重复消息 Session.DUPS_OK_ACKNOWLEDGE ???这个我没有验证通过
对生产者而言,如果开启了事务,则签收机制可以随便选择,事务的优先级高于签收机制
验证1:生产者未开启事务,采用自动签收的方式将消息发送到服务器
消费者采用手动签收,发现消息可以重复消费。
消息发送到服务器,运行消费者程序,消息没有被消费掉
开启消息签收机制后,消息不能重复消费
事务模式下的签收
生产者开启事务,签收模式为自动签收,将消息发送到服务器
消费者开启事务,采用手动签收模式,但是消息没有使用ack机制。
消息仍然被消费掉
验证:
生产者开启事务,签收模式为自动签收,将消息发送到服务器;
消费者开启事务,采用手动签收模式,消息使用ack机制。但是没有commit。消息能被重复消费
结论:
在事务性会话中,当一个事物被成功提交则消息被自动签收。如果事物回滚,则消息会被再次传送。
非事物性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgement)
activeMQ两种模式比较
1)工作模式上来说,主题采用订阅发布模式,如果没有订阅者消息就会被丢弃;如果有多个订阅者,则
就会被多个订阅者接收;队列采用一对一的模式,如果当前消息没有消费者,则该
消息也不会丢弃,如果有多个消费者,那么该消息只能被一个消费者消费,同时要求
消费者发送ack确认信息
2)从有无状态上来看,主题是无状态的,队列会默认在服务器上以文件的形式保存,
也可以配置DB存储
3)从消息传递的完整性来看,主题如果没有订阅者,则消息会丢弃,而队列不会
4) 处理效率,主题会随着订阅者的增多效率减低,而队列不会
activemq传输协议
http://activemq.apache.org/configuring-version-5-transports.html
activemq 默认采用tcp协议
在网络传输之前,序列化数据为字节流 open wire
tcp协议的优点
可靠性高稳定性强;效率高,采用字节流的方式传递;高效性、可用性支持所有平台
nio协议:
auto+nio
// public static String url = "tcp://localhost:61616";
// public static String url = "nio://localhost:61618";
public static String url = "auto+nio://localhost:5671";