目录
rabbitmq简介
一、RabbitMQ的结构
二、应用场景
1、流量削峰
2.应用解耦
3.异步处理
三、基本使用
四、rabbitmq高级特性
4.1消息的可靠性
4.1.1 发送方
4.1.2 消费端
4.1.3 RabbitMQ
4.2 发送端确认机制
引入
什么是发送端确认机制
三种确认机制
4.3 消息确认返回机制
引入
原理
4.4 消费端限流机制
引入
确认机制
代码
重回队列
4.5 消费端限流机制
RabbitMQ Qos
消费端限流机制参数设置
4.6 消息过期机制
4.7 死信队列
RabbitMQ 是轻量级的,易于在本地和云端部署。它支持多种消息传递协议。RabbitMQ 可以部署在分布式和联合配置中,以满足大规模、高可用性的要求。
rabbitmq遵循AMQP协议。
Broker:接收和分发消息的应用,RabbitMQ就是MessageBroker
Virtual Host:虚拟Broker,将多个单元隔离开
Connection: publisher/consumer和broker之间的tcp连接
Channel:connection内部建立的逻辑连接,通常每个线程创建单独的channel
Routing Key: 路邮件,用来指示消息的路由转发,相当于快递的地址
Exchange:交换机,相当于快递的分拨中心
Queue: 小队列,消息最终被送到这里等待consumer取走
Binding:exchange和queue之间的虚拟连接,用于message的分发依据
Exhange是AMQP协议和rabbitmq的核心组件
Exchange的功能是根据绑定关系和路由键为消息提供路由,将消息转发值相应的队列
exchange有4种类型:Direct/Topic/Fanout/Heders,其中Headers使用很少,以前三种为主
Direct(直接路由):Routing Key =Binding Key,容易配置使用
Fanout(广播路由):群发绑定的所有队列,使用与消息广播
Topic(话题路由):功能较为复杂,但使用零花,建议优先使用,为以后拓展留余地。
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正 常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限 制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体 验要好。
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可 以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api,B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
static String QUEUE = "restaurant";
static String EXCHANGE = "exchange.order.restaurant";
@Test
public void mqTestConnect() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
/**
* 生成一个交换机
* 1.交换机名称
* 2.交换机里面的消息是否持久化 默认消息存储在内存中
* 3.是否自动删除 如果服务器在不再使用交换机时删除该交换机
* 4.其他参数
*/
channel.exchangeDeclare(
EXCHANGE,
BuiltinExchangeType.DIRECT,
true,
false,
null
);
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE, false, false, false, null);
/**
* 绑定交换机与队列
*/
channel.queueBind(QUEUE,EXCHANGE,"key.order");
String payload = "msg";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish(EXCHANGE,"key.order",null,payload.getBytes());
}
}
rabbitmq控制台就会有对应的交换机与队列以及消息
绑定信息
消息payload
接下来编写消费者
@Test
public void mqConsumer() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
log.info("start listening message...");
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE, deliverCallback, consumerTag -> {
log.info("消息消费被中断");
});
while (true) {
Thread.sleep(100000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
DeliverCallback deliverCallback = ((consumerTag, message) -> {
String messageBody = new String(message.getBody());
log.info("deliverCallback:messageBody:{}", messageBody);
});
运行后,打印
1、消息发送后,发送端不知道RabbitMQ是否真的收到了消息
2、若RabbitMQ异常,消息丢失后,订单处理流程停止,业务异常
3、需要使用RabbitMQ发送端确认机制,确认消息发送
消息发送后,若中间件收到消息,会给发送端一个应答
生产者接收应答,用来确认这条消息是否正常发送到中间件
1、单条同步
static String EXCHANGE = "exchange.order.restaurant";
@Test
public void mqTestConnect() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
channel.confirmSelect();//开启确认模式
String payload = "user order ...";
channel.basicPublish(EXCHANGE,"key.order",null,payload.getBytes());
log.info("msg send ...");
if (channel.waitForConfirms(1000)) {//单条同步确认
log.info("msg confirm success");
}else {
log.info("msg confirm failed");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
控制台打印
2、多条同步确认
static String EXCHANGE = "exchange.order.restaurant";
@Test
public void mqTestConnect() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
channel.confirmSelect();//开启确认模式
String payload = "user order ...";
for (int i = 0; i < 10; i++) {
channel.basicPublish(EXCHANGE,"key.order",null,payload.getBytes());
log.info("msg send ...");
}
if (channel.waitForConfirms(1000)) {//单条同步确认
log.info("msg confirm success");
}else {
log.info("msg confirm failed");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
控制台打印
3、异步确认
public void mqACK() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
channel.confirmSelect();//开启确认模式
ConfirmListener listener = new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("ack deliveryTag: {},multiple: {}",deliveryTag,multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.info("nack deliveryTag: {},multiple: {}",deliveryTag,multiple);
}
};
channel.addConfirmListener(listener);
String payload = "user order ...";
for (int i = 0; i < 100; i++) {
channel.basicPublish(EXCHANGE,"key.order",null,payload.getBytes());
}
log.info("msg send ...");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
控制台打印
1、消息发送后,发送端不知道消息是否被正确路由,若路由异常,消息会被丢弃
2、消息丢弃后,订单处理流程停止,业务异常
3、需要使用RabbitMQ消息返回机制,确认消息被正确路由
消息发送后,中间件会对消息进行路由
若没有发现目标队列,中间件会通知发送方
Return Listener会被调用
消息返回的开启方法
在RabbitMQ基础配置中有一个关键配置项:Mandatory
Mandatory若为false,RabbitMQ将直接丢弃无法路由的消息
Mandatory若为true,RabbitMQ才会处理无法路由的消息
代码
@Test
public void mqRoutingAck() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
ReturnListener listener = (replyCode, replyText, exchange, routingKey, properties, body) ->
log.info("replyCode:{},replyText:{},exchange:{}," +
"routingKey:{},properties:{},body:{}",
replyCode,replyText,exchange,routingKey,properties,new String(body));
channel.addReturnListener(listener);
String payload = "user order ...";
channel.basicPublish(EXCHANGE,"key.order1",true,null,payload.getBytes());
log.info("msg send ...");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
控制台打印
默认情况下,消费端接收消息时,消息会被自动确认(ACK)
消费端消息处理异常时,发送端与消息中间件无法得知消息处理情况
需要使用RabbitMQ消费端确认机制,确认消息被正确处理
自动ACK:消费端收到消息后,会自动签收消息
手动ACK:消费端收到消息后,不会自动签收消息,需要我们在业务代码中显式签收消息(单条手动ACK 多条手动ACK)
但是我们是实际使用中,我们需要知道具体那条消息出现异常,推荐使用单条ACK;
重回队列
若设置了重回队列,消息被NACK之后,会返回队列末尾,等待进一步被处理
一般不建议开启重回队列,因为第一次处理异常的消息,再次处理,基本上也是异常
Channel channel;
@Test
public void mqConsumer() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
this.channel =channel;
log.info("start listening message...");
channel.basicConsume(QUEUE, false,deliverCallback ,consumerTag -> {
log.info("消息消费被中断");
});
while (true) {
Thread.sleep(100000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
DeliverCallback deliverCallback = ((consumerTag, message) -> {
String messageBody = new String(message.getBody());
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
log.info("deliverCallback:messageBody:{}", messageBody);
});
控制台打印
channel.basicNack(message.getEnvelope().getDeliveryTag(),false,true);
业务高峰期,可能出现发送端与接收端性能不一致,大量消息被同时推送给接收端,造成接收端服务崩溃,需要使用RabbitMQ消费端限流机制,限制消息推送速度,保障接收端服务稳定。
场景
1、业务高峰期,有个微服务崩溃了,崩溃期间队列挤压了大量消息,微服务上线后,收到大量并发消息
2、将同样多的消息推给能力不同的副本,会导致部分副本异常
针对以上问题,RabbitMQ开发了QoS(服务质量保证)功能,QoS功能保证了在一定数目的消息未被确认前,不消费新的消息,QoS功能的前提是不使用自动确认
原理:QoS原理是当消费端有一定数量的消息未被ACK确认时,RabbitMQ不给消费端推送新的消息,RabbitMQ使用QoS机制实现了消费端限流
prefetchCount:针对一个消费端最多推送多少未确认消息
global: true:针对整个消费端限流 false:针对当前channel
prefetchSize : 0(单个消息大小限制,一般为0)
prefetchSize与global两项,RabbitMQ暂时未实现
代码
Channel channel;
@Test
public void mqConsumer() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
this.channel =channel;
log.info("start listening message...");
channel.basicQos(2);
channel.basicConsume(QUEUE, false,deliverCallback ,consumerTag -> {
log.info("消息消费被中断");
});
while (true) {
Thread.sleep(100000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
DeliverCallback deliverCallback = ((consumerTag, message) -> {
String messageBody = new String(message.getBody());
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
log.info("deliverCallback:messageBody:{}", messageBody);
});
rabbitmq控制台
可以看到rabbitMq不会一下把消息全都推送到消费端,而是最多会推送我们指定的数值。
一、队列爆满怎么办?
1、默认情况下,消息进入队列,会永远存在,直到被消费
2、大量堆积的消息会给RabbitMQ产生很大的压力
3、2需要使用RabbitMQ消息过期时间,防止消息大量积压
二、RabbitMQ的过期时间(TTL)
RabbitMQ的过期时间称为TTL (Time to Live),生存时间
RabbitMQ的过期时间分为消息TTL和队列TTL
消息TTL设置了单条消息的过期时间
队列TTL设置了队列中所有消息的过期时间
三、如何找到适合自己的TTL?
1、TTL的设置主要考虑技术架构与业务
2、TTL应该明显长于服务的平均重启时间
3、建议TTL长于业务高峰期时间
代码
@Test
public void mqACK() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
factory.setHost("192.168.6.131");
factory.setPort(5672);
try (
Connection conn = factory.newConnection();
Channel channel = conn.openChannel().get()
){
channel.confirmSelect();//开启确认模式
String payload = "user order ...";
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().expiration("15000").build();
channel.basicPublish(EXCHANGE,"key.order",props,payload.getBytes());
log.info("msg send ...");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
rabbitmq控制台查看
过期时间为 15秒
过期之后会自动丢弃
队列设置过期
代码
HashMap args = new HashMap<>(16);
args.put("x-message-ttl",15000);
channel.queueDeclare(QUEUE+"ttl",
true,
false,
false,
args);
一、如何转移过期消息?
1、消息被设置了过期时间,过期后会直接被丢弃
2、直接被丢弃的消息,无法对系统运行异常发出警报
3、需要使用RabbitMQ死信队列,收集过期消息,以供分析
二、什么是死信队列
死信队列:队列被配置了DLX属性(Dead-Letter-Exchange)
当一个消息变成死信(dead message)后,能重新被发布到另一个Exchange,这个Exchange也是一个普通交换机
死信被死信交换机路由后,一般进入一个固定队列
三、怎样变成死信
1、消息被拒绝(reject/nack)并且requeue=false
2、消息过期(TTL到期)
3、队列达到最大长度
四、死信队列设置方法
1、设置转发、接收死信的交换机和队列:
Exchange: dlx.exchange
Queue: dlx.queue
RoutingKey: #
2、在需要设置死信的队列加入参数:
x-dead-letter-exchange = dlx.exchange
代码
//声明接收死信的交换机
channel.exchangeDeclare(
DLX_EXCHANGE,
BuiltinExchangeType.TOPIC,
true,
false,
null
);
//声明接收死信的队列
channel.queueDeclare(DLX_QUEUE,
true,
false,
false,
null);
channel.queueBind(DLX_QUEUE,DLX_EXCHANGE,"#");
HashMap args = new HashMap<>(16);
args.put("x-message-ttl",15000);
args.put("x-dead-letter-exchange",DLX_EXCHANGE);
channel.queueDeclare(QUEUE+"ttl",
true,
false,
false,
args);
rabbitmq控制台:
15秒后,可以看到 消息 转移到接收死信消息的队列中。