RabbitMQ实现延时消息

RabbitMQ原生并不支持延时消息,需要我们自行实现。

AMQP协议

amqp流程

  1. 生产者端发布消息到MQ中的交换机Exchange,每条消息会带一个路由键RoutingKey;
  2. 交换机与队列queue通过路由键进行绑定binding;
  3. 消息通过路由键路由到对应的队列queue,队列与交换器没有对应的绑定关系则消息会丢失;
  4. 消息由队列投递给消费者端进行消费。

交换机类型

  1. direct:直连交换机
  2. fanout:广播交换机
  3. topic:主题交换机
  4. headers:头交换机,基本不用,由direct替代

延时消息实现方式

消息过期时间+死信队列

普通队列绑定死信交换机,发送消息时设置过期时间,过期时间到了进入死信队列,监听死信队列消费。
存在的问题:消息的过期时间不一致时,使用死信队列可能起不到延时的作用。当发送两条不同过期时间的消息时,先发送的消息1过期时间(20s)大于后发送的消息2过期时间(10s),由于消息的顺序消费,消息2过期后并不会立即重新发布到死信交换机,而是等消息1过期后一起被消费。
使用场景:超时时间一致,如订单超时取消。

延时插件

  1. 下载延时插件,将插件移至rabbitmq插件目录下,通过命令应用延时插件。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  1. 声明一个类型为x-delayed-message的交换机CustomExchange。
  2. 参数添加x-delayed-type,值为交换机类型的属性,如direct,用于路由键的映射。
  3. 发送消息时,消息头header设置参数x-delay,值为延迟时间,单位毫秒。

云消息队列RabbitMQ(阿里云)

阿里云消息队列rabbitmq提供了延时消息的原生支持,只需要在消息头header里面增加参数delay,值为延迟时间,单位毫秒。

MessageProperties messageProperties = new MessageProperties();
String msgId = "CANCEL:" + order.getId();
//此处设置的msgId才能被会转成rabbitmq client的messageId,发送给broker
messageProperties.setMessageId(msgId);
long delay = 15 * 60 * 1000;
// 消息头设置延时时间
messageProperties.setHeader("delay", delay);
Message message = new Message(order.getId().getBytes(), messageProperties);
rabbitTemplate.convertAndSend(ORDER_CANCEL_TOPIC, Q_CANCEL_ORDER, message);

代码使用示例

  1. pom文件添加amqp依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置文件application.yml添加mq配置。
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: rabbitmq
    password: rabbitmq
    virtual-host: rabbitmq
    listener:
      simple:
        acknowledge-mode: manual
  1. 编写配置类RabbitMQConfig,创建交换机、队列并进行绑定。
@Configuration
public class RabbitMQConfig {

    // 普通交换机
    public static final String ORDER_CANCEL_EXCHANGE = "order_cancel_exchange";
    public static final String ORDER_CANCEL_QUEUE = "order_cancel_queue";
    // 死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String DEAD_ROUTING_KEY = "dead_routing_key";
    public static final String DEAD_QUEUE = "dead_queue";
    // 延迟插件交换机
    public static final String DMP_EXCHANGE = "dmp_exchange";
    public static final String DMP_ROUTING_KEY = "dmp_routing_key";
    public static final String DMP_QUEUE = "dmp_queue";

    @Bean
    public Exchange orderCancelExchange() {
        return ExchangeBuilder.directExchange(ORDER_CANCEL_EXCHANGE).build();
    }

    @Bean
    public Queue orderCancelQueue() {
        return QueueBuilder.durable(ORDER_CANCEL_QUEUE).deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey(DEAD_ROUTING_KEY).build();
    }

    @Bean
    public Exchange deadExchange() {
        return ExchangeBuilder.directExchange(DEAD_EXCHANGE).build();
    }

    @Bean
    public Queue deadQueue() {
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }

    @Bean
    public Binding orderCancelBinding(Exchange orderCancelExchange, Queue orderCancelQueue) {
        return BindingBuilder.bind(orderCancelQueue).to(orderCancelExchange).with("").noargs();
    }

    @Bean
    public Binding deadBinding(Exchange deadExchange, Queue deadQueue) {
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
    }

    // 延时插件使用
    @Bean
    public CustomExchange dmpExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DMP_EXCHANGE, "x-delayed-message", true, false, args);
    }

    @Bean
    public Queue dmpQueue() {
        return QueueBuilder.durable(DMP_QUEUE).build();
    }

    @Bean
    public Binding dmpBinding(CustomExchange dmpExchange, Queue dmpQueue) {
        return BindingBuilder.bind(dmpQueue).to(dmpExchange).with(DMP_ROUTING_KEY).noargs();
    }
}

4.编写消息生产者MsgSender,实现死信队列、延时插件消息发送。

@Component
@Slf4j
public class MsgSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 通过死信队列发送消息
     * @param message
     * @param time
     */
    public void send(String message, Integer time) {
        String expireTime = String.valueOf(time * 1000);
        rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_CANCEL_EXCHANGE, "", message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //设置消息的过期时间,是以毫秒为单位的
                message.getMessageProperties().setExpiration(expireTime);
                return message;
            }
        });
        log.info("死信队列消息:{}发送成功,过期时间:{}秒", message, time);
    }

    /**
     * 通过延迟插件发送消息
     * @param message
     * @param time
     */
    public void sendByPlugin(String message, Integer time) {
        rabbitTemplate.convertAndSend(RabbitMQConfig.DMP_EXCHANGE, RabbitMQConfig.DMP_ROUTING_KEY, message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
                message.getMessageProperties().setHeader("x-delay", time * 1000);
                return message;
            }
        });
        log.info("延迟插件消息:{}发送成功,过期时间:{}秒", message, time);
    }
}
  1. 编写消息消费者MsgListener,监听延时消息进行相应的业务处理。
@Component
@Slf4j
public class MsgListener {

    @RabbitListener(queues = RabbitMQConfig.DEAD_QUEUE)
    public void orderCancel(Message message, Channel channel) throws IOException {
        log.info("使用死信队列,收到消息:{}", new String(message.getBody()));
        // 手动ack
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    @RabbitListener(queues = RabbitMQConfig.DMP_QUEUE)
    public void orderCancelByPlugin(Message message, Channel channel) throws IOException {
        log.info("使用延迟插件,收到消息:{}", new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
  1. 编写测试类MsgController,启动服务发送消息测试。
@RestController
public class MsgController {

    @Autowired
    public MsgSender msgSender;

    @GetMapping("/send")
    public String send(@RequestParam String msg, Integer time) {
        msgSender.send(msg, time);
        return "success";
    }

    @GetMapping("/sendByPlugin")
    public String sendByPlugin(@RequestParam String msg, Integer time) {
        msgSender.sendByPlugin(msg, time);
        return "success";
    }
}

1)使用死信队列发送两次消息http://localhost:8081/send?msg=消息A&time=20 和http://localhost:8081/send?msg=消息B&time=10,消息会按发送顺序消费,并没有起到延时的效果。
死信队列测试

2)使用延时插件发送两次消息http://localhost:8081/sendByPlugin?msg=消息A&time=20 和http://localhost:8081/sendByPlugin?msg=消息B&time=10,消息会按照延时时间顺序消费。
延时插件测试结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值