RabbitMQ进阶

消息何去何从

mandatory 和immediate 是在消息传递过程中不可达目的地时返回消息给生产的开关. rabbitMQ还提供了Altername Exchange(备份交换器),用以存放未被路由成功的消息存储,防止返回给生产者.

mandatory

当出现消息无法路由到队列时,mandatory参数值不同,处理方式不同

  • 为true:
    RabbitMQ 将调用Basic.Return 命令将消息返回给生产者,客户端处理示例
		channel.basicPublish(EXCHANGE_NAME, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "mandatory test".getBytes());
		channel.addReturnListener(new ReturnListener() {
			@Override
			public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
				String message = new String(body);
				log.info(messages);
			}
		});
  • 为false:
    直接丢弃消息

immediate RabbitMQ3.0开始不支持这个参数

当交换器将消息路由到队列上,发现队列上没有消费者时,immediate 参数设置不同,处理方式不同

  • 为true: RabbitMQ将会调用Basic.return将消息返回给消费者.
  • 为false: 直接放入队列

备份交换器AE(Alternate Exchange)

在没有mandatory的情况下,可以使用AE实现消息存储

  • 备份交换器和队列声明
		Map<String, Object> map = new HashMap<>();
		map.put("alternate-exchange", "myAe");
		channel.exchangeDeclare("myAe", BuiltinExchangeType.FANOUT, true, false, map);
		channel.queueDeclare("unroutedQueue", true, false, false, null);
		channel.queueBind("unroutedQueue", "myAe", "");

备份交换器如果找不到适合的队列,消息丢失. 备份交换器的优先级高于mandatory 参数.

过期时间(TTL)

  • 消息的TTL
    两种方式设置消息过期时间,如果超过过期时间未被消费,消息就会变成"Dead Message",如果不设置标识消息不会过期,如果设置为0,标识如果此时可以直接投递到消费者,则直接投递,否则直接丢弃
  1. 设置队列属性
    在声明队列时添加参数 x-message-ttl 时间单位是毫秒,如果过期,消息直接从队列中丢弃
  2. 对消息本身设置
    在channel.basicPublish中加入expiration参数, 在投递消息时判断是否过期,如果过期则丢弃,不投递.

同时设置两者时,以时间短的为准

  • 队列的TTL
    通过在声明队列时添加参数 x-expires 控制队列被自动删除前处于未使用(没有任何消费者,队列没有被重新声明,并且没有被拉取过消息)状态的时间. 在重启RabbitMQ 后,队列的过期时间会被重算.

死信队列

DLX(Dead-Letter-Exchange) ,死信交换器,当消息在队列中变成死信之后,它可以被重新发送到另外一个交换器中,这个交换器就是DLX,和DLX绑定的队列称为死信队列.

死信来源:

  1. 消息被拒绝(Basic.Reject/Basic.Nack), 并且requeue参数为false
  2. 消息过期
  3. 队列达到最大长度
    可以用过在声明队列时添加参数 x-dead-letter-exchange 为队列添加DLX.
    为DLX指定路由键 通过添加参数 x-dead-letter-routing-key .
		channel.exchangeDeclare("exchange.dlx", "direct", true);
		channel.exchangeDeclare("exchange.normal", "fanout", true);
		Map<String, Object> queueMap = new HashMap<>();
		queueMap.put("x-message-ttl", 1000);
		queueMap.put("x-dead-letter-exchange", "exchange.dlx");
		queueMap.put("x-letter-routing-key", "routingkey");
		channel.queueDeclare("queue.normal", true, false, false, queueMap);
		channel.queueBind("queue.normal", "exchange.normal", "");

		channel.queueDeclare("queue.dlx", true, false, false, null);
		channel.queueBind("queue.dlx", "exchange.dlx", "routingkey");
		channel.basicPublish("exchange.normal", "rk", MessageProperties.PERSISTENT_TEXT_PLAIN, "dlx".getBytes());

延迟队列

使用场景: 支付场景中的30分钟内支付. 只能设备控制,指定时间后开始运行.
RabbitMQ本身是没有这种功能的,通过前面介绍的DLX和TTL模拟出队列的功能.
延时队列

优先级队列

优先级较高的消息具备优先被消费的特权,只有在broken中出现消息堆积的时候才会有效果,如果不出现消息堆积,设置优先级毫无意义.
通过设置队里的x-max-priority

		Map<String, Object> priMap = new HashMap<>();
		priMap.put("x-max-priority", 10); //队列里设定的最大优先级
		channel.queueDeclare("queue.priority", true, false, false, priMap);

		AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
		builder.priority(5); //消息的优先级,
		AMQP.BasicProperties properties = builder.build();
		channel.basicPublish("exchange_priority", "rk_priority", properties, "message".getBytes());

RPC实现

Remote Procedure Call 的简称,远程过程调用.
简单实现:

		//简单实现
		String callBackQueueName = channel.queueDeclare().getQueue();
		AMQP.BasicProperties props = new AMQP.BasicProperties().builder().replyTo(callBackQueueName).build();
		channel.basicPublish("","rpc_queue",props,messages.getBytes());

通过设置replyTo设置一个回调队列/ correlationId 用来关联request和RPC之后的回复(response). 以上缺陷: 为每个RPC请求创建一个回调队列,非常低效.

官方demo:

package com.example.demo.rabbitmq;

import com.rabbitmq.client.*;
import com.sun.org.apache.bcel.internal.generic.FADD;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @program: demo
 * @description:
 * @author: Jiliang.Lee
 * @create: 2019-09-16 17:31
 **/
@Slf4j
public class RpcServer {
	private static final String RPC_QUEUE_NAME = "rpc_queue";

	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost("127.0.0.1");
		connectionFactory.setPort(5672);
		connectionFactory.setUsername("root");
		connectionFactory.setPassword("root");
		Connection connection = connectionFactory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
		channel.basicQos(1);
		log.info("[x] Awaiting RPC requests");

		Consumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				AMQP.BasicProperties build = new AMQP.BasicProperties()
						.builder()
						.correlationId(properties.getCorrelationId())
						.build();
				String response = "";

				try {
					String message = new String(body, "UTF-8");
					int n = Integer.parseInt(message);
					System.out.println( " [ . ] fib( " + message + " ) " );
					response += fib(n);
				}catch (RuntimeException e){
					log.error(e.getMessage());
				}finally {
					channel.basicPublish("", properties.getReplyTo(), build, response.getBytes("UTF-8"));
					channel.basicAck(envelope.getDeliveryTag(), false);
				}
			}
		};
		channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
	}

	private static int fib(int n) {
		if(n==0) return 0;
		if(n==1) return 1;
		return fib(n - 1) + fib(n - 2);
	}
}

package com.example.demo.rabbitmq;

import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

/**
 * @program: demo
 * @description:
 * @author: Jiliang.Lee
 * @create: 2019-09-16 18:01
 **/
@Slf4j
public class RPCClient {
	private Connection connection;
	private Channel channel;
	private String requestQueueName = "rpc_queue";
	private String replyQueueName;
	private QueueingConsumer consumer;

	public RPCClient() throws IOException, TimeoutException {
		ConnectionFactory connectionFactory = new ConnectionFactory();
		connectionFactory.setHost("127.0.0.1");
		connectionFactory.setPort(5672);
		connectionFactory.setUsername("root");
		connectionFactory.setPassword("root");
		Connection connection = connectionFactory.newConnection();
		channel = connection.createChannel();
		replyQueueName = channel.queueDeclare().getQueue();
		consumer = new QueueingConsumer(channel);
		channel.basicConsume(replyQueueName, true, consumer);
	}

	public String call(String message) throws IOException, InterruptedException {
		String response = null;
		String corrId = UUID.randomUUID().toString();
		AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
				.correlationId(corrId)
				.replyTo(replyQueueName)
				.build();

		channel.basicPublish("", requestQueueName, properties, message.getBytes());
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			if (delivery.getProperties().getCorrelationId().equals(corrId)) {
				response = new String(delivery.getBody());
				break;
			}
		}
		return response;
	}

	public void close() throws IOException {
		connection.close();
	}

	public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
		RPCClient fibRpc = new RPCClient();
		log.info("[x] Requesting fib(30)");
		String call = fibRpc.call("30");
		log.info("[.] Got '" + call + "'");
		fibRpc.close();
	}
}

持久化

RabbitMQ持久化分三个部分

  • 交换器 durable 属性为true, 如果不是true不会导致数据丢失
  • 队列 durable 属性为true,如果不是true会导致数据丢失,队列元数据丢失.但是不能保证内部的消息一定不丢失.
  • 消息 消息持久化通过消息的投递模式 BasicProperties中的deliveryModel属性设置为2即可实现消息的持久化. MessageProperties.PERSISTENT_TEXT_PLAIN封装了这个属性. 持久化需要和队列同事设置为持久化状态.

这并不能保证消息一定不会丢,还需要确认机制的配合.但是还是不能保证消息一定不会丢,还有可能在服务宕机的时候丢数据,需要配合RabbitMQ的镜像队列机制.相当于配置了节点副本.这样能极大概率的提供消息不会丢失. 可以通过引入事务机制保证消息被正确的发送到RabbitMQ中,前提是交换器能正确的路由消息.

生产者确认

如何确保消息被成功的发送到RabbitMQ服务器

  • 通过事务机制实现

RabbitMQ中与事务机制相关的方法有三个:channel.txSelect/channel.txCommit/channel.txRollback.分别对应着事务的开启,提交,回滚。
示例:

channel.txSelect();//信道设置为事务模式
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,MessageProperties.PERSISTENT_TEST_PLAIN,"transaction message".getBytest());
channel.txCommit();//提交事务

catch(Excepion (IOException e){
	channel.txRollback();//回滚事务
}

使用事务会大大的降低RabbitMQ的性能。所以出现了下面这个方案。

  • 通过发送发确认(publish confirm)机制实现。

通过设置信道为confirm模式,该信道上的每个消息都会被分配一个唯一的ID,从1开始。一旦消息被投递到所有匹配的队列之后 RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),如果是需要持久化的消息,则会在持久化完成后才返回这个唯一id, 在deliveryTag中。basicAck还有一个参数multiple.表示到这个序号之前的所有消息都已经得到了处理。
示例:

channel.confirSelect(); //将信道设置为confirm模式
//发送消息
if(!channel.waitForConfirms()){
	//do something else .....
}

但是确认的动作还是同步的,和事务模式基本相同了,所有又衍生除了另外两种解决方案

  1. 批量confirm方法: 发送一批消息后,再调用channel.waitForConfirms方法,等待服务器的确认返回。
  2. 异步confirm方法: 提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理。
    批量确认模式中如果出现Basic.Nack或者超时情况时,客户端需要将这一批的消息全部重发。重复消息会过多。如果消息异常过多,导致重发消息太多反而会导致性能下降。
    示例:
package com.example.demo.rabbitmq;

import com.rabbitmq.client.Address;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class confirmModelDemo {
    private static final String QUEUE_NAME = "queue_demo";
    private static final String IP_ADDRESS = "127.0.0.1";
    private static final int PORT = 5672;
    private static final int BATCH_COUNT=100;

    public static void main(String[] args) throws IOException, TimeoutException {
        Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setUsername("root");
        connectionFactory.setPassword("root");
        Connection connection = connectionFactory.newConnection(addresses);
        Channel channel = connection.createChannel();

        channel.confirmSelect();
        int msgCount=0;
        while (true) {
            //发送消息
            //缓存消息
            if(++msgCount >=BATCH_COUNT){
                msgCount=0;
                try {
                    if (channel.waitForConfirms()) {
                        //清空缓存中的消息
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //将缓存中的消息重新发送。
                }
            }
        }
    }
}

异步confirm:
通过在channel接口中提供的addConfirmListener方法可以添加ConfirmListener这个回调接口。
这个接口中有两个方法:

  1. handleAck
  2. hangleNack
    分别用于处理RabbitMQ回传的Basic.Ack和 Basic.Nack,这两个方法中包含有一个参数deliveryTag (在发送确认模式下用来标记消息唯一有序序号)。通过为每一个信道维护一个“unconfirm”的消息序号集合。每发送一条消息 元素+1,每当调用confirmListener中的handleAck时,从集合中删除掉相应的一条未确认记录。
    示例:
package com.example.demo.rabbitmq;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.SortedSet;
import java.util.concurrent.TimeoutException;

public class confirmModelDemo {
    private static final String QUEUE_NAME = "queue_demo";
    private static final String IP_ADDRESS = "127.0.0.1";
    private static final int PORT = 5672;
    private static final int BATCH_COUNT=100;

    public static void main(String[] args) throws IOException, TimeoutException {
        Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setUsername("root");
        connectionFactory.setPassword("root");
        Connection connection = connectionFactory.newConnection(addresses);
        Channel channel = connection.createChannel();

        channel.confirmSelect();
//        int msgCount=0;
//        while (true) {
//            //发送消息
//            //缓存消息
//            if(++msgCount >=BATCH_COUNT){
//                msgCount=0;
//                try {
//                    if (channel.waitForConfirms()) {
//                        //清空缓存中的消息
//                    }
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                    //将缓存中的消息重新发送。
//                }
//            }
//        }
		SortedSet confirmSet=null;
        channel.addConfirmListener(new ConfirmListener() 

       
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                if (b) {
                    confirmSet.headSet(l - 1).clear();
                } else {
                    confirmSet.remove(l);
                }
            }

            @Override
            public void handleNack(long l, boolean b) throws IOException {
                if (b) {
                    confirmSet.headSet(l - 1).clear();
                } else {
                    confirmSet.remove(l);
                }
            }
        });
         //发送消息
        while (true) {
            long nextSeqNo = channel.getNextPublishSeqNo();
            //发送消息
//            channel.basicPublish();
            confirmSet.add(nextSeqNo);
        }
    }
}

消费端要点介绍

  • 消息分发
  • 消息顺序性
  • 弃用QueueingConsumer

消息分发:
主要设置channel的basicQos 数值和global,
global为true表示整个信道上所允许的最大未确认消息数量。
为false,表示每个消费者的最大未确认消息数量。

消息顺序性
RabbitMQ很难保证消息的顺序性,建议通过在消息体内维护顺序标识,业务自己控制消息顺序。

QueueingConsumer被弃用了,存在内存溢出,假死,替代方案是DefaultConsumer类

消息传输保障

传输保障三个层级

  • At most once: 最多一次,可能丢失消息,但是绝对不会重复。
  • At least once: 最少一次, 消息基本不会丢失,但是可能会重复传输。
  • Exactly once: 恰好一次。 每条消息肯定会被传输一次。
    RabbitMQ支持前两个
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值