交换机
-
direct 直连
把消息路由到那些binding key与routing key完全匹配的Queue -
topic Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue
和direct 都需要绑定route-key
topic的#和*,# 表示和多个关键字匹配,* 和一个关键字匹配 -
header 匹配不依赖于route-key和banding-key,会根据header中的值来 匹配规则,与queue绑定的时候不需要绑定route-key,但是可以设置x-match字段为any或者all,设置为any,则只要发送消息的header和绑定的header有一个相符合,则转发给队列,如果设置为all,header必须全部符合才转发给队列 ,因为按照header来匹配
-
fanout
它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
与queue绑定的时候不需要指定route-key(设置routeKey为""),发送给与fanout绑定的所有队列
Channel与Connection
Connection 是真实的TCP连接
Channel 是在TCP连接上虚拟的多个信道,,就像一条网线是一个Connect,,如果客户端程序是多线程的,,,有100人都要上网,为了降低成本,怎么办不可能拉100条网线吧。就给你每个人分了一个账号拨号上网,使用同一个网线。那怎么保证消息不会混乱呢? 每个账号就是唯一的标识,这个网线上的信息都会包含账号信息,,就是某条信息是哪个账号的。这样来保证消息不会混乱。
从ConnectionFactory可以生成connection,connection 可以生成Channel
channel可以声明 交换机,队列,绑定关系。发送消息。 很多工作都是由channel完成的。
两个很有用的类
- RabbitAdmin // 封装了 对Rabbitmq的管理
// Rabbitmq管理Bean
@Bean
public RabbitAdmin ampqManager(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) throws IOException, TimeoutException {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
// 申明交换机
rabbitAdmin.declareExchange(getExchange());
// 声明队列
rabbitAdmin.declareQueue(getQueue());
// 交换机和队列的绑定
rabbitAdmin.declareBinding(new Binding("queue1",Binding.DestinationType.QUEUE,"my_topicExchange","route-topic",null));
return rabbitAdmin;
}
- RabbitTemplate 简化了数据收发
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //
public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
数据收发使用RabbitTemplate中的现成的方法。
发送数据的底层是调用的basicPublish方法,
这个方法由三个重载
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body)
throws IOException;
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
throws IOException;
共有的参数是
- exchange 交换机
- routeKey 路由键
- mandatory 设置为true 则如果过根据route-key找不到合适的队列,就调用rutern 将消息返还给发送者,如果设置为false,找不到队列直接丢弃。
重点总结:
1、几种交换机的效率以及特点
- fanout模式 ,在设置交换机路由绑定的时候设置,会转发给每一个和当前fanout交换机绑定的队列,routekey设置为 “”。 效率最高
- topic模式,,即队列只接收关心的topic。 设置交换机,队列绑定的时候 灵活的routeKey。 如 topic1.*可以匹配后面一个任意字段(topic.xxx),topic.#可以匹配任意多个字段(topic.xixi.heihei) 效率次之。 通常还是用的比较多的。消息分类处理,,发送不同类型的消息的时候指定不同的routekey 来使得其进入不同的队列。。设置监听器的时候,通过监听不同的队列,来对不同的消息进行消费。
- direct交换机 直接连接,只有routekey完全匹配才会发送消息到对应的队列。适用于需要单独处理的消息。效率最低
2 消息监听的两种方式
1 注解监听。
优点:简单
缺点:不够灵活。效率没有监听器高。
@RabbitListener(queues = "directQueue")
public void receiveMessage(@Payload Message message) {
System.out.println("收到消息" + message.getBody().toString());
}
2 设置监听器。
理解: 监听器就像一个容器,就像班主任站在讲台上,下面每个学生都是一个队列,班主任监听,谁说话,就让体育老师抓出去去教育一顿。
- 教室和班主任就相当于 监听容器
- 学生 相当于放在容器中监听的队列
- 学生说话 相当于 队列消息事件
- 体育老师 相当于消费者
- PreFechCount 相当于设置体育老师一次抓出去的学生数量。一个一个抓效率低。有三个学生说话的,一次抓三个出去一个一个处理,处理完了之后再回来继续抓。
@Bean
public SimpleMessageListenerContainer cmdLogMessageContainer() throws IOException, TimeoutException {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
//队列丢失,true : 队列在容器运行时被移除,则容器停止;false : 继续尝试启动消费者,每个消费者在每次恢复尝试时都会进行3次尝试(间隔5秒)
container.setMissingQueuesFatal(false);
//将队列放入监听容器
container.setQueues(direct1(),topic1(),topic2());
//监听通道打开,默认打开
container.setExposeListenerChannel(true);
//最大消费者数
container.setMaxConcurrentConsumers(3);
// 每个消费者的发生一次ack处理数量
container.setPrefetchCount(100);
//每次接收消费者数
container.setConcurrentConsumers(3);
//设置确认模式手工确认
container.setAcknowledgeMode(AcknowledgeMode.AUTO);
//设置监听通道,自定义监听器
container.setMessageListener(new DirectHandler());
return container;
}
3、两个类 RabbitTemplate 和RabbitAdmin
这两个类是spring封装好的。使用RabbitMq这两个类也就足够了。
RabbitAdmin 来配置RabbitMq的,交换机,队列,绑定关系
RabbitTemplate 为我们封装了发送消息的方法,使得发送消变的更简单。
4、一个生产者对应多个消费者
5、 总之 ranbbitmq四个步骤
1 创建连接工厂Bean 这是第一步
@Bean
public ConnectionFactory connectionFactory() throws IOException, TimeoutException {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost(rabbitmqSetting.getHost());
factory.setPort(rabbitmqSetting.getPort());
factory.setUsername(rabbitmqSetting.getUsername());
factory.setPassword(rabbitmqSetting.getPassword());
factory.setVirtualHost(rabbitmqSetting.getVirtualHost());
factory.setPublisherConfirms(true); // 设置消息回调
return factory;
}
2 声明交换机,队列,绑定关系。。这个需要 拿到连接工厂的Bean
// Rabbitmq管理Beangit
@Bean
public RabbitAdmin ampqManager(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) throws IOException, TimeoutException {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
// 申明交换机
rabbitAdmin.declareExchange(getDirectExchange());
rabbitAdmin.declareExchange(getExchange()); // topic Exchange
rabbitAdmin.declareExchange(getFanoutExchange());
// 声明队列 direct
rabbitAdmin.declareQueue(direct1());
rabbitAdmin.declareQueue(direct2());
// fanout
rabbitAdmin.declareQueue(fanout1());
rabbitAdmin.declareQueue(fanout2());
// topic
rabbitAdmin.declareQueue(topic1());
rabbitAdmin.declareQueue(topic2());
// 交换机和队列的绑定
rabbitAdmin.declareBinding(new Binding("directQueue", Binding.DestinationType.QUEUE, "my_direct", "my_direct", null));
rabbitAdmin.declareBinding(new Binding("directQueue2", Binding.DestinationType.QUEUE, "my_direct", "my_direct", null));
// topic
rabbitAdmin.declareBinding(new Binding("topicQueue2", Binding.DestinationType.QUEUE, "my_topic", "my_topic.*", null));
rabbitAdmin.declareBinding(new Binding("topicQueue", Binding.DestinationType.QUEUE, "my_topic", "my_topic.q2", null));
//fanout 不用指定routekey 不能填写null 要填写"" fanout 广播模式,发送给所有的队列
rabbitAdmin.declareBinding(new Binding("fanoutQueue1",Binding.DestinationType.QUEUE,"my_fanout","",null));
// rabbitAdmin.purgeQueue("queue1");
return rabbitAdmin;
}
3 添加RabbitmqTemplate
,需要注意如果使用回调的话,在添加RabbitTemplate这个Bean的时候
/**
* <p>如果需要在生产者需要消息发送后的回调,需要对rabbitTemplate设置ConfirmCallback对象
* <p>由于不同的生产者需要对应不同的ConfirmCallback
* <p>如果rabbitTemplate设置为单例bean
* <p>则所有的rabbitTemplate实际的ConfirmCallback为最后一次申明的ConfirmCallback
* <p>SCOPE_PROTOTYPE 每次注入或者通过上下文获取的时候,都会创建一个新的bean实例
*/
//Template获取连接信息,然后才可以使用Template的模板方法
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //
public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
4 设置监听器
@Bean
public SimpleMessageListenerContainer cmdLogMessageContainer() throws IOException, TimeoutException {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
//队列丢失,true : 队列在容器运行时被移除,则容器停止;false : 继续尝试启动消费者,每个消费者在每次恢复尝试时都会进行3次尝试(间隔5秒)
container.setMissingQueuesFatal(false);
//将队列放入监听容器
container.setQueues(direct1(),topic1(),topic2());
//监听通道打开,默认打开
container.setExposeListenerChannel(true);
//最大消费者数
container.setMaxConcurrentConsumers(3);
// 每个消费者的发生一次ack处理数量
container.setPrefetchCount(100);
//每次接收消费者数
container.setConcurrentConsumers(3);
//设置确认模式手工确认
container.setAcknowledgeMode(AcknowledgeMode.AUTO);
//设置监听通道,自定义监听器
container.setMessageListener(new DirectHandler());
return container;
}
5 消费者,实现自定义监听器
@Component
public class DirectHandler implements ChannelAwareMessageListener {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Async
@Override
public void onMessage(Message message, Channel channel) throws Exception {
logger.info("监听器1收到数据"+new String(message.getBody()));
}
}
消息的延时
两种方式 ,
- 1 通过设置ttl和死信交换机
- 2 使用延时交换机插件
1 队列ttl 单条消息ttl
- 队列ttl的原理是 给一条普通队列设置ttl时间,以及绑定死信交换机,,在队列中的消息过期以后会路由到死信交换机,由死信交换机处理。。。死信交换机和普通交换机用法相同。
/**
* 定义死信队列相关信息
*/
public final static String deadQueueName = "dead_queue";
public final static String deadRoutingKey = "dead_routing_key";
public final static String deadExchangeName = "dead_exchange";
/**
* 死信队列 交换机标识符
*/
public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
/**
* 死信队列交换机绑定键标识符
*/
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
/**
* 队列的ttl时间
*/
public static final String QUEUE_MSG_TTL = "x-message-ttl";
@Bean
Queue ttlQueue() {
Map<String, Object> map = new HashMap<>();
map.put(QUEUE_MSG_TTL, 20000); //20s的存活时间 // 指定队列的存活时间20s
map.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName); // 指定死信交换机
map.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey); // route_key 死信交换机将用这个route_key来路由消息,作用于死信交换机和死信队列之间。
return new Queue("ttlQueue", true, false, false, map);
}
- 消息ttl是在发送消息的时候指定Properties的,此时延时队列可以指定队列的ttl,也可以不指定,如果队列和消息都有ttl, 则按照时间小的来。
@GetMapping("/deadByMsg")
public void deadByMessage(@RequestParam(value = "time")String time) {
logger.info("我是线程" + Thread.currentThread().getName());
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 持久
properties.setExpiration(time); // 设置消息的ttl
Message msg = new Message("测试死信消息---发送消息的时候指定ttl------我是测试延时队列".getBytes(), properties);
provide.send("ttlExchange", "ttl_key", msg);
}
2 延时交换机插件
实现消息延时发送有两种方式
- 一种是通过上面设置队列ttl或者单条消息的ttl,设置死信交换机和死信队列,让消息超时以后由死信交换机处理。以此达到延时目的
- 给rabbitmq安装延时插件,安装教程https://blog.csdn.net/liyongbing1122/article/details/81225761。 安装完成以后,rabbitmq就由了一种新的交换机类型"x-delayed-message",这个交换机发送消息。可以用如下方法设置延时发送
// 通过插件的方式,创建 延时交换机
@Bean
public Exchange delayExchange(){
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-delayed-type", "direct");
return new CustomExchange("delayExchange", "x-delayed-message", true, false, args); }
// 普通队列
@Bean
Queue delayQueue() {
return new Queue("delayQueue");
}
// 发送消息的时候,消息头添加 x-delay字段 指定延时时间
@GetMapping("/delayByMsg")
public void delayTask(@RequestParam(value = "time") Long time) {
logger.info("我是线程" + Thread.currentThread().getName());
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 持久
String msg = "测试延时发送--发送消息的时候指定延时-----";
// 方式1 设置 在properties 的参数里面 (超过Int范围,编译报错)
//properties.setDelay(time);
//Message message = new Message(msg.getBytes(),properties);
//provide.send("delayExchange", "delay_key", message);
// 方式2 直接设置header 其实设置properties 就是header,本质上是一样的(超过Int范围,编译,运行都不会有异常,,但是延时不生效,队列会立即收到消息)
rabbitTemplate.convertAndSend("delayExchange", "delay_key", msg, (message) -> {
message.getMessageProperties().setHeader("x-delay", time); //延迟9秒
return message;
});
}
CustomExchange 自定义交换机,
在使用到延迟交换机插件的时候,,,我们使用插件新添加了一个x-delayed-message类型的交换机。 但是在Rabbitadmin中并没有这个类型的交换机。然后找到了这个CustomExchange 它和其它交换机相同,实现了AbstructExchange。 唯一的区别是没有指定type类型。type类型可以自定义,这样我们就可以通过构造方法自定义交换机的类型。
两种延时方式的对比。
- 设置队列ttl,然后转发死信交换机。。处理灵活,对一类消息统一管理,适合队列里面同一类消息,ttl相同,只需要设置队列ttl就可以了。
- 设置消息的ttl,单独的某一类,个别消息的单独设置的情况。 超出ttl任然给到死信交换机
- 延迟交换机插件, 使用这个方法不需要死信交换机和死信队列,消息是在延迟时间到了之后,才会给到队列。
两种方法共同点。
延时时间的数据类型是Int 单位ms。。在设置队列的ttl的时候,超过Int范围会编译报错。
设置消息的ttl大于Int。有两种情况,设置了对列的ttl。会按照队列的ttl。没有设置队列的ttl。。???? 延时会不会超过int范围。。
延迟交换机的方式,两种方式 设置消息的properties的Delay参数,延时超过int编译报错,,直接设置header,编译,运行均没有异常,只是超过int延时不生效。
几个坑
- jiva这边修改了交换机的配置,然后需要在rabbitmq上先把exchange 解绑,然后再运行否则原先的配置不会删除,新的配置也会添加,导致都会生效。
- 在调试延时队列的时候,springboot启动报错。有可能的原因是,修改了rabbitmq的Config, 修改了队列的ttl。 然后再rabbitmq的管理后台没有删除队列,导致运行时候会添加相同的队列,但是ttl不同,造成冲突。。。解决办法。。如果修改了队列配置,交换机配置。。。在启动sprigboot之前要先删除掉原来配置,,,因为你启动的时候会再去声明交换机和队列。