以DirectExchange为例:
@Bean
DirectExchange directExchange() {
Map<String, Object> map = new HashMap<>();
//将exchange交换机设置为备份交换机
map.put("alternate-exchange", "exchange");
/**
* 参数:
* name:交换机的名称
* durable:设置是否持久化。持久化可以将交换机存盘,在服务器重启的时候不会丢失相关信息
* autoDelete:设置是否自动删除,自动删除的前提是至少有一个队列或者交换机与这个交换机绑定,之后所有与这个交换机绑定的队列或者交换机都与此解绑
*/
DirectExchange directExchange = new DirectExchange("fanoutExchange", true, false, map);
return directExchange;
}
备份交换机:
举个例子,有两个交换机normalExchange和myAe,normalExchange为direct类型的,绑定了normalQueue这个队列,绑定键为normalKey,另一个交换机myAe为fanout类型的,绑定了unroutedQueue这个队列,同时设置myAe为normalExchange的备份交换机
如果发送一条消息到normalExchange上,当路由键等于normalKey的时候,消息能正确路由到normalQueue这个队列中。如果路由键设置为其他值,消息不能被正确地路由到与normalExchange绑定的队列上,此时就会发送给myAe交换机,进而发送到unroutedQueue这个队列
消息被重新发送到备份交换机时的路由键和从生产者发出的路由键是一样的
1)、设置消息的TTL
有两种方法可以设置消息的TTL。第一种方法是通过队列属性x-message-ttl
(单位毫秒)设置,队列中所有消息都有相同的过期时间。第二种方法是对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的TTL值时,就会变成死信,消费者将无法再收到该消息
如果不设置TTL,则表示此消息不会过期;如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃
Queue构造参数详解:
@Bean
public Queue ttlQueue() {
Map<String, Object> map = new HashMap<>();
//设置队列中消息的过期时间为30分钟
map.put("x-message-ttl", 1800000);
/**
* 参数:
* name:队列的名称
* durable:设置是否持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息
* exclusive:设置是否排他。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除
* autoDelete:设置是否自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除
*/
Queue ttlQueue = new Queue("ttlQueue", true, false, false, map);
return ttlQueue;
}
2)、设置队列的TTL
通过队列属性x-expires
可以控制队列被自动删除前处于未使用状态的时间。未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也为调用Basic.Get命令
RabbitMQ会确保在过期时间到达后将队列删除,但是不确保删除的动作有多及时。在RabbitMQ重启后,持久化的队列的过期时间会被重新计算
x-expires
以毫秒为单位,不能设置为0
3)、死信队列
DLX,全称为Dead-Letter-Exchange称之为死信队列,当消息在一个队列中变成死信之后,它能重新被发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称之为死信队列
消息变成死信一般是由于以下几种情况:
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列存在死信时,RabbitMQ就会自动地将这个消息重新发不到设置的DLX上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消费者以进行相应的处理
通过x-dead-letter-exchange
属性为这个队列添加DLX
也可以通过x-dead-letter-routing-key
属性为这个DLX指定路由键,如果没有特殊指定,则使用原队列的路由键
举个例子,有两个交换机exchange.normal和exchange.dlx,exchange.normal为fanout类型的,绑定了queue.noraml这个队列,另一个交换机exchange.dlx为direct类型的,绑定了queue.dlx这个队列,绑定键为routingkey,queue.noraml这个队列设置了过期时间为10s,DLX为交换机exchange.dlx,DLK为routingkey
生产者发送一条携带路由键为rk的消息,经过交换机exchange.normal顺利地存储到队列queue.noraml中。由于队列queue.noraml设置了过期时间10s,在10s内没有消费者消费这条消息,那么判定这条消息为过期。由于设置了DLX,过期之时,消息被丢给交换机exchange.dlx,这时找到与exchange.dlx匹配的队列queue.dlx,最后消息被存储在queue.dlx这个死信队列中
DLX可以处理异常情况下,消息不能够被消费者正确消费而被置入死信队列中的情况,后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统
4)、延迟队列
延迟队列存储的对象是对应的延迟消息,所谓延迟消息是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定事件后,消费者才能拿到这个消息进行消费
延迟队列的使用场景有很多,比如:
RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过DLX和TTL模拟出延迟队列的功能
假设一个应用中需要将每条消息都设置为10秒的延迟,生产者通过exchange.normal这个交换机将发送的消息存储在queue.normal这个队列中。消费者订阅的并不是queue.normal这个队列,而是queue.dlx这个队列。当消息queue.normal这个队列中过期之后被存入queue.dlx这个队列中,消费者就恰巧消费到了延迟10秒的这条消息
在真实应用中,对于延迟队列可以根据延迟时间的长短分为多个等级
5)、优先级队列
优先级队列具有高优先级的队列具有最高的优先权,优先级高的消息具备优先被消费的特权
可以通过设置队列的x-max-priority
属性来实现
消息的优先级默认最低为0,最高位队列设置的最大优先级。优先级高的消息可以被优先消费,如果在消费者的消费速度远大于生产者的速度且Broker没有消息堆积的情况下,对发送的消息设置优先级没有什么实际意义
持久化可以提高RabbitMQ的可靠性,以防在异常情况下的数据丢失。RabbitMQ的持久化分为三分部分:交换机的持久化、队列的持久化和消息的持久化
如果交换机不设置持久化,那么在RabbitMQ服务重启之后,相关的交换机元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换机中了
如果队列不设置持久化,那么在RabbitMQ服务重启之后,相关队列的元数据会丢失,此时数据也会丢失
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失
可以将所有的消息都设置为持久化,但是这样会严重影响RabbitMQ的性能。对于可靠性不是那么高的消息可以不采用持久化以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做一个权衡
在使用org.springframework.amqp.rabbit.core.RabbitTemplate发送消息的时候,默认情况下发送消息为持久化的
一般情况下,会通过这种方式发送消息:
rabbitTemplate.convertAndSend(exchange, routeKey, message);
其中调用了convertAndSend(String exchange, String routingKey, final Object object)方法:
public void convertAndSend(String exchange, String routingKey, final Object object) throws AmqpException {
convertAndSend(exchange, routingKey, object, (CorrelationData) null);
}
接着调用了convertAndSend(String exchange, String routingKey, final Object object, CorrelationData correlationData)方法
@Override
public void convertAndSend(String exchange, String routingKey, final Object object,
@Nullable CorrelationData correlationData) throws AmqpException {
send(exchange, routingKey, convertMessageIfNecessary(object), correlationData);
}
调用send方法的时候调用了convertMessageIfNecessary(final Object object)
protected Message convertMessageIfNecessary(final Object object) {
if (object instanceof Message) {
return (Message) object;
}
return getRequiredMessageConverter().toMessage(object, new MessageProperties());
}
new了一个MessageProperties对象,它的持久化策略是MessageDeliveryMode.PERSISTENT,因此默认消息是持久化的
public class MessageProperties implements Serializable {
public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
public static final Integer DEFAULT_PRIORITY = 0;
RabbitMQ针对生产者确认这个问题,提供了两种解决方式:
1)、事务机制
RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit、channel.txRollback。channel.txSelect用于将当前的信道设置成事务模式,channel.txCommit用于提交事务,channel.txRollback用于事务回滚。在通过channel.txSelect方法开启事务之后,便可以发布消息RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚
事务提交:
事务回滚:
事务机制性能太差,RabbitMQ提供了一个改进方案,即发送方确认机制
2)、发送方确认机制
生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是持久化的,那么确认消息会在消息写入磁盘之后发出。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息都已经得到了处理
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续放下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用程序便可以通过回调方式来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack命令
生产者通过调用channel.confirmSelect方法将信道设置为confirm模式,之后RabbitMQ会返回Confirm.Select-Ok命令表示同意生产者将当前信道设置为confirm模式。所有被发送的后续消息都被ack或者nack一次,不会出现一条消息既被ack又被nack的情况,并且RabbitMQ也并没有对消息被confirm的快慢做任何保证
1)、通过实现ConfirmCallback接口,用于监听Broker端给我们返回的确认消息
添加配置
spring.rabbitmq.publisher-confirms=true
@Component
public class RabbitConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback {
Logger log = LoggerFactory.getLogger(RabbitConfirmCallbackConfig.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息的唯一标识:{},成功发送至Exchange", correlationData.getId());
} else {
log.error("消息的唯一标识:{},发送至Exchange失败,失败原因:{}", correlationData.getId(), cause);
}
}
}
2)、通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发(比如根据发送消息时指定的路由键找不到队列时会触发)
添加配置
#启用强制消息,设置为false收不到Publisher Return机制返回的消息(默认为true)
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.publisher-returns=true
@Component
public class RabbitReturnCallbackConfig implements RabbitTemplate.ReturnCallback {
Logger log = LoggerFactory.getLogger(RabbitReturnCallbackConfig.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setReturnCallback(this);
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息:{}发送队列失败,消息使用的exchange:{},消息使用的routingKey:{}", message, exchange, routingKey);
}
}
3)、消息接收确认(com.rabbitmq.client.Channel中的方法)
1)确认模式
添加配置
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@Component
@RabbitListener(queues = RabbitConfig.QUEUE)
public class ReceiveConfirmTestListener {
Logger log = LoggerFactory.getLogger(RabbitConfirmCallbackConfig.class);
@RabbitHandler
public void processMessage2(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
try {
log.info("接收到消息:" + message);
//TODO 业务逻辑
channel.basicAck(tag, false);
log.info("业务逻辑处理完成,应答RabbitMQ");
} catch (Exception e) {
log.error(e.getMessage());
channel.basicNack(tag, false, true);
log.info("业务逻辑,拒绝消息,要求RabbitMQ重新派发");
}
}
}
2)成功确认
//deliveryTag:该消息的index
//multiple:是否批量,设置为true一次性ack所有小于deliveryTag的消息
void basicAck(long deliveryTag, boolean multiple) throws IOException;
3)失败确认
//deliveryTag:该消息的index
//multiple:是否批量,设置为true一次性ack所有小于deliveryTag的消息
//requeue:被拒绝的是否重新入队
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
//deliveryTag:该消息的index
//requeue:被拒绝的是否重新入队
void basicReject(long deliveryTag, boolean requeue) throws IOException;
basicNack与basicReject的区别在于basicNack可以拒绝多条消息,而basicReject一次只能拒绝一条消息
4)消息拒绝后,再次发布消息
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
Connection可以用来创建多个Channel实例,但是Channel实例不能在线程间共享,应用程序应该为每一个线程开辟一个Channel。某些情况下Channel的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响发送方确认机制的运行,所以多线程间共享Channel实例是非线程安全的
RabbitMQ的消费模式分两种:推模式和拉模式。推模式采用Basic.Consume进行消费,而拉模式则是调用Basic.Get进行消费
1)、推模式:通过channel.basisConsume方法,以持续订阅的方式来消费信息
2)、拉模式:通过channel.basicGet方法可以单条地获取消息
一般消息中间件的消息传输保障分为三个层级:
RabbitMQ支持最多一次和最少一次,其中最少一次投递实现需要考虑以下几个方面的内容:
1)消息生产者需要开启事务机制或者publisher confirm机制,以确保消息可以可靠地传输到RabbitMQ中
2)消息生产者需要配合使用备份交换机来确保消息能够从交换机路由到队列中,进而能够保存下来而不会被丢弃
3)消息和队列都需要进行持久化处理,以确保RabbitMQ服务器在遇到异常情况时不会造成消息丢失
4)消费者在消费消息的同时需要将autoAck设置为false,然后通过手动确认的方式去确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失
最多一次的方法就无须考虑以上这些方面,生产者随意发送,消费者随意消费,不过专业那个很难确保消息不会丢失
恰好一次是RabbitMQ目前无法保障的。比如,消费者在消费完一条消息之后向RabbitMQ发送确认Basic.Ack命令,此时由于网络断开或者其他原因造成RabbitMQ并没有收到这个确认命令,那么RabbitMQ不会将此条消息标记删除。在重新连接之后,消费者还是会消费到这一条消息,这就造成了重复消费。再比如,生产者在使用publisher confirm机制的时候,发送完一条消息等待RabbitMQ返回确认通知,此时网络断开,生产者捕获到异常情况,为了确保消息可靠性选择重新发送,这样RabbitMQ中就有两条同样的消息,在消费的时候,消费者就会重复消费