消息重复消费
消费者在消费MQ 中的消息时,MQ 已经把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故MQ 未收到确认消息,该消息会重新发给其他消费者,或网络重新连接后再次发给该消费者,但是实际上该消息已被消费过了,造成消费者重复消费同一条消息。
解决思路
MQ 消费者的幂等性的解决一般使用全局ID 或者写个唯一标识,如时间戳、UUID 或 MQ的ID等,每次消费消息时用ID 判断该消息是否已经消费过。
业界主流的幂等性有两种操作:
曾经后端系统是使用 redis 来存储定时轮询,而 redis 只能用List 做一个简单的消息队列,并不能实现优先级的场景。所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是VIP客户的订单,就给一个相对较高的优先级,否则默认优先级。
Map<String , Object> params = new HashMap<>();
params.put("x-max-priority", 10);//官方允许是0-255 之间 ,此处设置10 允许优先级范围为0-10 不要设置过大,浪费CPU与内存
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
for (int i = 0; i < 10; i++) {
String message = "info"+(i+1);
if((i+1) == 5){
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder().priority(5).build();
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
}else{
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
}
}
先运行生产者发消息,然后再运行消费者,消息info5 因为设置了优先级,优先被消费了。
RabbitMQ 从3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到响应的消息时才会被加载到内存中,它的一个重要设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机或者由于维护而关闭等)而致使长时间内不能消费消息造成堆积是,惰性队列就很有必要了。
默认情况下,当消费者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会消耗较长的时间,也会阻塞队列的操作,进而无法接受新的消息。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode","lazy");
channel.queueDeclare("myqueue",false,false,false,args);
在发送1百万条消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用1.5MB。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上,保证服务的可用性。
创建一个队列,并发送一个消息
然后通过命令把节点1停止,发现自动又备份到节点3上。
HAProxy 提供高可用性、负载均衡及基于 TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案。HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。
因为减少网络延迟,两个地方(比如北京和上海)都有MQ服务,但是需要数据同步,就需要Federation 插件解决这个问题。
需要保证每台节点单独运行
在每台机器上开启 Federation 相关插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.1.131");
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fed_exchange", BuiltinExchangeType.DIRECT);
channel.queueDeclare("node2_queue", true, false, false, null);
channel.queueBind("node2_queue", "fed_exchange", "routeKey");
//声明 接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody()));
};
//取消消息时的回调
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息消费被中断");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
联邦队列可以在多个 Broker 节点(或者集群)之间为单个队列提供负载的功能。一个联邦队列可以连接一个或多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息的需求。
跟 Federation 具备的数据转发功能类似,Shovel 够可靠、持续地从一个 Broker 中的队列(作为源端,即 source)拉取数据并转发至另一个 Broker 中的交换器(作为目的端,即 destination)。作为源端的队列和作为目的端的交换器可以同时位于同一个 Broker,也可以位于不同的 Broker上。Shovel 可以翻译为“铲子”,是一种比较形象的比喻,这个“铲子”可以将消息从一方“铲到”另一方。Shovel 行为就像优秀的客户端应用程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。