1.概述
RabbitMQ有几个重要的概念:虚拟主机,交换机,队列和绑定
- 虚拟主机:一个虚拟主机持有一组交换机、队列和绑定,我们可以从虚拟主机层面的颗粒度进行权限控制
- 交换机:Exchange用于转发消息,它并不存储消息,如果没有Queue队列绑定到Exchange,它会直接丢弃掉生产者发来的数据。
交换机还有个关联的重要概念:路由键,消息转发到哪个队列根据路由键决定 - 绑定:就是绑定交换机和队列,它是多对多的关系,也就是说多个交换机可以绑同一个队列,也可以一个交换机绑多个队列
2.交换机
交换机有四种类型的模式Direct, topic, Headers and Fanout
2.1
Direct Exchage
Direct模式使用的是RabbitMQ的默认交换机,也是最简单的模式,适合比较简单的场景
如下图所示,使用Direct模式,我们需要创建不同的队列,而默认交换机则通过Routing key
路由键的值来决定转发到哪个队列,可以看到,路由键绑定队列是可以指定多个的
2.2 Topic Exchange
Topic模式主要是根据通配符匹配,也就类似于模糊匹配,当这种匹配模式和路由键匹配后交换机就能转发消息到指定队列
- 路由键为一串字符串,由句号(
.
)隔开,比如a.b.c
- (
*
)代表指定位置一个单词,(#
)代表零个或者多个单词,比如a.*.b.#
,表示a和b中间随意填个单词,b后面可以跟n个单词,比如a.x.b.c.d.e
Topic模式和Direct模式的区别在于交换机需要自己指定,路由键支持模糊匹配,例如:
rabbitTemplate.convertAndSend("topicExchange","a.x.b.d", " hello world!");
2.3 Headers Exchage
Headers也是根据规则匹配,但它不是根据路由键了,headers有个自定义匹配规则,它将匹配键值设在了消息的headers属性上,当这些键值对有一对或者全部匹配时,消息才会被投递到对应队列,这种模式效率相对较低,一般不推荐使用
2.4 Fanout Exchange
Fanout即为大名鼎鼎的广播模式了,它不需要管路由键,会把消息发给绑定它的全部队列,就算配置了路由键也会被忽略
3. 复杂情况
- 首先我们Direct模式,一个生产者一个消费者的情况,也就对应了一个发送者和一个队列A接收,这是没有疑问的,发送什么接收什么
- 当Direct模式,一个生产者发消息,开启多个消费者也就是多个相同queue,此时消息由多个消费者均匀分摊,不会重复消费(前提ack正常)
- 当Topic模式,一个交换机绑定两个队列,路由键有重叠关系,如下代码,此时指定路由键
topic.message
发送消息,队列queueMessage
和queueMessages
都能接收到相同消息,也就是说,topic模式可以实现类似于广播模式的形式,甚至更加灵活,它能否转发到消息由路由键决定。 - 相比于Fanout模式,我们如果要对消费者队列分组发送,我们需要指定不同的路由键;而Fanout模式则需要指定不同的交换机和队列绑定,实际使用结合实际情况
4. Demo 代码
4.1 pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
4.2 配置文件
spring:
rabbitmq:
# addresses: 10.111.36.4
addresses: localhost
port: 5672
username: guest
password: guest
virtual-host: /saas-mail
mail-queue: mail_queue
publisher-confirms: true
publisher-returns: true
template:
mandatory: true
listener:
simple.acknowledge-mode: manual
simple:
concurrency: 5
max: 10
spring.rabbitmq.publisher-confirms
为true,表示生产者消息发出后,MQ的broker接收到了消息,发送回执表示确认接收,不设置则可能导致消息丢失spring.rabbitmq.publisher-returns
为true,表示当消息不能到达MQ的Broker端,,则使用监听器对不可达的消息做后续处理,这种一般是路由键没配好,或MQ宕机才可能发生spring.rabbitmq.template.mandatory
当上面两个为true时,这个一定要配true,否则上面两个不起作用spring.rabbitmq.listener.simple.acknowledge-mode
这个为manual
表示手工确认,实际生产应该设为手工,才能保证你的业务是处理完成的,注意业务的幂等性,可重复调用,手工确认代码如下例子
4.3 java 代码
- rabbit 配置类
/**
* @Description rabbit provider
* @Author yanghanwei
* @Mail yanghanwei@geotmt.com
* @Date 17:58 2019-11-25
* @Version v1
**/
@Configuration
public class DirectRabbitConfig {
private static final Logger logger = LoggerFactory.getLogger(DirectRabbitConfig.class);
@Value("${spring.rabbitmq.mail-queue}")
private String queueName;
/**
* 邮件交换机名称
*/
public static final String mail_Direct_Exchange = "mailDirectExchange";
/**
* 邮件路邮键
*/
public static final String mail_Direct_Routing = "mailDirectRouting";
@Autowired
private CachingConnectionFactory connectionFactory;
@Bean
public Queue testDirectQueue() {
//true 是否持久化
return new Queue(queueName, true);
}
/**
* Direct交换机 起名:mailDirectExchange
*
* @return
*/
@Bean
DirectExchange directExchange() {
return new DirectExchange("mailDirectExchange", true, false);
}
/**
* 绑定 将队列和交换机绑定, 并设置用于匹配键:mailDirectRouting
*
* @return
*/
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(testDirectQueue()).to(directExchange()).with("mailDirectRouting");
}
@Bean
public RabbitTemplate rabbitTemplate() {
/**
* 若使用confirm-callback或return-callback,必须要配置publisherConfirms或publisherReturns为true
* 每个rabbitTemplate只能有一个confirm-callback和return-callback,如果这里配置了,那么写生产者的时候不能再写confirm-callback和return-callback
* 使用return-callback时必须设置mandatory为true,或者在配置中设置mandatory-expression的值为true
*/
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
/**
* 如果消息没有到exchange,则confirm回调,ack=false
* 如果消息到达exchange,则confirm回调,ack=true
* exchange到queue成功,则不回调return
* exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
*/
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String msg) {
if (ack) {
logger.info("Message send success : correlationData({}),cause({})", correlationData, msg);
} else {
logger.info("Message send failure : correlationData({}),cause({})", correlationData, msg);
}
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.info("Message miss : exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message);
}
});
return rabbitTemplate;
}
}
队列属性
- queue :队列的名字
- durable :为true表示队列中数据持久化到磁盘,可以防止mq宕机重启数据丢失
- exclusive :为true表示排他性,只允许一个当前连接访问该队列,当前已连接就不允许新的连接进入否则报错,当连接断开当前队列会销毁
- autoDelete :为true表示自动删除,当没有Connection连接到队列的时候,会自动删除
- arguments :这个参数用来添加一些额外参数的,如下图片
- 比如添加
x-message-ttl
为5000,则表示消息超过5秒没被处理就会超时过期; x-expires
设置120000表示队列在2分钟内没被消费则被删除;x-max-length
,x-max-length-bytes
表示传送数据的最大长度和字节数x-dead-letter-exchange
,x-dead-letter-routing-key
表示死信交换机和死信路由,放在需要过期或处理失败的队列属性中,这些数据会转发到死信队列存储起来,创建普通的交换机和队列绑定,把交换机名填到x-dead-letter-exchange
的值,填写路由键要符合死信队列的路由键x-max-priority
,表示设置优先级,范围为0~255,只有当消息堆积的时候,这个优先级才有意义,数字越大优先级越高x-queue-mode
当为lazy
,表示惰性队列,3.6.0之后才被引入的概念,相比默认的模式,惰性队列模式会将生产者产生的消息直接存到磁盘中,这当然会增加IO开销,但适合应对大量消息堆积的情况;因为当大量消息堆积时,内存也不够存放,会将消息转存到磁盘,这个过程也是比较耗时且过程中不能接收新的消息。如果需要将普通队列转换成惰性队列需要将原来的队列删除,重新创建个惰性队列绑定。
交换机属性
- exchange :交换机名称
- type :交换机类型
- durable :持久化,同队列
- autoDelete :是否自动删除,同队列
- internal :若为true,表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定。
- arguments :额外参数,目前只有个
alternate-exchange
,表示当生产者发送消息到这个交换机,路由不到该交换机的队列,则会尝试这个参数指定的交换机进行路由,若路由键匹配,则路由到alternate-exchange
指定的队列,相当于转发了,刚好和上一个参数internal
配合,若不想本交换机起到路由队列的作用,可以设置internal
为true,把消息都转发到alternate-exchange
指定的交换机,由该交换机来路由指定队列,
- rabbit 生产者
/**
* @Description 生产者
* @Author yanghanwei
* @Mail yanghanwei@geotmt.com
* @Date 11:51 2019-11-26
* @Version v1
**/
@Component
public class RabbitSender {
private static final Logger logger = LoggerFactory.getLogger(RabbitSender.class);
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(MailQueueMessageVo mailQueueMessageVo) {
logger.info("======= RabbitMQ ======= sender message: {}", mailQueueMessageVo);
rabbitTemplate.convertAndSend(DirectRabbitConfig.mail_Direct_Exchange, DirectRabbitConfig.mail_Direct_Routing, JSONObject.toJSONString(mailQueueMessageVo));
}
}
rabbit 消费者(监听)
/**
* @Description mq 监听
* @Author yanghanwei
* @Mail yanghanwei@geotmt.com
* @Date 09:42 2019-12-02
* @Version v1
**/
@Configuration
public class AmqpListener {
private static final Logger logger = LoggerFactory.getLogger(AmqpListener.class);
@Value("${spring.rabbitmq.mail-queue}")
private String mailQueue;
@Autowired
private SendMailUtils sendMailUtils;
@Autowired
private AckMailMsgService ackMailMsgService;
@Bean
public SimpleMessageListenerContainer messageContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
// 可监听多个队列
container.setQueues(new Queue(mailQueue));
// 设置当前消费者数量为1
container.setConcurrentConsumers(1);
// 设置最大的消费者数量为5
container.setMaxConcurrentConsumers(5);
// 是否重回队列:否
container.setDefaultRequeueRejected(false);
// 设置自动签收模式
container.setAcknowledgeMode(AcknowledgeMode.AUTO);
// 自定义消费者标签生成策略(队列名+uuid)
container.setConsumerTagStrategy(new ConsumerTagStrategy() {
@Override
public String createConsumerTag(String queue) {
return queue + "_" + UUID.randomUUID().toString();
}
});
/*
* 第一种监听方式:
* 设置消息监听,ChannelAwareMessageListener是Spring AMQP提供的匿名的监听接口
* 如果有一条消息发送到队列,则会被监听器监听到,进入onMessage方法
*/
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
try{
// send(sendMailUtils, msg);
logger.info("msg:{}",msg)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
}catch (Exception e){
logger.error(" send mail failed" + e.getMessage(), e);
// 失败补偿,当下立即补偿一次,剩下的定时任务扫描,失败队列补偿,最多重试3次
// Long id = ackMailMsgService.insert(new AckMailMsgVo().setMailMsgStr(msg));
// 补偿消费
// compensationConsumption(sendMailUtils, ackMailMsgService, msg, id);
// 第三个参数: true 重新放进队列, false 抛弃此条消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
});
// 第二种监听方式: 适配器模式,自定义一个delegate委托进行监听
// container.setMessageListener(new MessageListenerAdapter(new MessageDelegate()));
return container;
}
/**
* @Description mq 监听
* @Author yanghanwei
* @Mail yanghanwei@geotmt.com
* @Date 10:26 2019-12-02
* @Version v1
**/
public class MessageDelegate {
public void handleMessage(String msg) {
try {
send(sendMailUtils, msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}