Spring boot + Redis的健銷燬監控 和 RabbitMQ 延時隊列 處理定時觸發時間

一 基於Redis實現

1.場景:
    電商系統或者購票系統都必須具備訂單功能,生成訂單後一段時間不支付訂單會自動關閉。最簡單的想法是設置定時任務輪詢,    但是每個訂單的創建時間不一樣,定時任務的規則無法設定,如果將定時任務執行的間隔設置的過短,太影響效率。還有一種想法,在用戶進入訂單界面的時候,判斷時間執行相關操作。方式可能有很多,在這裏介紹一種監聽Redis鍵值對過期時間來實現訂單自動關閉。

2.思路:
    在生成訂單時,向Redis中增加一個KV鍵值對,K爲訂單號,或者訂單id,保證通過K能定位到數據庫中的某個訂單即可,V可爲任意值(後邊會解釋爲什麼V可爲任意值)。

    假設,生成訂單時向Redis中存放K爲訂單號,V也爲訂單號的鍵值對,並設置過期時間爲30分裝,如果該鍵值對在30分鐘過期後能夠發送給程序一個通知,或者執行一個方法,那麼即可解決訂單關閉問題。

    實現:通過監聽Redis提供的過期隊列來實現,監聽過期隊列後,如果Redis中某一個KV過期了,那麼將向監聽者發送消息,監聽者可以獲取到該鍵值對的K,注意,是獲取不到V的,因爲已經過期了,這就是上面所提到的,爲什麼要保證能通過K來定位到訂單,而V爲任意值即可。拿到K後,通過K定位訂單,並判斷其狀態,如果是未支付,更新爲關閉,或者取消狀態即可。

3.實現:
    1.項目爲SSM框架基礎架構。

    2.使用SpringDataRedis來操作Redis(SDR)。

    3.創建一個監聽者類,實現SDR提供的監聽者接口。

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

@Component
public class TopicMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] bytes) {
        byte[] body = message.getBody();// 請使用valueSerializer
        byte[] channel = message.getChannel();
        //設置監聽頻道
        String topic = new String(channel);
        //key
        String itemValue = new String(body);
        System.out.println("頻道topic:"+topic);
        System.out.println("過期的鍵值對的K:"+itemValue);
   }

}

 

4. 增加監聽者的配置    

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: Ribbon
 * @Date 2019/1/10 17:11
 **/
@Component
public class RedisLinsterConf {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private TopicMessageListener topicMessageListener;

    @Bean
    MessageListenerAdapter messageListenerAdapter() {
        return new MessageListenerAdapter(topicMessageListener);
    }
    @Bean
    RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        Map<MessageListener, Collection<? extends Topic>> map = new HashMap<>();
        map.put(messageListenerAdapter(), Arrays.asList(new ChannelTopic("__keyevent@0__:expired")));
        container.setMessageListeners(map);
        return container;
    }
}

6.這個配置,配置的是監聽的頻道,格式爲固定,Redis有16個庫,配置中0代表監聽第0個庫,如果要監聽所有庫,可將0改爲*,星號,如果監聽其他庫,將0改爲庫的編號即可0-15。keyevent代表監聽的事件類型,expired表示,監聽的時間爲過期事件,也就是當第0個庫中如果有KV過期,那麼,監聽者類將接受到消息。注意,配置中有兩處出現了下劃線,每一處下劃線均有兩個下劃線組成,一定要注意  這是一個下劃線  _   這是兩個下劃線  __ 。

 

7.修改Redis配置文件,開啓過期通知功能

 標記1處原來是被註釋掉的,打開註釋。

    標記2處原來是沒有註解的,將其註釋掉。這兩處爲開始Redis的過期通知功能,保證跟圖中的註釋一致即可。

    還有要保證程序能夠連接上Redis,該配置中0.0.0.0表示任意ip都可連接Redis。

 

 8.那麼到這裏其實重要的配置已經完成,可以啓動項目,進行測試。打開Redis客戶端,存放KV並設置過期時間,如set testKey testValue Ex 5。存放一個鍵值對,過期時間爲5秒,那麼5秒後監聽者類就會收到消息。

 

二 基於RabbitMq延時隊列實現

延時隊列的使用場景:

1.訂單業務:在電商中,用戶下單後30分鐘後未付款則取消訂單。

2.短信通知:用戶下單並付款後,1分鐘後發短信給用戶。

延時隊列實現思路
AMQP協議和RabbitMQ隊列本身沒有直接支持延遲隊列功能,但是我們可以通過RabbitMQ的兩個特性來曲線實現延遲隊列:

特性一:Time To Live(TTL)

RabbitMQ可以針對Queue設置x-expires 或者 針對Message設置 x-message-ttl,來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間爲準),則消息變爲dead letter(死信)
RabbitMQ針對隊列中的消息過期時間有兩種方法可以設置。
A: 通過隊列屬性設置,隊列中所有消息都有相同的過期時間。
B: 對消息進行單獨設置,每條消息TTL可以不同。

如果同時使用,則消息的過期時間以兩者之間TTL較小的那個數值爲準。消息在隊列的生存時間一旦超過設置的TTL值,就成爲dead letter

特性二:Dead Letter Exchanges(DLX)

RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key發送
隊列出現dead letter的情況有:
消息或者隊列的TTL過期
隊列達到最大長度

消息被消費端拒絕(basic.reject or basic.nack)並且requeue=false

1. rabbitmq配置

package com.db.demo.test.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @author: Ribbon
 * @Date 2019/1/11 14:24
 **/
@Configuration

public class DelayRabbitConfig {
    /**
     *發送
     *
     */


    private static final String ORDER_DELAY_QUEUE = "shop.order.delay.queue";//延遲隊列 TTL 名稱
    public static final String ORDER_DELAY_EXCHANGE = "shop.order.delay.exchange";//dead letter發送到的 exchange  延時消息就是發送到該交換機的
    public static final String ORDER_DELAY_ROUTING_KEY = "shop.order_delay";//routing key 名稱


    /**
     * 消費
     */

    public static final String ORDER_QUEUE_NAME = "shop.order.queue";
    public static final String ORDER_EXCHANGE_NAME = "shop.order.exchange";
    public static final String ORDER_ROUTING_KEY = "order";





    /**
     * 延遲隊列配置
     * 1、params.put("x-message-ttl", 5 * 1000);
     * 第一種方式是直接設置 Queue 延遲時間 但如果直接給隊列設置過期時間,這種做法不是很靈活,(當然二者是兼容的,默認是時間小的優先)
     *
     * 2、rabbitTemplate.convertAndSend(book, message -> {
     * message.getMessageProperties().setExpiration(2 * 1000 + "");
     * return message;
     * });
     * 第二種就是每次發送消息動態設置延遲時間,這樣我們可以靈活控制
     **/

    @Bean
    public Queue delayOrderQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 聲明瞭隊列裏的死信轉發到的DLX名稱,
        params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
        // x-dead-letter-routing-key 聲明瞭這些死信在轉發時攜帶的 routing-key 名稱。********************
        params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
        //params.put("x-message-ttl", 60000);
        return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
    }

    /**
     * 需要將一個隊列綁定到交換機上,要求該消息與一個特定的路由鍵完全匹配。
     * 這是一個完整的匹配。如果一個隊列綁定到該交換機上要求路由鍵 “dog”,則只有被標記爲“dog”的消息才被轉發,
     * 不會轉發dog.puppy,也不會轉發dog.guard,只會轉發dog。
     * @return DirectExchange
    */
    @Bean
    public DirectExchange orderDelayExchange() {
        return new DirectExchange(ORDER_DELAY_EXCHANGE);
    }

    /**
     * shop.order.delay.queue隊列與hop.order.delay.exchange 的交換機綁定 並指定了rountingkey  order_delay
     * @return
     */
    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
    }


    /*******************************************************************************************************************/

    @Bean
    public Queue orderQueue() {
        return new Queue(ORDER_QUEUE_NAME, true);
    }
    /**
     * 將路由鍵和某模式進行匹配。此時隊列需要綁定要一個模式上。
     * 符號“#”匹配一個或多個詞,
     * 符號“*”匹配不多不少一個詞。
     * 因此“audit.#”能夠匹配到“audit.irs.corporate”
     * ,但是“audit.*” 只會匹配到“audit.irs”。
     **/
    @Bean
    public TopicExchange orderTopicExchange() {
        return new TopicExchange(ORDER_EXCHANGE_NAME);
    }
    @Bean
    public Binding orderBinding() {
        // 如果要讓延遲隊列之間有關聯,這裏的 routingKey 和 綁定的交換機很關鍵    ************************************8
        return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
    }


}

2.消息發送

package com.db.demo.test.sender;

import com.db.demo.test.config.DelayRabbitConfig;
import com.db.demo.test.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author: Ribbon
 * @Date 2019/1/11 14:41
 **/
@Component
@Slf4j
public class DelaySender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendDelay(Order order) {
        log.info("【訂單生成時間】" + new Date().toString() +"【1分鐘後檢查訂單是否已經支付】" + order.toString() );
        rabbitTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE, DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order, message -> {
         // 如果配置了 params.put("x-message-ttl", 5 * 1000); 那麼這一句也可以省略,具體根據業務需要是聲明 Queue 的時候就指定好延遲時間還是在發送自己控制時間
            message.getMessageProperties().setExpiration(3 * 1000 * 60 + "");
            return message;
        });
    }



}

3.消息接受

package com.db.demo.test.receive;

import com.db.demo.test.config.DelayRabbitConfig;
import com.db.demo.test.entity.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;
 
@Component
@Slf4j
public class DelayReceiver {
 
    @RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
    public void orderDelayQueue(Order order, Message message, Channel channel) {
        log.info("【orderDelayQueue 監聽的消息】 - 【消費時間】 - [{}]- 【訂單內容】 - [{}]",  new Date(), order.toString());
        log.info("###########################################");
    }
}

4.消息測試

@GetMapping("/sendDelay")
public Object sendDelay() {
    Order order1 = new Order();
    order1.setOrderStatus(0);
    order1.setOrderId("123456");
    order1.setOrderName("小米6");
    Order order2 = new Order();
    order2.setOrderStatus(1);
    order2.setOrderId("456789");
    order2.setOrderName("小米8");
    delaySender.sendDelay(order1);
    delaySender.sendDelay(order2);
    return "ok";
}

注意: 

 1.

當修改此表示信息時  需要刪除原先的隊列,否側不生效

2.

此處的兩個rountingKey必須對應上  , 當消息成爲死信消息時   會發送到消費交換機上 此時攜帶的rountingKey就是這個 所以需要對應上

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章