目录
rabbitmq简介
RabbitMQ 是轻量级的,易于在本地和云端部署。它支持多种消息传递协议。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(话题路由):功能较为复杂,但使用零花,建议优先使用,为以后拓展留余地。
二、应用场景
1、流量削峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正 常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限 制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体 验要好。
2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3.异步处理
有些服务间调用是异步的,例如 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);
});
运行后,打印
四、rabbitmq高级特性
4.1消息的可靠性
4.1.1 发送方
- 需要使用RabbitMQ发送端确认机制,确认消息成功发送到RabbitMQ并被处理
- 需要使用RabbitMQ消息返回机制,若没有发现目标队列,中间件会通知发送方
4.1.2 消费端
- 需要使用RabbitMQ消费端确认机制,确认消息没有发生异常
- 需要使用RabbitMQ消费端限流机制,限制消息推送速度,保障接收端服务稳定
4.1.3 RabbitMQ
- 大量的消息堆积会给rabbitMQ产生很大的压力,需要rabbitmq消息过期时间,防止消息大量积压
- 过期后会直接被丢弃,无法对系统运行发出警报,需要使用rabbitMQ死信队列,收集过期消息。
4.2 发送端确认机制
引入
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);
}
}
控制台打印
4.3 消息确认返回机制
引入
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);
}
}
控制台打印
4.4 消费端限流机制
引入
默认情况下,消费端接收消息时,消息会被自动确认(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);
4.5 消费端限流机制
业务高峰期,可能出现发送端与接收端性能不一致,大量消息被同时推送给接收端,造成接收端服务崩溃,需要使用RabbitMQ消费端限流机制,限制消息推送速度,保障接收端服务稳定。
场景
1、业务高峰期,有个微服务崩溃了,崩溃期间队列挤压了大量消息,微服务上线后,收到大量并发消息
2、将同样多的消息推给能力不同的副本,会导致部分副本异常
RabbitMQ Qos
针对以上问题,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不会一下把消息全都推送到消费端,而是最多会推送我们指定的数值。
4.6 消息过期机制
一、队列爆满怎么办?
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<String, Object> args = new HashMap<>(16);
args.put("x-message-ttl",15000);
channel.queueDeclare(QUEUE+"ttl",
true,
false,
false,
args);
4.7 死信队列
一、如何转移过期消息?
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<String, Object> 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秒后,可以看到 消息 转移到接收死信消息的队列中。