rabbitMq+spring实现延迟队列(适用订单超时未支付的问题)

6 篇文章 0 订阅
1 篇文章 0 订阅

什么是延迟队列


通俗一点说,延迟队列和我们生活中常用的定时器有点像,定时器会在指定的时间后响起,延迟队列则会在指定的时间后处理消息。延迟队列主要的应用场景有订单超时取消、超时自动评价等等。

实现原理


RabbitMQ给我们提供了TTL(Time-To-Live)和DLX (Dead-Letter-Exchange)这两个特性,(队列的TTL和消息的TTL如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter)使用RabbitMQ实现延迟队列利用的正是这两个特性。TTL用于控制消息的生存时间,如果超时,消息将变成Dead Letter。DLX用于配置死信队列,可以通过配置x-dead-letter-exchange和x-dead-letter-routing-key这两个参数来指定消息变成死信后需要路由到的交换器和队列。

图例

首先看看死信队列的图

然后就是延迟队列,延迟队列本身就是死信队列的扩展

代码实例

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/rabbit
                           http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!--创建连接工厂-->
    <rabbit:connection-factory id="mqConnectionFactory"
                               host="***.***.**.**"
                               port="****"
                               username="***"
                               password="***"
                               publisher-confirms="true"
                               virtual-host="*****"/>

    <!--通过指定下面的admin信息,当前producer中的exchange和queue会在rabbitmq服务器上自动生成 -->
    <rabbit:admin id="orderConnectAdmin" connection-factory="mqConnectionFactory"/>
   <!-- <rabbit:admin id="delayConnectAdmin" connection-factory="delayConnectionFactory"/>-->

    <!-- 说明:将需要关闭的订单的消息发送到此队列
        durable: 为 true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
        autoDelete: 设置是否自动删除。为 true 则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到
        这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除
     -->
    <rabbit:queue name="shanreal_order_shutdown_queue" durable="false" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Long" />
            <entry key="x-dead-letter-exchange" value="shanreal_exchange_delay" />
            <entry key="x-dead-letter-routing-key" value="shanreal_delay_key" />
        </rabbit:queue-arguments>
    </rabbit:queue>

    <!--正常交换机
         durable:true 持久化。可以将交换器存盘,在服务器重启 的时候不会丢失相关信息。false 反之
         auto-declare:true 自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定 ,
         之后所有与这个交换器绑定的队列或者交换器都与此解绑
    -->
    <rabbit:fanout-exchange name="shanreal_exchange_normal" durable="false" auto-delete="true" id="shanreal_exchange_normal">
        <rabbit:bindings>
            <rabbit:binding queue="shanreal_order_shutdown_queue" />
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!--死亡队列,用于存储超时消息-->
    <rabbit:queue name="shanreal_delay_queue" durable="false" auto-declare="true"/>

    <!--死亡交换机,消息过时后会由此交换机转发到对应的队列-->
    <rabbit:direct-exchange name="shanreal_exchange_delay" durable="false" auto-delete="true" id="shanreal_exchange_delay">
        <rabbit:bindings>
            <rabbit:binding queue="shanreal_delay_queue" key="shanreal_delay_key"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>


    <!--定义rabbit orderAmqpTemplate用于数据的生产 -->
    <rabbit:template id="orderAmqpTemplate" connection-factory="mqConnectionFactory" exchange="shanreal_exchange_normal"/>

    <!--定义rabbit delayAmqpTemplate用于死信队列数据的消费 -->
    <rabbit:template
            id="delayAmqpTemplate"
            connection-factory="mqConnectionFactory"
            exchange="shanreal_exchange_delay"
            queue="shanreal_delay_queue"/>

    <!-- 配置线程池 -->
    <bean id ="taskExecutor"  class ="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" >
        <!-- 线程池维护线程的最少数量 -->
        <property name ="corePoolSize" value ="5" />
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name ="keepAliveSeconds" value ="30000" />
        <!-- 线程池维护线程的最大数量 -->
        <property name ="maxPoolSize" value ="1000" />
        <!-- 线程池所使用的缓冲队列 -->
        <property name ="queueCapacity" value ="200" />
    </bean>

    <bean id="delayTask" class="com.shanreal.controller.gb.rubbitMq.DelayTask" />

    <!-- Queue Listener 当有消息到达时会通知监听在对应的队列上的监听对象-->
    <rabbit:listener-container connection-factory="mqConnectionFactory" acknowledge="auto" task-executor="taskExecutor">
        <rabbit:listener queues="shanreal_delay_queue" ref="delayTask"/>
    </rabbit:listener-container>


</beans>

生产者:

这里可以单独对每个消息设置TTL(消息生存时间)

import com.rabbitmq.client.ConnectionFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class ProducerService {

/*    @Resource(name="mqConnectionFactory")
    private ConnectionFactory mqConnectionFactory;*/

    @Resource(name="orderAmqpTemplate")
    private AmqpTemplate orderAmqpTemplate;

    public void send(String msg) {
        orderAmqpTemplate.convertAndSend((Object) msg, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });
        System.out.println("Sent: " + msg);
    }

}

 

setExpiration是基于单个消息的超时设置,但是当我们先增加一条过期时间大(10000)的A消息进入,之后再增加一个过期时间小的(1000)消息B,并没有出现想象中的B消息先被消费,A消息后被消费,而是出现了当10000过去的时候,AB消息同时被消费,也就是B消息的消费被阻塞了。

为什么会出现这样的现象呢?
我们知道利用TTL DLX特性实现的方式,实际上在第一个延时队列C里面设置了dlx,生产者生产了一条带ttl的消息放入了延时队列C中,等到延时时间到了,延时队列C中的消息变成了死信,根据延时队列C中设置的dlx的exchange的转发规则,转发到了实际消费队列D中,当该队列中的监听器监听到消息时就会正式开始消费。那么实际上延时队列中的消息也是放入队列中的,队列满足先进先出,而延时大的消息A还没出队,所以B消息也不能顺利出队。
 

利用Rabbitmq的插件x-delay-message实现

为了解决上面的问题,Rabbitmq实现了一个插件x-delay-message来实现延时队列。

消费者:

这里可以手动消费

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class ConsumerService {
    @Resource(name="delayAmqpTemplate")
    private AmqpTemplate delayAmqpTemplate;

    public void recive() {
        System.out.println("Received: " + delayAmqpTemplate.receiveAndConvert());
    }
}

执行生产者操作

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;

@Controller
@RequestMapping(value="/mqTest")
public class TestController {

    @Resource(name = "producerService")
    private ProducerService producerService;

    @Resource(name = "consumerService")
    private ConsumerService consumerService;


    @ResponseBody
    @RequestMapping(value="/producerTest")
    public void producerTest() throws Exception {
        String  s = "我是生产者测试";
        producerService.send(s);
    }

    @ResponseBody
    @RequestMapping(value="/consumerTest")
    public void consumerTest() throws Exception {
        consumerService.recive();
    }
}

通过监听死信队列的消息来进行消费

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;

public class DelayTask implements MessageListener {
    @Override
    public void onMessage(Message message) {
        try {
            String receivedMsg = new String(message.getBody(), "UTF-8");
            System.out.println("Received : " + receivedMsg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

参考:rubbitMq实战指南

 

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值