控制台介绍
登录后页面如上图。overview页面时总览信息,connnections为连接信息,channels为通道信息,连接后建立通道。exchanges为交换机信息,queues为队列信息。admin为用户管理信息(操作用户的增删改)
消息发送
消息有生产者和发送者。最终的目的是生产者生产消息,由消费者消费。举个例子来讲,老师给同学布置作业。老师就是消息的生产者,同学就是消息的消费者。作业本身就是消息。
那么布置作业的形式久会有多种。
- 老师单独给某一个同学布置作业
- 老师给所有同学布置作业
- 老师给男同学布置作业
- 等等
在控制台实现消息发送
在控制台中,只有交换机和消息队列。消息的发送由交换机完成,消息的接收由队列完成。
在控制台中新建一个交换机
可以看到交换机的类型有4种,分别是fanout,headers,direct,topic。
- fanout:广播,将消息交给所有绑定到交换机的队列
新建一个fanout类型的交换机,点击进入交换机。在里面绑定已经建好的队列
将队列与交换机绑定后,通过交换机上的publish message发送消息,则可以在绑定的queue中获取消息数据。
- headers:header模式暂时没有场景
- direct:定向,把消息交给符合指定routing key 的队列
在交换机绑定队列的时候,指定routing key
消息发送的时候,设置routing key。如果设置了routing key为m,则只有路由key对应的队列才会收到消息。
- topic:通配符,相对于direct模式,可以把routing key设置为通配形式。
这个exchange绑定了两个queue,使用的routingKey分别是test.#和test.select。发送消息的时候,使用的routingKey是test.add。上面#代表多个,*代表一个。test.add满足了test.#的规范。所以消息路由到了queue_sms这个队列
springboot集成
增加maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
增加配置文件
spring:
rabbitmq:
host: 35.10.228.124
port: 5672
username: rabbitmq
password: 123456
消息发送
在控制台中建好了exchange(交换机)和queue(队列)后,可以直接使用代码进行消息发送。也可以通过代码建交换机和队列
@SpringBootTest(classes = MyApplication.class)
@RunWith(SpringRunner.class)
public class Send {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void sendMsgByTopics() {
rabbitTemplate.convertAndSend("exchange.sms", "routing.sms", "这是一条消息");
System.out.println("send over");
}
}
消息接收
@Component
public class RabbitMqReceiveHandler {
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "queue_sms"),
exchange = @Exchange(value = "exchange.sms", type = ExchangeTypes.TOPIC)))
public void receiveSms(String msg) {
System.out.println("收到了消息:" + msg);
}
}
使用@RabbitListener注解,接收消息。里面表名了交换机和队列。满足此绑定的消息即可被接收处理。消费者程序启动后,可以在控制台的connections中看到对应的连接内容
在通道(channels)中也可以看到所建立的通道
消息接收后,处理消息过程中,如果碰到异常情况,可能会有如下几种处理方式
- 告知服务器消息已经收到了,出错了,我不想再处理这个消息了。
- 告知服务器消息已经收到了,出错了,我想要再次处理这个消息。
上面的处理方式中,使用了rabbitmq的自动确认机制。自动确认, 这也是默认的消息确认情况。 AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
如果需要进行再次消费,则需要进行手动确认,下面进行手动确认的代码编写
手动确认
在消费者端建立一个MyReAckReciver类
package com.example.demo.mq;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
String msg = new String(message.getBody());
System.out.println("消费的主题消息来自:" + message.getMessageProperties().getConsumerQueue());
Integer integer = consumeMsg(msg);
System.out.println(deliveryTag);
if (integer > 5) {
channel.basicAck(deliveryTag, true); //第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
} else {
channel.basicReject(deliveryTag, true);//第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
}
} catch (Exception e) {
channel.basicReject(deliveryTag, false);
e.printStackTrace();
}
}
public Integer consumeMsg(String msg) {
System.out.println(msg);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i = new Random().nextInt(10);
if (i == 5) {
throw new RuntimeException("5 Exception");
}
return i;
}
}
如何使用刚才建立的消息消费者。需要建立一个SimpleMessageListenerContainer类,这个类通过配置文件建立
package com.example.demo.mq;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageListenerConfig {
@Autowired
private CachingConnectionFactory connectionFactory;
@Autowired
private MyAckReceiver myAckReceiver;//消息接收处理类
@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setConcurrentConsumers(1);
container.setMaxConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息
//设置一个队列
container.setQueueNames("TestDirectQueue","queue_sms");
//如果同时设置多个如下: 前提是队列都是必须已经创建存在的
// container.setQueueNames("TestDirectQueue","TestDirectQueue2","TestDirectQueue3");
//另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues
//container.setQueues(new Queue("TestDirectQueue",true));
//container.addQueues(new Queue("TestDirectQueue2",true));
//container.addQueues(new Queue("TestDirectQueue3",true));
container.setMessageListener(myAckReceiver);
return container;
}
}
此时,生产者发送消息后,消费者就会使用MyAckReceiver进行消息的消费。我们在SimpleMessageListenerContainer中设置了消息的回复机制为AcknowledgeMode.MANUAL (手动确认)。在MyAckReceiver中消息消费,消息消费模拟了3中情况,一种是满足条件的正常返回(>5),一种是不满足情况的正常返回(<5),还有一种是异常抛出(=5)。
当满足条件的正常返回时,我们使用了肯定确认channel.basicAck(deliveryTag, true); 在这个ack方法中,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息。
当不满足条件的正常返回时,我们使用了否定确认。在regict方法中,第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝。这种情况要慎用,如果考虑不周,则会出现消息一直入队消费的死循环,导致消息堆积。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了
当抛出异常时,就使用了reject的false选项。表示我知道这个消息了,已经丢掉了。