赞
踩
MQ(Message Queue)消息队列,字面来看就是存放消息的队列(一种数据结构);
RabbitMQ中的一些角色:
publisher:生产者;
consumer:消费者;
exchange:交换机,负责消息路由;
queue:队列,存储消息;
virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息 ;
使用MQ可实现微服务之间的异步通讯;
降低服务之间的耦合;
提升性能和吞吐量;
服务之间没有强依赖,不用担心级联失败问题;
流量削峰;
架构复杂了,业务没有明显的流程线,不好管理(对程序员的技术要求高了);
需要依赖于Broker的可靠、安全、性能;
ActiveMQ
RabbitMQ
RocketMQ
Kafka
对比 | RabbitMQ | ActiveMQ | RocketMQ | Kafka |
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire, STOMP,REST, XMPP, AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
我们在Centos7虚拟机中使用Docker来安装;
(1)在线拉取(联网):
docker pull rabbitmq:3.8-management
(2)或者使用镜像包(找不到可以评论获取)
上传到虚拟机后使用加载镜像命令即可:
docker load -i rabbmitmq-3.8.tar
安装mq
拉取完或者上传完后来进行安装
- docker run \
- -e RABBITMQ_DEFAULT_USER=itcast \
- -e RABBITMQ_DEFAULT_PASS=123321 \
- -v mq-plugins:/plugins \
- --name mq \
- --hostname mq \
- -p 15672:15672 \
- -p 5672:5672 \
- -d \
- rabbitmq:3.8-management
首先,我们需要让3台MQ互相知道对方的存在。
分别在3台机器中,设置 /etc/hosts文件,添加如下内容:
192.168.150.101 mq1
192.168.150.102 mq2
192.168.150.103 mq3
基本消息队列的消息发送流程:
1. 建立connection
2. 创建channel
3. 利用channel声明队列
4. 利用channel向队列发送消息
5.关闭连接和channel
- package cn.itcast.mq.helloworld;
-
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.Connection;
- import com.rabbitmq.client.ConnectionFactory;
- import org.junit.Test;
-
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
-
- public class PublisherTest {
- @Test
- public void testSendMessage() throws IOException, TimeoutException {
- // 1.建立连接
- ConnectionFactory factory = new ConnectionFactory();
- // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
- factory.setHost("192.168.200.130");
- factory.setPort(5672);
- factory.setVirtualHost("/");
- factory.setUsername("itcast");
- factory.setPassword("123321");
- // 1.2.建立连接
- Connection connection = factory.newConnection();
-
- // 2.创建通道Channel
- Channel channel = connection.createChannel();
-
- // 3.创建队列
- String queueName = "simple.queue";
- channel.queueDeclare(queueName, false, false, false, null);
-
- // 4.发送消息
- String message = "hello, rabbitmq!";
- channel.basicPublish("", queueName, null, message.getBytes());
- System.out.println("发送消息成功:【" + message + "】");
-
- // 5.关闭通道和连接
- channel.close();
- connection.close();
-
- }
- }
基本消息队列的消息接收流程:
1. 建立connection
2. 创建channel
3. 利用channel声明队列
4. 定义consumer的消费行为handleDelivery()
5. 利用channel将消费者与队列绑定
- package cn.itcast.mq.helloworld;
-
- import com.rabbitmq.client.*;
-
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
-
- public class ConsumerTest {
-
- public static void main(String[] args) throws IOException, TimeoutException {
- // 1.建立连接
- ConnectionFactory factory = new ConnectionFactory();
- // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
- factory.setHost("192.168.200.130");
- factory.setPort(5672);
- factory.setVirtualHost("/");
- factory.setUsername("itcast");
- factory.setPassword("123321");
- // 1.2.建立连接
- Connection connection = factory.newConnection();
-
- // 2.创建通道Channel
- Channel channel = connection.createChannel();
-
- // 3.创建队列
- String queueName = "simple.queue";
- channel.queueDeclare(queueName, false, false, false, null);
-
- // 4.订阅消息
- channel.basicConsume(queueName, true, new DefaultConsumer(channel){
- @Override
- public void handleDelivery(String consumerTag, Envelope envelope,
- AMQP.BasicProperties properties, byte[] body) throws IOException {
- // 5.处理消息
- String message = new String(body);
- System.out.println("接收到消息:【" + message + "】");
- }
- });
- System.out.println("等待接收消息。。。。");
- }
- }
SpringAMQP是基于RabbitMQ封装的一套模板,并且利用SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAmqp的官方地址:https://spring.io/projects/spring-amqp
SpringAMQP提供了三个功能:
自动声明队列、交换机及其绑定关系;
封装了RabbitTemplate工具,用于发送消息;
基于注解的监听器模式,异步接收消息;
- <!--AMQP依赖,包含RabbitMQ-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-amqp</artifactId>
- </dependency>
首先配置MQ地址,在publisher服务的application.yml中添加配置:
- spring:
- rabbitmq:
- host: 192.168.200.130 # 主机名
- port: 5672 # 端口
- virtual-host: / # 虚拟主机
- username: itcast # 用户名
- password: 123321 # 密码
然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
- package cn.itcast.mq.spring;
-
- import org.junit.jupiter.api.Test;
- import org.springframework.amqp.rabbit.core.RabbitTemplate;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.test.context.junit4.SpringRunner;
-
- @SpringBootTest
- public class SpringAmqpTest {
-
- @Autowired
- private RabbitTemplate rabbitTemplate;
-
- @Test //不要导错包,用比较长的import org.junit.jupiter.api.Test;
- public void testSimpleQueue() {
- // 队列名称
- String queueName = "simple.queue";
- // 消息
- String message = "hello, spring amqp!";
- // 发送消息:此处并不会自动创建队列
- rabbitTemplate.convertAndSend(queueName, message);
- }
- }
#如果没有创建指队列:simple.queue,可在浏览器管理端中手动创建
然后在consumer服务中新建一个类SpringRabbitListener,代码如下:
- package cn.itcast.mq.listener;
-
- import org.springframework.amqp.rabbit.annotation.RabbitListener;
- import org.springframework.stereotype.Component;
-
- @Component
- public class SpringRabbitListener {
-
- @RabbitListener(queues = "simple.queue")
- public void listenSimpleQueueMessage(String msg) {
- System.out.println("spring 消费者接收到消息:【" + msg + "】");
- }
- }
3.2.1、消息发送
- /**
- * workQueue
- * 向队列中不停发送消息,模拟消息堆积。
- */
- @Test
- public void testWorkQueue() throws InterruptedException {
- // 队列名称
- String queueName = "simple.queue";
- // 消息
- String message = "hello, message_";
- for (int i = 1; i <= 50; i++) {
- // 发送消息
- rabbitTemplate.convertAndSend(queueName, message + i);
- Thread.sleep(20);
- }
- }
3.2.2、消息接收
1、注释掉之前接收消息的监听器
2、注意两个消费者的消费速度不一致,模拟消息分配方式(是否平分呢?)
- //@RabbitListener(queues = "simple.queue")
- //public void listenSimpleQueueMessage(String msg) {
- // System.out.println(msg);
- //}
-
- @RabbitListener(queues = "simple.queue")
- public void listenWorkQueue1(String msg) throws InterruptedException {
- System.out.println(LocalTime.now() + "消费者1:" + msg);
- Thread.sleep(20);
- }
-
- @RabbitListener(queues = "simple.queue")
- public void listenWorkQueue2(String msg) throws InterruptedException {
- System.err.println(LocalTime.now() + "消费者2:" + msg);
- Thread.sleep(200);
- }
看控制台
这是什么情况呢,这得让1能者多劳啊,这就得修改配置文件了:
- spring:
- rabbitmq:
- host: 192.168.200.130 # 主机名
- port: 5672 # 端口
- virtual-host: / # 虚拟主机
- username: itcast # 用户名
- password: 123321 # 密码
- listener:
- simple:
- prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给exchage(交换机)
Consumer:消费者,与以前一样,订阅队列,没有变化
Queue:消息队列也与以前一样,接收消息、缓存消息。
Exchange:交换机(消息路由)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange有以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符!
举例:
item.#:能够匹配item.spu.insert 或者 item.spu
item.*:只能匹配item.spu
Queue1:绑定的是
china.#
,因此凡是以china.
开头的routing key
都会被匹配到。包括china.news和china.weatherQueue2:绑定的是
#.news
,因此凡是以.news
结尾的routing key
都会被匹配。包括china.news和japan.news
在consumer服务的SpringRabbitListener中添加方法:(消息接收)
- package cn.itcast.mq.listener;
-
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.amqp.rabbit.annotation.Exchange;
- import org.springframework.amqp.rabbit.annotation.Queue;
- import org.springframework.amqp.rabbit.annotation.QueueBinding;
- import org.springframework.amqp.rabbit.annotation.RabbitListener;
- import org.springframework.stereotype.Component;
-
- @Slf4j
- @Component
- public class SpringRabbitListener {
-
- @RabbitListener(bindings = @QueueBinding(
- value = @Queue(name = "topic.queue1"),
- exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
- key = "china.#"))
- public void listenTopicQueue1(String msg){
- System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
- }
-
-
- @RabbitListener(bindings = @QueueBinding(
- value = @Queue(name = "topic.queue2"),
- exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
- key = "#.news"))
- public void listenTopicQueue2(String msg){
- System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
- }
- }
在publisher服务的SpringAmqpTest类中添加测试方法:(消息发送)
- /**
- * topicExchange
- */
- @Test
- public void testSendTopicExchange() {
- // 交换机名称
- String exchangeName = "itcast.topic";
- // 消息
- String message = "喜报!孙悟空大战哥斯拉,胜!";
- // 发送消息
- rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
- }
消息从发送,到消费者接收,会经历多个过程:
其中的每一步都可能导致消息丢失,常见的丢失原因包括:
发送时丢失:
生产者发送的消息未送达exchange;
消息到达exchange后未到达queue;
MQ宕机,queue将消息丢失;
consumer接收到消息后未消费就宕机;
主要从三个层面考虑
第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据
返回结果有两种方式:
publisher-confirm,发送者确认
消息成功投递到交换机,返回ack
消息未投递到交换机,返回nack
publisher-return,发送者回执
消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化
1、交换机持久化
(默认的是非持久化的、SpringAMQP中可以通过代码指定交换机持久化 )
@Bean public DirectExchange simpleExchange(){ // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange("simple.direct", true, false); }2、队列持久化
(默认的是非持久化的、SpringAMQP中可以通过代码指定交换机持久化
事实上,默认情况下,由SpringAMQP声明的队列都是持久化的)
@Bean public Queue simpleQueue(){ // 使用QueueBuilder构建队列,durable就是持久化的 return QueueBuilder.durable("simple.queue").build(); }3、消息持久化
(
利用SpringAMQP发送消息时,可以设置消息的属性(MessageProperties),指定delivery-mode:
1:非持久化:MessageDeliveryMode.NON_PERSISTENT
2:持久化:MessageDeliveryMode.PERSISTENT
)
第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack;
修改配置文件
spring: rabbitmq: listener: simple: acknowledge-mode: auto # 根据异常自动ack
第四消费失败重试机制,我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理
实现也是修改配置文件
spring: rabbitmq: listener: simple: retry: enabled: true # 开启消费者失败重试 initial-interval: 1000 # 初识的失败等待时长为1秒 multiplier: 2 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval max-attempts: 3 # 最大重试次数 stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
这种问题其实这个就是典型的幂等的问题,比如,redis分布式锁、数据库的锁都是可以解决
举个例子:
我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了
延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。
如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。
我记得RabbitMQ还有一种方式可以实现延迟队列,在RabbitMQ中安装一个死信插件,这样更方便一些,我们只需要在声明交互机的时候,指定这个就是死信交换机,然后在发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL要省略了一些步骤
解决消息堆积有三种思路:
第一:提高消费者的消费能力 ,可以使用多线程消费任务
第二:增加更多消费者,提高消费速度
使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息
第三:扩大队列容积,提高堆积上限
可以使用RabbitMQ惰性队列,
@RabbitListener(queuesToDeclare = @Queue( name = "lazy.queue", durable = "true", //开启惰性队列 arguments = @Argument(name = "x-queue-mode", value = "lazy") )) public void listLazyQueue(String msg) { log.info("接收到 lazy.queue 的消息:{}", msg); }惰性队列的好处主要是
①接收到消息后直接存入磁盘而非内存
②消费者要消费消息时才会从磁盘中读取并加载到内存
③支持数百万条的消息存储
一般工作里,都会搭建集群的
我们当时项目在生产环境下,使用的集群,当时搭建是镜像模式集群,使用了3台机器。
镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机后,镜像节点会替代成新的主节点,不过在主从同步完成前,主节点就已经宕机,可能出现数据丢失
那出现丢数据怎么解决呢:我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。