基于Rabbitmq实现延迟消息

  • 1. 预备知识
    • 1.1 消息传递
    • 1.2 交换器类型
    • 1.3 消息延迟
  • 2. 具体实现
    • 2.1 rabbitmq配置
    • 2.2 消息发布者
    • 2.3 消费者
    • 2.4 测试
  • 3. 总结
  • 4. 源码链接

本文基于Rabbitmq,使用java客户端实现延迟消息队列。
基于Redis实现参考这里

1. 预备知识

1.1 消息传递

首先我们知道消费者是从队列中获取消息的,那么消息是如何到达队列的?
当我们发送一条消息时,首先会发给交换器(exchange),交换器根据规则(路由键:routing key)将会确定消息投递到那个队列(queue)。
带着这几个关键字:交换器、路由键和队列。

1.2 交换器类型

如之前所说,交换器根据规则决定消息的路由方向。因此,rabbitmq的消息投递分类便是从交换器开始的,不同的交换器实现不同的路由算法便实现了不同的消息投递方式。

  • direct交换器

    direct -> routingKey -> queue,相当一种点对点的消息投递,如果路由键匹配,就直接投递到相应的队列

  • fanout交换器

    fanout交换器相当于实现了一(交换器)对多(队列)的广播投递方式

  • topic交换器

    提供一种模式匹配的投递方式,我们可以根据主题来决定消息投递到哪个队列。

    以上内容,在《Rabbitmq in Action》中有详细讲解。

1.3 消息延迟

本文想要实现一个可延迟发送的消息机制。消息如何延迟?
- ttl (time to live) 消息存活时间
ttl是指一个消息的存活时间。

  • Per-Queue Message TTL in Queues
    引用官方的一句话:

    TTL can be set for a given queue by setting the x-message-ttl argument to queue.declare, or by setting the message-ttl policy. A message that has been in the queue for longer than the configured TTL is said to be dead.
    我们可以通过x-message-ttl设置一个队列中消息的过期时间,消息一旦过期,将会变成死信(dead-letter),可以选择重新路由。

  • Per-Message TTL in Publishers
    引用官方的一句话:

A TTL can be specified on a per-message basis, by setting the expiration field in the basic AMQP class when sending a basic.publish.
The value of the expiration field describes the TTL period in milliseconds. The same constraints as for x-message-ttl apply. Since the expiration field must be a string, the broker will (only) accept the string representation of the number.
我们可以通过设置每一条消息的属性expiration,指定单条消息有效期。消息一旦过期,将会变成死信(dead-letter),可以选择重新路由。


  • 重新路由-死信交换机(Dead Letter Exchanges)
    引用官方一句话:

Dead Letter Exchanges

Messages from a queue can be ‘dead-lettered’; that is, republished to
another exchange when any of the following events occur:

The message is rejected (basic.reject or basic.nack) with
requeue=false, The TTL for the message expires; or The queue length
limit is exceeded. Dead letter exchanges (DLXs) are normal exchanges.
They can be any of the usual types and are declared as usual.
To set the dead letter exchange for a queue, set the x-dead-letter-exchange argument to the name of the exchange.
我们可以通过设置死信交换器(x-dead-letter-exchange)来重新发送消息到另外一个队列,而这个队列将是最终的消费队列。

2. 具体实现

有了以上原理,无论使用哪种语言都比较好实现,示例使用gradle构建,java实现,使用Springboot。
注意,采用了xml配置要在springboot启动主类中引用,使用@Component等注解要在启动主类中添加包扫描,如本示例扫描生产者。详见源码。

2.1 rabbitmq配置

属性文件-rabbitmq.properties

交换、路由等配置按照以上策略,其中,添加了prefetch参数来根据服务器能力控制消费数量。

# 连接用户名
mq.user                             =sms_user
# 密码
mq.password                         =123456
# 主机
mq.host                             =192.168.99.100
# 端口
mq.port                             =5672
# 默认virtual-host
mq.vhost                            =/
# the default cache size for channels is 25
mq.channelCacheSize                 =50

# 发送消息路由
sms.route.key                       =sms_route_key
# 延迟消息队列
sms.delay.queue                     =sms_delay_queue
# 延迟消息交换器
sms.delay.exchange                  =sms_delay_exchange
# 消息的消费队列
sms.queue                           =sms_queue
# 消息交换器
sms.exchange                        =sms_exchange
# 每秒消费消息数量
sms.prefetch                        =30

配置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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/rabbit
     http://www.springframework.org/schema/rabbit/spring-rabbit-1.7.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder location="rabbitmq.properties"/>
    
    <rabbit:connection-factory id="connectionFactory"
                       username="${mq.user}" password="${mq.password}"
                       host="${mq.host}" port="${mq.port}" virtual-host="${mq.vhost}" />

    
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" />

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

    
    <rabbit:queue name="${sms.queue}" durable="true" auto-delete="false" exclusive="false" />
    
    <rabbit:queue name="${sms.delay.queue}" durable="true" auto-delete="false">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl">
                
                <value type="java.lang.Long">3600000value>
            entry>
            
            <entry key="x-dead-letter-exchange" value="${sms.exchange}"/>
        rabbit:queue-arguments>
    rabbit:queue>

    
    <rabbit:direct-exchange name="${sms.exchange}" durable="true" auto-delete="false">
        <rabbit:bindings>
            <rabbit:binding queue="${sms.queue}" key="${sms.route.key}"/>
        rabbit:bindings>
    rabbit:direct-exchange>
    
    <rabbit:direct-exchange name="${sms.delay.exchange}" durable="true" auto-delete="false">
        <rabbit:bindings>
            <rabbit:binding queue="${sms.delay.queue}" key="${sms.route.key}"/>
        rabbit:bindings>
    rabbit:direct-exchange>

    
    <bean id="messageReceiver" class="git.yampery.consumer.MsgConsumer"/>
    
    <rabbit:listener-container connection-factory="connectionFactory" prefetch="${sms.prefetch}">
        <rabbit:listener queues="${sms.queue}" ref="messageReceiver"/>
    rabbit:listener-container>
beans>

2.2 消息发布者

package git.yampery.producer;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @decription MsgProducer
 * 

生产者

* @author Yampery * @date 2018/2/11 11:44 */
@Component public class MsgProducer { @Resource private AmqpTemplate amqpTemplate; @Value("${sms.delay.exchange}") private String SMS_DELAY_EXCHANGE; @Value("${sms.exchange}") private String SMS_EXCHANGE; @Value("${sms.route.key}") private String SMS_ROUTE_KEY; /** * 延迟消息放入延迟队列中 * @param msg * @param expiration */ public void publish(String msg, String expiration) { amqpTemplate.convertAndSend(SMS_DELAY_EXCHANGE, SMS_ROUTE_KEY, msg, message -> { // 设置消息属性-过期时间 message.getMessageProperties().setExpiration(expiration); return message; }); } /** * 非延迟消息放入待消费队列 * @param msg */ public void publish(String msg) { amqpTemplate.convertAndSend(SMS_EXCHANGE, SMS_ROUTE_KEY, msg); } }

2.3 消费者

消费者通过实现MessageListener,并未对失败消息做处理,可以选择重新路由到处理失败的队列或日志等其他方案。

package git.yampery.consumer;

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

/**
 * @decription MsgConsumer
 * 

消费者

* @author Yampery * @date 2018/2/11 11:43 */
public class MsgConsumer implements MessageListener { @Override public void onMessage(Message message) { String msg; try { // 线程每秒消费一次 Thread.sleep(1000); msg = new String(message.getBody(), "utf-8"); System.out.println(msg); } catch (Exception e) { // 这里并没有对服务异常等失败的消息做处理,直接丢弃了 // 防止因业务异常导致消息失败造成unack阻塞再队列里 // 可以选择路由到另外一个专门处理消费失败的队列 return; } } }

2.4 测试

启动项目之后,测试向队列发布一条消息

package git.yampery.mq;

import com.alibaba.fastjson.JSONObject;
import git.yampery.producer.MsgProducer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

/**
 * @decription TestMq
 * 

测试

* @author Yampery * @date 2018/2/11 15:03 */
@RunWith(SpringRunner.class) @SpringBootTest public class TestMq { @Resource private MsgProducer producer; @Test public void testMq() { JSONObject jObj = new JSONObject(); jObj.put("msg", "这是一条短信"); producer.publish(jObj.toJSONString(), String.valueOf(10 * 1000)); } }

3. 总结

文章利用rabbitmq的ttl和死信路由机制,实现了一个点对点的延迟消息队列。
基于Redis实现参考这里

4. 源码链接

上述代码中并没有gradle配置等,具体请查看源码:
https://github.com/Yampery/rabbit.git

你可能感兴趣的:(中间件)