java B2B2C 仿淘寶電子商城系統-基於Rabbitmq實現延遲訊息

小兵2147775633發表於2019-02-14
  1. 預備知識

1.1 訊息傳遞

首先我們知道消費者是從佇列中獲取訊息的,那麼訊息是如何到達佇列的? 當我們傳送一條訊息時,首先會發給交換器(exchange),交換器根據規則(路由鍵:routing key)將會確定訊息投遞到那個佇列(queue)。

需要JAVA Spring Cloud大型企業分散式微服務雲構建的B2B2C電子商務平臺原始碼 一零三八七七四六二六

帶著這幾個關鍵字:交換器、路由鍵和佇列。

1.2 交換器型別

如之前所說,交換器根據規則決定訊息的路由方向。因此,rabbitmq的訊息投遞分類便是從交換器開始的,不同的交換器實現不同的路由演算法便實現了不同的訊息投遞方式。

direct交換器

direct -> routingKey -> queue,相當一種點對點的訊息投遞,如果路由鍵匹配,就直接投遞到相應的佇列

fanout交換器

fanout交換器相當於實現了一(交換器)對多(佇列)的廣播投遞方式

topic交換器

提供一種模式匹配的投遞方式,我們可以根據主題來決定訊息投遞到哪個佇列。

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)來重新傳送訊息到另外一個佇列,而這個佇列將是最終的消費佇列。

  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

<?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"
       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"/>
    <!--配置connection-factory,指定連線rabbit server引數 -->
    <rabbit:connection-factory id="connectionFactory"
                       username="${mq.user}" password="${mq.password}"
                       host="${mq.host}" port="${mq.port}" virtual-host="${mq.vhost}" />

    <!--定義rabbit template用於資料的接收和傳送 -->
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" />

    <!--通過指定下面的admin資訊,當前producer中的exchange和queue會在rabbitmq伺服器上自動生成 -->
    <rabbit:admin connection-factory="connectionFactory" />

    <!--定義queue -->
    <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">3600000</value>
            </entry>
            <!-- 訊息過期根據重新路由 -->
            <entry key="x-dead-letter-exchange" value="${sms.exchange}"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <!-- 定義direct exchange,sms_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>
    <!-- 延遲訊息配置,durable=true 持久化生效 -->
    <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"/>
    <!-- queue litener  觀察 監聽模式 當有訊息到達時會通知監聽在對應的佇列上的監聽物件-->
    <rabbit:listener-container connection-factory="connectionFactory" prefetch="${sms.prefetch}">
        <rabbit:listener queues="${sms.queue}" ref="messageReceiver"/>
    </rabbit:listener-container>
</beans>
複製程式碼

訊息釋出者

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
* <p>生產者</p>
* @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);
   }
}
複製程式碼

消費者

package git.yampery.consumer;

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

/**
* @decription MsgConsumer
* <p>消費者</p>
* @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;
       }
   }
}
複製程式碼

測試

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
 * <p>測試</p>
 * @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));
    }
}
複製程式碼

java B2B2C 仿淘寶電子商城系統

相關文章