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> 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 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.Spring boot + Redis的健销毁监控 和 RabbitMQ 延时队列处理定时触发任务_第1张图片

当修改此表示信息时  需要删除原先的队列,否侧不生效

2.

Spring boot + Redis的健销毁监控 和 RabbitMQ 延时队列处理定时触发任务_第2张图片

Spring boot + Redis的健销毁监控 和 RabbitMQ 延时队列处理定时触发任务_第3张图片

此处的两个rountingKey必须对应上  , 当消息成为死信消息时   会发送到消费交换机上 此时携带的rountingKey就是这个 所以需要对应上

你可能感兴趣的:(中间件,redis,rabbitmq,spring,boot,延时队列)