这是RabbitMQ最为经典的队列类型。在单机环境中,拥有比较高的消息可靠性。
经典队列可以选择是否持久化(Durability)以及是否自动删除(Auto delete)两个属性。
Durability有两个选项,Durable和Transient。 Durable表示队列会将消息保存到硬盘,这样消息的安全性更高。但是同时,由于需要有更多的IO操作,所以生产和消费消息的性能,相比Transient会比较低。
Auto delete属性如果选择为是,那队列将在至少一个消费者已经连接,然后所有的消费者都断开连接后删除自己。
经典队列不适合积累太多的消息。如果队列中积累的消息太多了,会严重影响客户端生产消息以及消费消息的性能。因此,经典队列主要用在数据量比较小,并且生产消息和消费消息的速度比较稳定的业务场景。比如内部系统之间的服务调用。
仲裁队列,是3.8引入的一个新队列类型;仲裁队列相比Classic经典队列,在分布式环境下对消息的可靠性保障更高。
Quorum是基于Raft一致性协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式设计的。简单理解就是quorum队列中的消息需要有集群中多半节点同意确认后,才会写入到队列中。
Quorum队列更适合于 队列长期存在,并且对容错、数据安全方面的要求比低延迟、不持久等高级队列更能要求更严格的场景。例如 电商系统的订单,引入MQ后,处理速度可以慢一点,但是订单不能丢失。
Quorum不适合的场景如下:
Spring创建仲裁队列需要设置参数“-x-queue-type”为“quorum”
@Configuration
public class QuorumConfig {
public final static String QUEUE_TYPE = "x-queue-type";
public final static String QUEUE_TYPE_VAL = "quorum";
public final static String QUEUE_NAME = "quorumQueue";
@Bean
public Queue quorumQueue() {
HashMap<String, Object> params = new HashMap<>();
params.put(QUEUE_TYPE,QUEUE_TYPE_VAL);
Queue queue = new Queue(QUEUE_NAME, true, false, false, params);
return queue;
}
}
Rabbit Client创建Quorum队列:
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","quorum");
//声明Quorum队列的方式就是添加一个x-queue-type参数,指定为quorum。默认是classic
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
Quorum队列的消息是必须持久化的,所以durable参数必须设定为true,如果声明为false,就会报错。同样,exclusive参数必须设置为false。这些声明,在Producer和Consumer中是要保持一致的。
Spring AMQP目前还不支持创建Stream队列;只能使用原生API创建
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","stream");
params.put("x-max-length-bytes", 20_000_000_000L); // maximum stream size: 20 GB
params.put("x-stream-max-segment-size-bytes", 100_000_000); // size of segment files: 100 MB
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
Stream队列的durable参数必须声明为true,exclusive参数必须声明为false。
x-max-length-bytes 表示日志文件的最大字节数, x-stream-max-segment-size-bytes 每一个日志文件的最大大小。这两个是可选参数,通常为了防止stream日志无限制累计,都会配合stream队列一起声明。
消费者:
Map<String,Object> consumeParam = new HashMap<>();
consumeParam.put("x-stream-offset","last");
channel.basicConsume(QUEUE_NAME, false,consumeParam, myconsumer);
x-stream-offset的类型:
Stream队列产品目前不够成熟,目前用的最多的还是Classic经典队列。RabbitMQ目前主推的是Quorum队列;
有以下三种情况,RabbitMQ会将一个正常消息转成死信
消息被消费者确认拒绝。消费者把requeue参数设置为true(false),并且在消费后,向 RabbitMQ返回拒绝。channel.basicReject或者channel.basicNack。
消息达到预设的TTL时限还一直没有被消费。
消息由于队列已经达到最长长度限制而被丢掉
TTL即最长存活时间 Time-To-Live 。消息在队列中保存时间超过这个TTL,即会被认为死亡。死亡的消息会被丢入死信队列,如果没有配置死信队列的话,RabbitMQ会保证死了的消息不会再次被投递,并且在未来版本中,会主动删除掉这些死掉的消息。
声明队列时、设置"x-message-ttl"值;
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
消息被作为死信转移到死信队列后,header中还会加上第一次成为死信的三个属性,并且这三个属性在以后的传递过程中都不会更改。具体可以调试去看看;
代码:
死信交换机、队列:
@Configuration
public class DeadConfig {
public final static String DEAD_EXCHANGE = "deadExchange";
public final static String DEAD_QUEUE_NAME = "deadQueue";
@Bean
public FanoutExchange deadExchange() {
FanoutExchange directExchange = new FanoutExchange(DEAD_EXCHANGE);
return directExchange;
}
@Bean
public Queue deadQueue() {
Queue queue = new Queue(DEAD_QUEUE_NAME);
return queue;
}
@Bean
public Binding deadBinding(FanoutExchange deadExchange, Queue deadQueue) {
return BindingBuilder.bind(deadQueue).to(deadExchange);
}
}
发送者:
@Controller
public class MessageTx {
@Autowired
private MessageService messageService;
@GetMapping("/sendDeadMsg")
@ResponseBody
public String sendMoreMsgTx(){
//发送10条消息
for (int i = 0; i < 10; i++) {
String msg = "msg"+i;
System.out.println("发送消息 msg:"+msg);
// xiangjiao.exchange 交换机
// xiangjiao.routingKey 队列
messageService.sendMessage(MessageConfig.EXCHANGE_NAME, "", msg);
//每两秒发送一次
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "send ok";
}
}
@Slf4j
@Component
public class MessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(String exchange,String routingKey,Object msg) {
// 暂时关闭 return 配置
//rabbitTemplate.setReturnCallback(this);
//发送消息
rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}
}
消费者:
public class MessageConsumer {
// @RabbitHandler : 标记的方法只能有一个参数,类型为String ,若是传Map参数、则需要传入map参数
// @RabbitListener:标记的方法可以传入Channel, Message参数
@RabbitListener(queues = MessageConfig.MESSAGE_QUEUE_NAME)
public void listenObjectQueue(Channel channel, Message message, String msg) throws IOException {
System.out.println("接收到object.queue的消息" + msg);
System.out.println("消息ID : " + message.getMessageProperties().getDeliveryTag());
try {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
System.out.println("拒绝消息 , tag = " + message.getMessageProperties().getDeliveryTag());
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch (IOException exception) {
//拒绝确认消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
//拒绝消息
// channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
注入容器:
@Configuration
public class MessageConfig {
public final static String EXCHANGE_NAME = "deadMessageTestExchange";
public final static String MESSAGE_QUEUE_NAME = "deadMessageTestQueue";
public final static String MESSAGE_ROUTE_KEY = "deadMessageTestRoutingKey";
public final static String DEAD_EXCHANGE_KEY = "x-dead-letter-exchange";
@Bean
public FanoutExchange deadMessageTestExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
@Bean
public Queue deadMessageTestQueue() {
HashMap<String, Object> params = new HashMap<>();
params.put(DEAD_EXCHANGE_KEY, DeadConfig.DEAD_EXCHANGE);
return new Queue(MESSAGE_QUEUE_NAME, true, false, false, params);
}
@Bean
public MessageConsumer deadMessageTestConsumer() {
return new MessageConsumer();
}
@Bean
public Binding messageBinding(Queue deadMessageTestQueue, FanoutExchange deadMessageTestExchange) {
return BindingBuilder.bind(deadMessageTestQueue).to(deadMessageTestExchange);
}
}
RabbitMQ有提供插件使用延迟队列, 另外可借助 死信队列 实现延迟队列;
实现思路:
懒队列会尽可能早的将消息内容保存到硬盘当中,并且只有在用户请求到时,才临时从硬盘加载到RAM内存当中。 可解决部分消息积压问题、(海量消息积压,RabbitMQ存不下就得使用分布式存储消息)
适用的一些场景:
默认情况下,RabbitMQ接收到消息时,会保存到内存以便使用,同时把消息写到硬盘。但是,
消息写入硬盘的过程是会阻塞队列的。RabbitMQ虽然做了优化,但是在长队列中表现不是很理想,所以有了懒队列、 以磁盘IO为代价解决消息积压问题;
SpringBoot懒队列声明方式:
@Configuration
public class LazyQueueConfig {
@Bean
public Queue lazyQueue() {
HashMap<String, Object> params = new HashMap<>();
params.put("x-queue-mode", "lazy");
return new Queue("lazyQueue", true, false, false, params);
}
}
原生API方式:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
懒队列适合消息量大且长期有堆积的队列,可以减少内存使用,加快消费速度。但是这是以大量消耗集群的网络及磁盘IO为代价的。
分布式环境下,是不允许单点故障存在,需要保证高可用, 因此需要集群环境保证高可用,另外若存在海量消息,还需要保证存放得下、即分布式存储;
先看看哪些情况下,会存在丢失消息?
1,2,4步骤是可能丢消息的,因为三个步骤都是跨网络的;
消息若是只存内存中,则宕机会丢失消息, 因此队列需要开启持久化,durable参数、默认创建队列,durable都会为true; 而Quorum和Stream队列默认都是开启持久化;
普通集群模式,消息是分散存储的,不会主动进行消息同步了,是有可能丢失消息的。而镜像模式集群,数据会主动在集群各个节点当中同步,这时丢失消息的概率不会太高。
消费者确认,分为自动确认,手动确认;若是自动确认,则消息处理完,会返回确认ack;若是处理出现异常, 则会重新入队,再次处理, 因此存在重复消费问题;
若是手动确认,消息处理过程中使用channel#basicAck, basicNack, basicReject返回确认或拒绝;SpringBoot配置文件中通过属性spring.rabbitmq.listener.simple.acknowledge-mode需要设置mutual手动确认;
SpringBoot配置文件中通过属性spring.rabbitmq.listener.simple.acknowledge-mode 进行指定。可以设定为 AUTO 自动应答; MANUAL 手动应答;NONE 不应答;
当消费者消费消息处理业务逻辑时,如果抛出异常,或者不向RabbitMQ返回响应,默认情况下,RabbitMQ会无限次数的重复进行消息消费。
处理幂等问题,要设定RabbitMQ的重试次数。在SpringBoot集成RabbitMQ时,可以在配置文件
中指定spring.rabbitmq.listener.simple.retry开头的一系列属性,来制定重试策略。
需要在业务上处理幂等问题, 处理幂等问题的关键是要给每个消息一个唯一的标识;虽然RabbitMQ会给每条消息带上MessageId (处理幂等问题的关键是要给每个消息一个唯一的标识);
SpringBoot框架集成RabbitMQ后,可以给每个消息指定一个全局唯一的MessageID,在消费者端针对MessageID做幂等性判断。
//发送者
Message message2 = MessageBuilder.withBody(message.getBytes()).setMessageId(UUID.randomUUID().toString()).build();
rabbitTemplate.send(message2);
//消费者获取MessageID,自己做幂等性判断
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
...
}
可为了业务上的方便,再封装一层, 专门用来放入消息ID, 否则设置ID的代码随处可见;
RabbitMQ中保证顺序的方法是 单队列+单消息推送; 若是多队列的情况下,RabbitMQ没有很好的解决方案;
个人思考:如果RabbitMQ架构上很难处理,可以通过业务设置保证顺序, 即给每条消息设置序号, 消费时、查询数据库之前的消息是否处理完,若没有查到,则等待一会, 若查得到,则处理消息,处理完后,把消息id + 序号 放入数据库代表已经处理完;
bbitMQ一直以来都有一个缺点,就是对于消息堆积问题的处理不好。当RabbitMQ中有大量消息堆积时,整体性能会严重下降。而目前新推出的Quorum队列以及Stream队列,目的就在于解决这个核心问题。目前大部分企业还是围绕Classic经典队列构建应用。因此,在使用RabbitMQ时,还是要非常注意消息堆积的问题。尽量让消息的消费速度和生产速度保持一致。