RabbitMQ简单入门
服务端安装及配置
docker安装
使用docker安装RabbitMQ,注意,要选择tag包含management的镜像(包含web端管理插件)
docker pull rabbitmq:3.7.7-management
docker run -d --name rabbitmq3.7.7 -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=xxx rabbitmq:3.7.7-management
设置初始账号和密码,web页面地址如下
http://ip:15672
客户端访问
添加依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.4.3</version>
</dependency>
发布消息
public class TestRabbitClient {
public static void main(String[] args) throws IOException, TimeoutException {
String host = "";
String queueName = "hello_queue";
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(host);
factory.setUsername("");
factory.setPassword("");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
//队列名称 是否持久化到硬盘 是否消息共享(多个消费者消费) 是否自动删除
channel.queueDeclare(queueName, false, false, false, null);
String message = "Hello World!";
//发布消息 使用的默认交换机
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费消息
channel.basicConsume(queueName, true, new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println("receive message: " + new String(message.getBody()));
}
}, (x) -> {
System.out.println("消息被中断");
});
注意:channel被关闭之后 就不能接收到消息了
交换机
交换机类型
- Direct (直连交换机): 最常使用,会根据routingkey进行精准匹配。直连交换机可以分发任务给多个工作者(worker)
- Fanout (扇形交换机): 将消费分发给所有绑定的队列,而不会理会routingkey。优点是转发消息最快,性能最好。一般会用来处理广播消息(broadcast routing)。
- Topic(主题交换机): 根据routingkey进行模糊匹配,将消息分发给一个或多个队列(delimited by dots)。 routingkey可以有通配符'','#'。 表示匹配一个单词,# 匹配0个或多个单词。当绑定建为'#'时,表示接收所有消息,和Fanout交换机类似了,当绑定键不包含*和#时,和Direct交换机功能类似了。
- Headers (头交换机): 类似于直连交换机。不同点在于头交换机的路由规则建立在头属性之上而不是路由键,一般开发使用较少。
使用Fanout交换机实现广播
@Configuration
public class RabbitConfig {
@Bean
public FanoutExchange testExchange2() {
return new FanoutExchange("test_exchange2");
}
}
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(), //注意这里不要定义队列名称,系统会随机生成 spring.gen-4klrfbJ0TfmrGQnCpyPAQg这种格式
exchange = @Exchange(value = "test_exchange2", type = ExchangeTypes.FANOUT))
)
public class RabbitMqReceiver {
/**
* 测试消息接收
*/
@RabbitHandler
public void process(String context, Message message, Channel channel) {
}
}
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(), //注意这里不要定义队列名称,系统会随机生成 spring.gen-4klrfbJ0TfmrGQnCpyPAQg这种格式
exchange = @Exchange(value = "test_exchange2", type = ExchangeTypes.FANOUT))
)
public class RabbitMqReceiver {
/**
* 测试消息接收
*/
@RabbitHandler
public void process(String context, Message message, Channel channel) {
}
}
通过Spring来创建动态队列,动态绑定关系。如果我们不指定队列名称,Spring 会创建非持久化、排他、自动删除的队列,具体逻辑可以看 RabbitListenerAnnotationBeanPostProcessor 的 declareQueue 方法。
死信队列
当一条消息在队列中出现以下三种情况的时候,该消息就会变成一条死信。
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期(通过x-message-ttl属性设置)
- 队列达到最大长度(通过x-max-length属性设置)
当消息在一个队列中变成一个死信之后,如果配置了死信队列,它将被重新publish到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。
//声明订单取消正常队列 当消息过期时,将消息发布到死信队列
Map<String, Object> params = Map
.of("x-dead-letter-exchange", "order_cancel_delay_queue_exchange",
"x-dead-letter-routing-key", "order_cancel_delay_queue_key",
"x-message-ttl", 20 * 1000);//消息过期时间 毫秒 在这里设置不灵活(不能改),可以在消息发布时设置
channel.queueDeclare("order_cancel_queue", true, false, false, params);
channel.exchangeDeclare("order_cancel_queue_exchange", BuiltinExchangeType.DIRECT, true);
channel
.queueBind("order_cancel_queue", "order_cancel_queue_exchange", "order_cancel_queue_key");
在发布消息时指定过期时间
//声明订单取消正常队列 当消息过期时,将消息发布到死信队列
Map<String, Object> params = Map
.of("x-dead-letter-exchange", "order_cancel_delay_queue_exchange",
"x-dead-letter-routing-key", "order_cancel_delay_queue_key");//消息过期时间 毫秒
channel.queueDeclare("order_cancel_queue", true, false, false, params);
channel.exchangeDeclare("order_cancel_queue_exchange", BuiltinExchangeType.DIRECT, true);
channel
.queueBind("order_cancel_queue", "order_cancel_queue_exchange", "order_cancel_queue_key");
BasicProperties basicProperties = new Builder()
.expiration("15000")//消息过期时间 15s
.build();
//向正常队列发布消息
channel.basicPublish("order_cancel_queue_exchange", "order_cancel_queue_key", basicProperties,
"hello".getBytes());
延时队列
- 使用死信队列的消息过期方式来实现
有一个问题,如果一个队列配置了死信队列,在发布消息时指定过期时间,第一条20S,第二条5S,那么第二条也会延迟到20S后执行,因为rabbitmq只会检查第一个消息是否过期。 - 使用rabbitmq的延迟插件,创建的交换机类型为x-delayed-message,并设置x-delayed-type属性为direct,这种方式也没有第一种方式的问题。
public Exchange demo08Exchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(Demo01Message.NEW_DELAY_EXCHANGE, "x-delayed-message", true, false, args);
}
//消息发布
rabbitTemplate.convertAndSend(Demo01Message.NEW_DELAY_EXCHANGE, Demo01Message.ROUTING_KEY, message, msg -> {
msg.getMessageProperties().setDelay(20 * 1000);//延迟20S
return msg;
});
整合Spring
- @EnableRabbit注解导入的RabbitListenerAnnotationBeanPostProcessor来处理@RabbitListener注解和@RabbitHandler注解
- 将消息处理器包装成MultiMethodRabbitListenerEndpoint,注册到RabbitListenerEndpointRegistrar对象中
- RabbitListenerEndpointRegistrar对象只是一个工具类,最终是注册到RabbitListenerEndpointRegistry对象中,它也是通过@EnableRabbit注解导入的
- RabbitListenerEndpointRegistry将RabbitListenerEndpoint对象包装成MessageListenerContainer对象,它其中包含MessageListener对象(包装了消息处理器)
- 在MessageListenerContainer的start()方法过程中,调用checkMismatchedQueues()方法
- 通过调用CachingConnectionFactory创建connection,在这个过程中调用CompositeConnectionListener的onCreate()方法
- ConnectionListener是通过RabbitAdmin对象添加到CachingConnectionFactory中的
- 通过RabbitAdmin对象声明Queue,Exchange,Binding
- 继续MessageListenerContainer的start()方法,将消息处理器包装成AsyncMessageProcessingConsumer对象,它是一个Runnable,交给线程池处理,可以通过 concurrentConsumers 属性来控制消费者数量从而并发消费
- AsyncMessageProcessingConsumer中包含一个BlockingQueueConsumer对象,在它的start()方法中会注册消息处理的回调,具体类为InternalConsumer
- 有消息到来时,InternalConsumer向BlockingQueueConsumer的queue中加入一条消息(推拉结合)
- AsyncMessageProcessingConsumer一直在轮训获取BlockingQueueConsumer的queue,最大等待时间1秒。
- 获取到消息,交给最终的消息处理器处理(就是我们标记@RabbitListener注解和@RabbitHandler注解的方法)
关于ConfirmCallback和ReturnCallback的使用
- ConfirmCallback : 注意:只能确认消息是否能到达 Exchange
- ReturnCallback : 注意 它是当交换机路由不到 队列的时候 它才会被触发
如何保证消息不被重复消费
每个消息添加一个Id,消费时判断是否已经消费过
//消息发送方
Message message = MessageBuilder.withBody("hello".getBytes(StandardCharsets.UTF_8))
.setContentType(
MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
.setContentEncoding(StandardCharsets.UTF_8.name())
.setMessageId(UUID.randomUUID().toString()).build();
rabbitTemplate.convertAndSend("test_exchange1", "test_ronting_key1", message);
//消息消费方
@RabbitHandler
public void process(String context, Message message, Channel channel) {
System.out.println("接收到消息: " + context);
System.out.println(message.getMessageProperties().getMessageId());
// 不管成功失败,向原队列确认消费
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
log.error("原队列确认消费失败:" + e.getLocalizedMessage());
}
}
参数中context的内容类似new String(message.getBody(), StandardCharsets.UTF_8);,源码位于 SimpleMessageConverter 的 fromMessage() 方法的100行
如何保证消息不丢失
- 生产端,使用confirm机制,发送失败重试
- RabbitMQ服务端,队列持久化,消息内容持久化
- 消费端,手动确认机制
集群部署
-
普通集群模式(无高可用性): 默认模式,以两个节点(rabbit01、rabbit02)为例来进行说明。对于Queue来说,消息实体只存在于其中一个节点rabbit01(或者rabbit02),rabbit01和rabbit02两个节点仅有相同的元数据,即队列的结构。当消息进入rabbit01节点的Queue后,consumer从rabbit02节点消费时,RabbitMQ会临时在rabbit01、rabbit02间进行消息传输,把A中的消息实体取出并经过B发送给consumer。
-
镜像集群模式(高可用性): 最常用的集群模式,把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案。该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取。
当然,负载均衡还是需要我们自己做的。
参考
RabbitMQ 多实例 广播消息_松月的博客-程序员宅基地_rabbitmq 广播消息
RabbitMQ集群镜像模式部署
消息幂等