rabbitMQ学习笔记
- rabbitMQ是一款MQ(消息队列)产品,除了rabbitMQ之外,常见的MQ产品还有kafka、socketMQ、activeMQ等...消息队列作为一种中间件,广泛应用于软件开发领域,主要的作用有异步、解耦、削峰。
- 消息队列产品都将程序分为生产者和消费者,消息的生产者将消息发送给消息队列,再有消息队列将消息发送给消费者。
- rabbitMQ支持七种模式:
- "Hello World":helloworld是最简单的模式,一个生产者通过队列对应一个消费者
- Work queues:(工作队列)一个生产者通过队列对应多个消费者
- Publish/Subscribe:(发布/订阅)一个生产者通过交换机对应多个队列,每个队列对应一个消费者
- Routing:(路由)
- Topics:(话题)
- RPC:(请求/回复)
- Publisher Confirms:(发布确认)
Hello World模式
- hello world是最简单的消息队列模式
代码实现
- 获取MQ连接的工具类
public class MqUtils {
private static final ConnectionFactory CONNECTION_FACTORY;
static {
//获取与MQ的连接工厂
CONNECTION_FACTORY = new ConnectionFactory();
//设置主机ip
CONNECTION_FACTORY.setHost("**.**.**.**");
//设置端口
CONNECTION_FACTORY.setPort(5672);
//设置连接的虚拟主机
CONNECTION_FACTORY.setVirtualHost("/edu");
//设置用户名和密码
CONNECTION_FACTORY.setUsername("ye");
CONNECTION_FACTORY.setPassword("1234qweR");
}
public static Connection getConnection() {
try {
//获取连接对象
return CONNECTION_FACTORY.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void close(Connection conn, Channel channel) {
try {
if (Objects.nonNull(channel)) {
channel.close();
}
if (Objects.nonNull(conn)) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 消息生产者示例
//生产消息
@Test
public void testSendMessage() throws IOException, TimeoutException {
Connection conn = MqUtils.getConnection();
assert conn != null;
Channel channel = conn.createChannel();
//队列声明
//第一个参数:队列的名称 第二个参数:是否需要将队列持久化
//第三个参数:是否独占队列 第四个参数:消费完成后是否删除队列
//第五个参数:额外的参数设置
channel.queueDeclare("hello",false,false,false,null);
//发布消息
//第一个参数:使用的交换机名称 第二个参数:使用的队列名称
//第三个参数:传递消息的额外设置 第四个参数:传递的消息体(字节)
Objects.requireNonNull(channel).basicPublish("", "hello", null, "hello rabbitMQ!".getBytes());
MqUtils.close(conn, channel);
}
注意
- 队列声明的第二个参数只能将队列持久化,并不能将队列中的消息持久化,要想将队列中的消息持久化,需要在消息发送时指定一个额外设置:MessageProperties.PERSISTENT_TEXT_PLAIN。
- 消息消费者示例
Connection conn = MqUtils.getConnection();
assert conn != null;
Channel channel = conn.createChannel();
//第二个参数:是否开启自动确认
Objects.requireNonNull(channel).basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("=========" + new String(body));
}
});
//mq消费者采用异步模型,如果不关闭连接,就会一直监听消息队列,当队列里有消息之后就会被消费
//在异步模型中,由于main线程和消费者线程是属于两个线程,所以当在main线程中关闭连接后,可能消费线程还没有执行回调
//MqUtils.close(conn, channel);
- 注意
- 消费者采用异步模型,不能在单元测试中测试(单元测试中只支持同步模型,不支持多线程)。
- 在关闭连接时,要先关闭Channel再关闭Connection,因为关闭Connection时会清空Channel,这时如果再关闭Channel就会爆出com.rabbitmq.client.alreadyclosedexception异常。
- 生产者和消费者所使用的队列参数要严格对应。
Work queues
- Work queues(工作队列):在消费者之间分配任务(默认平均的消费者模式)
代码实现
消费者默认是一次从队列中拿出多条消息,按照消费者数量进行平均分配,如果开启了消费自动确认,那么当消费者从队列中拿到分配给自己的消息之后,会告知队列自己已经确认拿到了消息,队列将会把这些消息删除。如果消费者拿到了五条消息,但是在处理完第三条消息之后宕机,那么其余两条消息就会丢失。并且这种一次性拿取所有消息的方式会降低程序的性能,更建议用竞争的方式去获取消息。
- 竞争的消费者实现
...
Channel channel = conn.createChannel();
channel.basicQos(1);//一次只获取一条消息
//第二个参数:是否开启消息自动确认,这里需要手动确认消息
Objects.requireNonNull(channel).basicConsume("hello", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("Consumer2==========" + new String(body));
//手动确认消息,第一个参数:当前消费的消息的标识 第二个参数:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
发布/订阅模式(fanout)
fanout(扇出)模式,一个生产者对应一个交换机对应多个临时队列。每个队列对应一个消费者。
在发布/订阅模式中,是一个消息的提供者对应多个消息的消费者。就是一条消息会被多个消费者进行消费。
代码实现
- 消息生产者示例
//获取连接
Connection connection = MqUtils.getConnection();
Channel channel = connection.createChannel();
//声明一个交换机,名字是logs 类型是fanout
channel.exchangeDeclare("logs","fanout");
//消息发布,fanout模式下不需要设置队列名
channel.basicPublish("logs","",null,"hello".getBytes());
MqUtils.close(connection,channel);
- 消息消费者示例
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定交换机
channel.queueBind(queue,"logs","");
//消息处理
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("消费者1*******"+new String(body));
}
});
消费者1和消费者2都会收到provider的消息,并且各自进行处理。
路由模式(routing)
路由模式是在发布/订阅模式的基础上(将消息全部发送给消费者),修改为将消息通过路由分别发送给不同的消费者。
代码实现
- 消息发送者示例
//声明交换机,类型是direct
channel.exchangeDeclare("logs_direct","direct");
//发送消息
channel.basicPublish("logs_direct","error",null,"this is error...".getBytes());
- 消息消费者示例1
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//声明交换机
channel.exchangeDeclare("logs_direct", "direct");
//绑定队列交换机和routingKey
channel.queueBind(queue, "logs_direct", "error");
//消费消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("当前消息为: "+new String(body));
}
});
此消费者1通过绑定交换机、路由和routingKey,只接收routingKey为"error"的消息
- 消息消费者示例2
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//声明交换机
channel.exchangeDeclare("logs_direct", "direct");
//绑定交换机和routingKey
channel.queueBind(queue, "logs_direct", "error");
channel.queueBind(queue, "logs_direct", "info");
channel.queueBind(queue, "logs_direct", "warning");
//消费消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("当前消息为: "+new String(body));
}
});
此消费者2通过队列绑定,接收routingKey为"error"、"info"、"warning"的消息
动态路由模型(topic)
动态路由模型(topic)是在路由模型(routing)的基础上加入了通配符'*'和'#'
* :可以代替一个单词。
# :可以代替多个单词。
代码实现
- 消息生产者示例
//声明一个交换机,类型是topic
channel.exchangeDeclare("topics","topic");
//发送的routingKey
//此消息会被1,2共同消费
String routingKey = "user.save";
//此消息只会被2消费
//String routingKey = "user.save.findAll"
//发送消息
channel.basicPublish("topics",routingKey,null,("这里是topics动态路由模型,routingKey:"+routingKey).getBytes());
- 消息消费者示例1
//声明交换机
channel.exchangeDeclare("topics", "topic");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//绑定动态路由,消费以user.开头,并且后面只有一个单词的所有消息
channel.queueBind(queue, "topics", "user.*");
//处理消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("消费者1:"+new String(body));
}
});
消费以user.开头,并且后面只有一个单词的消息
- 消息消费者示例2
***
//绑定动态路由,消费routingKey以user.开头的所有消息
channel.queueBind(queue, "topics", "user.#");
***
springboot整合rabbitMQ
- 导入rabbitMQ的启动器依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 在配置文件中配置连接MQ的各种参数
# rabbitMQ的参数配置
spring.rabbitmq.host=**.**.**.**
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/edu
spring.rabbitmq.username=*****
spring.rabbitmq.password=*******
- 在消息生产端注入MQ的模板对象
@Autowired RabbitTemplate rabbitTemplate;
注意:
- 模板对象是启动器自动提供的,不需要手动定义,只要在配置文件中配置了MQ的连接参数,就会自动加入IOC容器。
- 在springboot中,交换机和队列声明是由消费者端完成的,生产端只需要指定队列(交换机)名,然后发送消息即可。
- 编写消息生产代码
//向名为topic的交换机中发送路由key为user.save的消息,消息体为:user.save
rabbitTemplate.convertAndSend("topics","user.save","user.save");
- 编写消息消费者代码
- hello world模型
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello"))
public class HelloConsumer {
@RabbitHandler
public void receive(String message) {
System.out.println("消费者消费了消息:\t"+message);
}
}
- work模型
@Component
public class WorkConsumers {
@RabbitListener(queuesToDeclare = @Queue(value = "work"))
public void consumer1(String message) {
System.out.println("work的消费者1消费了消息:\t"+message);
}
@RabbitListener(queuesToDeclare = @Queue(value = "work"))
public void consumer2(String message) {
System.out.println("work的消费者2消费了消息:\t"+message);
}
}
- fanout模型
@Component
public class FanoutConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //绑定临时队列
exchange = @Exchange(value = "log",type = "fanout") //绑定名为log,类型是fanout的交换机
)
})
public void consumer1(String message) {
System.out.println("消费者1消费了消息:\t"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, //绑定临时队列
exchange = @Exchange(value = "log",type = "fanout") //绑定名为log,类型是fanout的交换机
)
})
public void consumer2(String message) {
System.out.println("消费者2消费了消息:\t"+message);
}
}
- route模型
@Component
public class RouteConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "direct",type = "direct"),
key = {"info","error","warn"}
)
})
public void consumer1(String message) {
System.out.println("消费者1消费了消息:\t"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "direct",type = "direct"),
key = {"info"}
)
})
public void consumer2(String message) {
System.out.println("消费者2消费了消息:\t"+message);
}
}
- topics模型
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics",type = "topics"),
key = {"user.*"}
)
})
public void consumer1(String message) {
System.out.println("消费者1:\t"+message);
}
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics",type = "topics"),
key = {"user.#"}
)
})
public void consumer2(String message) {
System.out.println("消费者2:\t"+message);
}
注意:@RabbitListener注解可以作用于类上,也可以作用于方法上,当作用于类上时,有@RabbitHandler注解的方法作为消费者。当作用于方法上时,方法作为消费者