开发过程中通常会碰到这样的需求:

  • 淘宝订单业务:下单后 30min 之内没有付款,就自动取消订单。

  • 饿了吗订餐通知:下单成功后 60s 之后给用户发送短信通知。

  • 关闭空闲连接:服务器中有很多客户端的连接,空闲一段时间之后需要关闭之。

  • 缓存:缓存中的对象,超过了空闲时间,从缓存中移出。

  • 任务超时处理:在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。

  • 失败重试机制:业务操作失败后,间隔一定的时间进行失败重试。

这类业务的特点就是:需要延迟工作,需要进行失败重试。一种比较笨的方式是使用一个后台线程,遍历所有对象,挨个检查。这种方法简单好用,但是对象数量过多时,可能存在性能问题,检查间隔时间不好设置,间隔时间过大,影响精确度,过小则存在效率问题,而且做不到按超时的时间顺序处理。

再比如常见的场景:

场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时。

场景二:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单。

实现原理

RabbitMQ给我们提供了TTL(Time-To-Live)和DLX (Dead-Letter-Exchange)这两个特性,使用RabbitMQ实现延迟队列利用的正是这两个特性。TTL用于控制消息的生存时间,如果超时,消息将变成Dead Letter。DLX用于配置死信队列,可以通过配置x-dead-letter-exchange和x-dead-letter-routing-key这两个参数来指定消息变成死信后需要路由到的交换器和队列。关于TTL(Time-To-Live)和DLX (Dead-Letter-Exchange)的详细介绍可以参考文末的参考链接。

代码示例

简单介绍一下,项目首先引入Spring与RabbitMQ相关的依赖,然后在spring-rabbitmq.xml中配置一个常规队列(delay_queue)和一个死信队列(task_queue),在常规队列上配置x-message-ttl和x-dead-letter-exchange等参数指向死信队列。最后配置任务处理器(delayTask),并将任务处理器与死信队列绑定并配置到监听器上。然后就可以开开心心地实现业务逻辑了~

项目中有两个Service,ProducerService和ConsumerService,导入项目后可以直接运行这两个类的main方法查看延迟队列的效果。也可以部署到本地服务器后运行web项目来测试,但是测试中只使用到了ProducerService。

spring-rabbitmq.xml
<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/rabbit
           http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionFactory" class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
        <property name="host" value="localhost"/>
        <property name="port" value="5672"/>
        <property name="username" value="guest"/>
        <property name="password" value="guest"/>
    </bean>

    <rabbit:admin connection-factory="connectionFactory"/>

    <rabbit:queue name="delay_queue" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="5000" value-type="java.lang.Long" />
            <entry key="x-dead-letter-exchange" value="exchange_delay" />
            <entry key="x-dead-letter-routing-key" value="task_queue" />
        </rabbit:queue-arguments>
    </rabbit:queue>

    <rabbit:queue name="task_queue" auto-declare="true"/>
    <rabbit:direct-exchange name="exchange_delay" durable="false" auto-delete="false" id="exchange_delay">
        <rabbit:bindings>
            <rabbit:binding queue="task_queue" />
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" queue="delay_queue" routing-key="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.shenofusc.task.DelayTask" />

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

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

    @Resource
    private AmqpTemplate amqpTemplate;

    public void send(String msg) {
        amqpTemplate.convertAndSend(msg);
        System.out.println("Sent: " + msg);
    }

    public static void main(String[] args) {
        ApplicationContext context = new GenericXmlApplicationContext("classpath:/spring-rabbitmq.xml");
        AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class);
        amqpTemplate.convertAndSend("Hello World");
        System.out.println("Sent: Hello World");
    }

}
ConsumerService
@Service
public class ConsumerService {

    @Resource
    private AmqpTemplate amqpTemplate;

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

    public static void main(String[] args) {
        ApplicationContext context = new GenericXmlApplicationContext("classpath:/spring-rabbitmq.xml");
        AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class);
        System.out.println("Received: " + amqpTemplate.receiveAndConvert());
    }

}

参考:https://blog.csdn.net/shy_1023/article/details/78517210