RabbitMQ详解-06RabbitMQ高级

1. 过期时间TTL

可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息自动被删除。RabbitMQ可以对消息和队列设置TTL。有以下两种设置方法:

  • 通过队列属性设置,队列中所有消息都有相同的过期时间。
  • 对消息进行单独设置,每条消息TTL可以不同。

若两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。

1.1. 设置队列TTL

配置类中设置:

java 复制代码
args.put("x-message-ttl",5000);
return QueueBuilder.durable(ITEM_QUEUE).withArguments(args).build();

参数 x-message-ttl 的值必须是非负 32 位整数 (0 <= n <= 2^32-1) ,以毫秒为单位表示 TTL 的值。这样,值 6000 表示存在于 队列 中的当前消息将最多只存活 6 秒钟。

如果不设置TTL,则表示此消息不会过期。如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。

1.2. 设置消息TTL

在发送消息(可以发送到任何队列,不管该队列是否属于某个交换机)的时候设置过期时间即可。在测试类中编写如下方法发送消息并设置过期时间到队列:

java 复制代码
/**
     * 过期消息
     * 该消息投递任何交换机或队列中的时候;如果到了过期时间则将从该队列中删除
     */
    @Test
    public void ttlMessageTest(){
        MessageProperties messageProperties = new MessageProperties();
        //设置消息的过期时间,5秒
        messageProperties.setExpiration("5000");
​
        Message message = new Message("测试过期消息,5秒钟过期".getBytes(), messageProperties);
        //路由键与队列同名
        rabbitTemplate.convertAndSend("my_ttl_queue", message);
    }

expiration 字段以毫秒为单位表示 TTL 值。且与 x-message-ttl 具有相同的约束条件。因为 expiration 字段必须为字符串类型,broker 将只会接受以字符串形式表达的数字。

当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用。

2. 死信队列

DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也称为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。

消息变成死信,可能是由于以下的原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度

DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。

要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange 指定交换机即可。

2.1消息TTL过期

2.1.1演示

生产者:

java 复制代码
public class Producer {
	private static final String NORMAL_EXCHANGE = "normal_exchange";
		public static void main(String[] argv) throws Exception {
			try (Channel channel = RabbitMqUtils.getChannel()) {
				channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
				//设置消息的 TTL 时间
				AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
				//该信息是用作演示队列个数限制
				for (int i = 1; i <11 ; i++) {
					String message="info"+i;
					channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties, message.getBytes());
					System.out.println("生产者发送消息:"+message);
				}
			}
		} 
	}
}

消费者1代码(启动之后关闭该消费者 模拟其接收不到消息)

java 复制代码
public class Consumer01 {
	//普通交换机名称
	private static final String NORMAL_EXCHANGE = "normal_exchange";
	//死信交换机名称
	private static final String DEAD_EXCHANGE = "dead_exchange";
	public static void main(String[] argv) throws Exception {
		Channel channel = RabbitUtils.getChannel();
		//声明死信和普通交换机 类型为 direct
		channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
		channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
		//声明死信队列
		String deadQueue = "dead-queue";
		channel.queueDeclare(deadQueue, false, false, false, null);
		//死信队列绑定死信交换机与 routingkey
		channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
		//正常队列绑定死信队列信息
		Map<String, Object> params = new HashMap<>();
		//正常队列设置死信交换机 参数 key 是固定值
		params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
		//正常队列设置死信 routing-key 参数 key 是固定值
		params.put("x-dead-letter-routing-key", "lisi");

		String normalQueue = "normal-queue";
		channel.queueDeclare(normalQueue, false, false, false, params);
		channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
		System.out.println("等待接收消息.....");
		DeliverCallback deliverCallback = (consumerTag, delivery) -> {
			String message = new String(delivery.getBody(), "UTF-8");
			System.out.println("Consumer01 接收到消息"+message);
		};
		channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
		});
	}
}

消费者2代码(以上步骤完成后 启动 C2 消费者 它消费死信队列里面的消息)

java 复制代码
public class Consumer02 {
	private static final String DEAD_EXCHANGE = "dead_exchange";
	public static void main(String[] argv) throws Exception {
		Channel channel = RabbitUtils.getChannel();
		channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
		String deadQueue = "dead-queue";
		channel.queueDeclare(deadQueue, false, false, false, null);
		channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
		System.out.println("等待接收死信队列消息.....");
		DeliverCallback deliverCallback = (consumerTag, delivery) -> {
			String message = new String(delivery.getBody(), "UTF-8");
			System.out.println("Consumer02 接收死信队列的消息" + message);
		};
		channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
		});
	}
}

2.1.2流程

具体因为队列消息过期而被投递到死信队列的流程:

2.2 队列达到最大长度

2.2.1演示

生产者:

java 复制代码
public class Producer {
	private static final String NORMAL_EXCHANGE = "normal_exchange";
		public static void main(String[] argv) throws Exception {
			try (Channel channel = RabbitMqUtils.getChannel()) {
				channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
				//该信息是用作演示队列个数限制
				for (int i = 1; i <11 ; i++) {
					String message="info"+i;
					channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null, message.getBytes());
					System.out.println("生产者发送消息:"+message);
				}
			}
		} 
	}
}

消费者1修改以下代码(启动之后关闭该消费者 模拟其接收不到消息)

注意此时需要把原先队列删除 因为参数改变了

消费者2代码不变(启动 C2 消费者)

2.2.2流程

消息超过队列最大消息长度而被投递到死信队列的流程在前面的图中已包含。

2.3消息被拒

消息生产者代码同上生产者一致

消费者1代码(启动之后关闭该消费者 模拟其接收不到消息)

java 复制代码
public class Consumer01 {
	//普通交换机名称
	private static final String NORMAL_EXCHANGE = "normal_exchange";
	//死信交换机名称
	private static final String DEAD_EXCHANGE = "dead_exchange";
	public static void main(String[] argv) throws Exception {
		Channel channel = RabbitUtils.getChannel();
		//声明死信和普通交换机 类型为 direct
		channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
		channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
		//声明死信队列
		String deadQueue = "dead-queue";
		channel.queueDeclare(deadQueue, false, false, false, null);
		//死信队列绑定死信交换机与 routingkey
		channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
		//正常队列绑定死信队列信息
		Map<String, Object> params = new HashMap<>();
		//正常队列设置死信交换机 参数 key 是固定值
		params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
		//正常队列设置死信 routing-key 参数 key 是固定值
		params.put("x-dead-letter-routing-key", "lisi");
		String normalQueue = "normal-queue";
		channel.queueDeclare(normalQueue, false, false, false, params);
		channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
		System.out.println("等待接收消息.....");
		DeliverCallback deliverCallback = (consumerTag, delivery) -> {
			String message = new String(delivery.getBody(), "UTF-8");
			if(message.equals("info5")){
				System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
				//requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
				channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
			}else {
				System.out.println("Consumer01 接收到消息"+message);
				channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
			}
		};
		boolean autoAck = false;
		channel.basicConsume(normalQueue, autoAck, deliverCallback, consumerTag -> {
		});
	}
}

消费者代码不变

启动消费者 1 然后再启动消费者 2

3. 延迟队列

延迟队列存储的对象是对应的延迟消息;所谓"延迟消息" 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

在RabbitMQ中延迟队列可以通过 过期时间 + 死信队列 来实现;具体如下流程图所示:

在上图中;分别设置了两个5秒、10秒的过期队列,然后等到时间到了则会自动将这些消息转移投递到对应的死信队列中,然后消费者再从这些死信队列接收消息就可以实现消息的延迟接收。

延迟队列的应用场景;如:

  • 在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理。
  • 在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理。

4. 消息确认机制

确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。

4.1 发布确认

有两种方式:消息发送成功确认和消息发送失败回调。

4.1.1消息发送成功确认

在配置文件当中需要添加:

java 复制代码
spring.rabbitmq.publisher-confirm-type=correlated

⚫ NONE

禁用发布确认模式,是默认值

⚫ CORRELATED

发布消息成功到交换器后会触发回调方法

⚫ SIMPLE

经测试有两种效果:

其一效果和 CORRELATED 值一样会触发回调方法,

其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法

等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker。

消息确认回调方法:

java 复制代码
public class MsgSendConfirmCallBack implements RabbitTemplate.ConfirmCallback {
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("消息确认成功....");
        } else {
            //处理丢失的消息
            System.out.println("消息确认失败," + cause);
        }
    }
}

发送消息:

java 复制代码
@Test
    public void queueTest(){
        //路由键与队列同名
        rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。");
    }

管理界面确认消息发送成功:

消息确认回调:

4.1.2消息发送失败回调

消息失败回调方法:

java 复制代码
@Component
public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {
	@Autowired
	private RabbitTemplate rabbitTemplate;
	//rabbitTemplate 注入之后就设置该值
	@PostConstruct
	private void init() {
		rabbitTemplate.setConfirmCallback(this);
		/**
		* true:
		* 交换机无法将消息进行路由时,会将该消息返回给生产者
		* false:
		* 如果发现消息无法进行路由,则直接丢弃
		*/
		rabbitTemplate.setMandatory(true);
		//设置回退消息交给谁处理
		rabbitTemplate.setReturnCallback(this);
	}

    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        String msgJson  = new String(message.getBody());
        System.out.println("Returned Message:"+msgJson);
    }
}

模拟消息发送失败:

java 复制代码
@Test
public void testFailQueueTest() throws InterruptedException {
    //exchange 正确,queue 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
    amqpTemplate.convertAndSend("test_fail_exchange", "", "测试消息发送失败进行确认应答。");
}

4.2 事务支持

场景:业务处理伴随消息的发送,业务处理失败(事务回滚)后要求消息不发送。rabbitmq 使用调用者的外部事务,通常是首选,因为它是非侵入性的(低耦合)。

相关推荐
坐吃山猪11 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫11 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao11 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区12 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT13 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy13 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss15 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续15 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben04415 小时前
ReAct模式解读
java·ai