3.交换机
①基础知识:
(1)概念
之前是生产者直接将消息发送到队列中,现在在生产者和队列中加了一层:交换机。
生产者将消息发送给交换机,交换机再将消息发送给队列。
生产者生产的消息不会直接发送到队列
交换机要会处理收到的消息:是将消息放到特定队列还是将消息丢弃(由交换机的类型决定)
交换机的类型:直接(direct)、主题(topic)、标题(headers)、扇出(fanout)(发布订阅)
(2)默认exchange
默认交换机:用“”空字符串表示(第一个参数)
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
(3)临时队列
没有被持久化的队列,断开消费者连接就会被自动删除
String queueName = channel.queueDeclare().getQueue();
(4)绑定binding
是指exchang和queue之间的绑定关系,通过rountingKey进行识别这个绑定关系。
②exchange类型
(1)Fanout扇出类型
将接受的所有消息广播(routerKey相同)到它知道的所有队列(只有和它绑定的队列)中
消费者:
public class Receiver1 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印在屏幕........... ");
//发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("控制台打印接收到的消息" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}
public class Receiver2 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息写到文件........... ");
//发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
File file = new File("D:\\rabbitmq_info.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("数据写入文件成功");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}
生产者:
public class EmitLog {
private static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel= RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner sc=new Scanner(System.in);
System.out.println("请输入信息:");
while(sc.hasNext()){
String message=sc.nextLine();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:"+message);
}
}
}
(2)直接direct Exchange路由
路由:通过routerkey来进行指定发送的队列
消费者:
public class Receiver1 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "disk";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println("等待接收消息.......... ");
//发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
message="接受绑定键:"+delivery.getEnvelope().getRoutingKey()+":消息:"+message;
System.out.println("error接收到的消息" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}
public class Receiver2 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "console";
//队列声明
channel.queueDeclare(queueName, false, false, false, null);
//队列绑定
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
System.out.println("等待接收消息...");
//发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
message = "接收绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message;
System.out.println("info和warning 消息已经接收:\n" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
生产者:
public class EmitLog {
private static final String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel= RabbitMQUtils.getChannels();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Map<String,String> bindingKey=new HashMap<>();
bindingKey.put("info","普通info");
bindingKey.put("error","错误");
bindingKey.put("warning","警告");
bindingKey.put("debug","调试");
for(Map.Entry<String,String> bindingKeyEntry:bindingKey.entrySet()){
String bindingKeys=bindingKeyEntry.getKey();
String message=bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKeys,null,message.getBytes());
System.out.println("生产者发送消息:"+message);
}
}
}
(3)Topic Exchange
主题交换机是在direct交换机上的进一步改进,比direct更加灵活(我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候direct 就办不到了。这个时候就只能使用 topic 类型)
Topic的要求:
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词
比如说:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit".这种类型的。
当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的:
-
*(星号)可以代替一个单词
-
#(井号)可以替代零个或多个单词
Topic 匹配案例
下图绑定关系如下
-
Q1-->绑定的是
-
中间带 orange 带 3 个单词的字符串
(*.orange.*)
-
-
Q2-->绑定的是
-
最后一个单词是 rabbit 的 3 个单词
(*.*.rabbit)
-
第一个单词是 lazy 的多个单词
(lazy.#)
-
上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的
例子 | 说明 |
---|---|
quick.orange.rabbit | 被队列 Q1Q2 接收到 |
azy.orange.elephant | 被队列 Q1Q2 接收到 |
quick.orange.fox | 被队列 Q1 接收到 |
lazy.brown.fox | 被队列 Q2 接收到 |
lazy.pink.rabbit | 虽然满足两个绑定但只被队列 Q2 接收一次 |
quick.brown.fox | 不匹配任何绑定不会被任何队列接收到会被丢弃 |
quick.orange.male.rabbit | 是四个单词不匹配任何绑定会被丢弃 |
lazy.orange.male.rabbit | 是四个单词但匹配 Q2 |
注意:
-
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
-
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
代码实现:
生产者:
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
/**
* Q1-->绑定的是
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
* Q2-->绑定的是
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)
*
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
消费者:
public class ReceiveLogsTopic01 {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明 Q1 队列与绑定关系
String queueName = "Q1";
//声明
channel.queueDeclare(queueName, false, false, false, null);
//绑定
channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列:" + queueName + " 绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
public class ReceiveLogsTopic02 {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明 Q2 队列与绑定关系
String queueName = "Q2";
//声明
channel.queueDeclare(queueName, false, false, false, null);
//绑定
channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列:" + queueName + " 绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
4.队列
①死信队列
死信概念:
死信就是指由于某种原因queue中的消息不能被消费,一个队列中均是死信,那这个队列就是死信队列
应用:当消息消费发生异常的时候,将消息投入到死信队列中保证订单业务的消息数据不丢失,比如在用户下单后点击支付但是未成功支付,在指定时间未支付就自动失效
死信来源:
-
消息TTL过期
-
队列达到最大长度(不能再添加数据到mq中)
-
消息被拒绝
-
代码实现:
消息TTL过期:
C1:
public class Consumer1 {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
//声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue, false, false, false, null);
//死信队列绑定:队列、交换机、路由键(routingKey)
channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
//正常队列
String normalQueue = "normal-queue";
channel.queueDeclare(normalQueue, false, false, false, params);
channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
System.out.println("等待接收消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer01 接收到消息" + message);
};
channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
});
}
}
c2:
public class Consumer2 {
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
//声明交换机
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue, false, false, false, null);
channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
System.out.println("等待接收死信消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer02 接收到消息" + message);
};
channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
});
}
}
生产者:
public class Provider {
private static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMQUtils.getChannels();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的 TTL 时间 10s
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
//该信息是用作演示队列个数限制
for (int i = 1; i < 11; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
System.out.println("生产者发送消息:" + message);
}
}
}
队列最大长度:
//设置正常队列长度
params.put("x-max-length",6);
拒绝消息:
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
if(message.equals("info5")){
System.out.println("Consumer01 被拒绝的消息" + message);
channel.basicReject(delivery.getEnvelope().getDeliveryTag(),false);
}
System.out.println("Consumer01 接收到消息" + message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
channel.basicConsume(normalQueue, false, deliverCallback, consumerTag -> {
});
②延迟队列
概念:
存放在指定时间到了以后或者之前取出和处理(需要在指定时间被处理)的消息。
使用场景:
-
订单在十分钟之内未支付则自动取消
-
新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
-
用户注册成功后,如果三天内没有登陆则进行短信提醒。
-
用户发起退款,如果三天内没有得到处理则通知相关运营人员。
-
预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
TTL:
-
队列TTL:
一旦消息过期就会被队列丢弃,如果设置死信队列就会被丢弃到死信队列中
-
消息TTL
消息即使过期,也不会被马上过期,如果当时队列有严重的消息积压情况,已过期的消息也许能存活较长时间
架构:
普通版本:
X,Y是交换机、QA、QB、QD是队列(QA的TTL设置为10s,QB的TTL设置为40s)QD为死信队列
有个缺陷:如果需要延迟一个小时,那又需要新建一个队列;那每次都需要新建一个队列和设置TTL太过麻烦!
优化:
增加一个QC队列但是不设定TTL,由生产者发送TTL给QC来限制延迟时间
总结
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
5.RabbitMQ其他知识点
①幂等性
概念:
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用。
(支付:点击支付后付款成功但是由于网络问题未显示又支付了一次,这时两次都被付款了)
消息重复消费:
消费者已经消费了MQ中的消息,但是在发送ack时网络中断导致ack未成功发送,MQ会将该消息重新发送给别的消费者又进行一次消费
解决思路:
使用一个全局ID或者写个唯一标识进行判断,每次消费消息时用该id判断该消息是否已经被消费过
业界主流的幂等性有两种操作:
a. 唯一 ID+指纹码机制,利用数据库主键去重,
b.利用 redis 的原子性去实现
-
唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
-
note Redis 原子性
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
②优先级队列
队列中添加优先级:
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
消息中添加优先级:
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(10).build();
public class PriorityConsumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
System.out.println(message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
③惰性队列
:消息是保存在内存中还是磁盘上
正常情况:消息是保存在内存中default
惰性队列:消息是保存在磁盘上(性能不太好 不常用)lazy
使用场景:消费者宕机、下线导致长时间不能消费消息造成信息的堆积(此时将消息放在磁盘上,可以不消耗RabbitMQ的内存)
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
④集群
总结
RabbitMQ的介绍就到这里啦!