一个生产者,一个默认的交换机,一个队列,一个消费者
一个生产者,一个默认的交换机,一个队列,多个消费者
由于只有一个队列,且一个队列中的消息只能被一个消费者消费,会造成消息竞争
应用场景:
- 抢红包
- 资源任务调度
一个生产者,一个Fanout类型交换机,多个队列,每个队列绑定一个消费者
Fanout类型的交换机会把每条消息都发布到每个队列中,每个队列可以有一个消费者接收消息进行消费逻辑
应用场景:发布广告
一个生产者,一个DIRECT类型交换机,根据消息的规则发布到不同的队列,每个队列绑定一个消费者
生产者在创建DIRECT类型的exchange后,根据RoutingKey去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可
应用场景: 根据生产者的要求发送给特定的一个或者一批队列发送信息。
一个生产者,一个Topic类型交换机,根据通配符的规则,发布到不同的队列,每个队列绑定一个消费者
生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式 xxx.xxx.xxx去编写, * 代表一个单词,而# 代表0或者多个单词
消费者接收到消息进行处理后,有可能失败,失败后消费者可以拒绝该条消息,可以选择拒绝多条或者拒绝一条,可以选择销毁消息或者是重新放回队列
channel.basicReject:只支持对一条消息进行拒绝
void basicReject(long deliveryTag, boolean requeue) throws IOException;
deliveryTag:消息的投递标签,每一条消息在该channel内都有且是唯一的,类似于id
requeue:是否重新放入队列,false:直接销毁;true:重新放入队列
channel.basicNack是 channel.basicReject的补充,提供一次对多条消息进行拒绝的功能
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
multiple:批量确认标志,false:只拒绝本消息;true:所有比该deliveryTag小的消息都会被拒绝,已经ack的除外
除此之外需要注意的是,如果队列只有一名消费者,那么requeue之后需要防止出现死循环!!!
消息传递的可靠性
生产者要确定消息能够通过交换机到达到队列中
RabbitMq的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,然后定时再次发送当前消息;但是事务非常影响效率,因此RabbitMQ还提供了Confirm的确认机制
三步走:1. 开启confirm, 2. 发送消息, 3. 判断操作是否成功
//3.1 开启confirm
channel.confirmSelect();
//3.2 发送消息
String msg = "Hello-World!";
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//3.3 判断消息发送是否成功
if(channel.waitForConfirms()){
System.out.println("消息发送成功");
}else{
System.out.println("发送消息失败");
}
同样三步走:1. 开启confirm, 2. 批量发送消息, 3. 判断是否发送到交换机;如果有一个失败就全部失败
//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = "Hello-World!" + i;
channel.basicPublish("","HelloWorld",null,msg.getBytes());
}
//3.3 确定批量操作是否成功
channel.waitForConfirmsOrDie(); // 当你发送的全部消息,有一个失败的时候,就直接全部失败 抛出异常IOException
同样三步走:1. 开启confirm, 2. 批量发送消息, 3. 开始异步回调;重写发布成功和发布失败的方法
//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = "Hello-World!" + i;
channel.basicPublish("","HelloWorld",null,msg.getBytes());
}
//3.3 开启异步回调
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送成功,标识:" + deliveryTag + ",是否是批量" + multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送失败,标识:" + deliveryTag + ",是否是批量" + multiple);
}
});
三种confirm机制都是保证消息能够到达交换机,有可能队列名称、路由错了,到不了队列中;因此可以使用Return机制来监听消息是否到达指定队列中
开启Return机制,并在发送消息时,指定mandatory为true
// 开启return机制
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 当消息没有送达到queue时,才会执行。
System.out.println(new String(body,"UTF-8") + "没有送达到Queue中!!");
}
});
// 在发送消息时,指定mandatory参数为true
channel.basicPublish("","HelloWorld",true,null,msg.getBytes());
spring:
rabbitmq:
publisher-confirm-type: simple
publisher-returns: true
关闭自动确认,消费者在消费完消息后必须手动ACK
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动的
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动的
retry:
enabled: true # 开启消费者进行重试
max-attempts: 5 # 最大重试次数
initial-interval: 3000 # 重试时间间隔
在消费者消费消息之前,先将消息的id放到Redis中;
id-0:表示正在执行业务;id-1表示执行业务成功
在rabbitMQ将消息发送给消费者时:
- 先setnx
- 如果key不存在,那么就直接执行;
- 如果key已存在(setnx返回值1表示成功,返回0表示已存在):获取key的值,如果是0,那么消费者什么都不做;如果是1,直接手动ack
为了避免rabbitMQ宕机丢失消息,可以开启RabbitMQ的持久化
1.交换机持久化 在声明时指定durable为true
2.队列持久化 在声明时指定durable为true
3.消息持久化 在声明时指定delivery_mode为2(在BasicProperties props中)
创建队列的时候,使用OverFlow来配置队列的溢出操作
- 默认值是drop-head将头部(最老的)消息丢弃或者变成死信
- 如果设置为reject-publish就是拒绝接收发布者的消息;同时如果开启了confirm生产者确认模式,会调用basicNack()方法通知发送者消息被拒绝
需要注意的是,如果消息被路由到多个队列,那么就算信道通知生产者消息被拒绝,但是还是会路由到别的可以接受的队列
TTL指的是Time To Live,存活时间
如果队列中的消息指定了存活时间,过期了之后没有被消费,要么直接丢弃,要么放入死信队列
指定存活时间两种方式:
- 给单个消息设置
- 给队列设置,队列中的所有消息都有相同的过期时间
应用场景:下单后,半个小时未支付后自动删除
给单个消息设置过期时间
MessageProperties messageProperties=new MessageProperties(); // 消息属性对象
messageProperties.setMessageId(UUID.randomUUID().toString());
messageProperties.setExpiration("10000"); // 设置消息的过期时间为10秒
Message message = new Message(msg.getBytes(),messageProperties);
给队列设置过期时间
@Bean
public Queue ttlDirectQueue(){
// 在队列上 设置 此队列中 消息的过期时间
Map<String,Object> map=new HashMap<>();
// 队列中 所有的消息的过期时间 为 20秒
//map.put("x-message-ttl",20000);
// 队列中 所有的消息的过期时间 为 5秒
map.put("x-message-ttl",5000);
//return new Queue(TTL_DIRECT_QUEUE,true,false,false);
return new Queue(TTL_DIRECT_QUEUE,true,false,false,map);
}
死信队列:存放的是一些没有被及时消费的消息,也是一个普通的队列
死信交换机:Dead Letter Exchange,当消息成为死信后,会被重新发送到一个死信交换机,死信交换机也是一个普通的交换机
在以下三种情况会成为死信:
- 队列的长度达到了极限
- 消费者拒收了消息(basic.reject/ basic.nack),并且没有即使入队(requeue = false)
- 消息在队列中过了存活时间且没有被消费
使用步骤:
- 新建普通交换机,作为死信交换机
- 新建普通队列1,作为死信队列,绑定死信交换机
- 新建普通队列2,指定死信交换机,如果2中的消息成了死信,就会被转发到队列1中
实现方式:TTL+死信队列
实现步骤:
使用场景
如果一个用户在注册成功后,需要给他发短信并发邮件进行提示;已经注册成功了,发短信和发邮件并不是最重要的,可以慢慢处理
- 串行的方式:需要这些步骤挨个完成,需要150ms
- 并行:注册50ms;然后开两个线程,等这两个线程执行完都返回ok需要50ms;共计100ms
- 使用消息队列:注册50ms;然后将信息放入队列;放入队列用时5ms;共计55ms
串行方式
并行方式
使用消息队列
这个很好理解;双十一的时候,订单系统直接调用库存系统;如果库存系统宕机了,整个系统就瘫痪了
加入消息队列后,两者靠消息队列进行连接;库存系统宕机后不会影响订单系统;消息可以暂时存放在消息队列中慢慢处理
改进后
秒杀活动的时候,由于抢的人太多可能会导致服务宕掉
可以使用消息队列,队列满了之后可以直接抛弃掉后面的请求
可以看上面的延时队列
简图
详细架构图