乐优商城(18)--延时消息

乐优商城(18)–延时消息

一、需求分析

  • 卖家发货后后,默认7天用户确认收货
  • 买家确认收货后72小时不评价,那么该订单自动评价为好评
  • 订单未及时支付,将该订单关闭

1.1、方案一

cron定时任务

启动一个cron定时任务,每小时跑一次,将订单完成时间超过72小时的订单取出,置为好评,并把评价状态设置为已评价

缺点:

  • 轮询效率比较低
  • 每次扫描数据库,已经被执行过的记录任然会被扫描,导致重复计算
  • 时效性不好,可能产生误差

1.2、方案二

延时消息

利用消息中间件实现延时消息,当用户确认收货则发送消息给消息队列,72小时不评价,那么该订单自动评价为好评

高效延时消息,包含两个重要的数据结构:

  1. 环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
  2. 任务集合,环上每一个slot是一个Set<Task>

同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。

Task结构中有两个很重要的属性:

  1. Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
  2. Task-Function:需要执行的任务指针

假设当前Current Index指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:

  1. 计算这个Task应该放在哪一个slot,现在指向1,3610秒之后,应该是第11格,所以这个Task应该放在第11个slot的Set<Task>
  2. 计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1

Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set<Task>,每个Task看Cycle-Num是不是0:

  1. 如果不是0,说明还需要多移动几圈,将Cycle-Num减1
  2. 如果是0,说明马上要执行这个Task了,取出Task-Funciton执行(可以用单独的线程来执行Task),并把这个Task从Set<Task>中删除

使用了“延时消息”方案之后,“订单72小时后关闭评价”的需求,只需将在订单被用户确认收货后,触发一个48小时之后的延时消息即可

优点:

  • 无需再轮询全部订单,效率高
  • 一个订单,任务只执行一次
  • 时效性好,精确到秒(控制timer移动频率可以控制精度)

二、RabbitMQ延时队列

RabbitMQ本身并不直接支持延迟队列功能,但是可以根据其特性Per-Queue Message TTLDead Letter Exchanges实现延时队列。也可以通过改特性设置消息的优先级

2.1、TTL概述

RabbitMQ可以针对消息和队列设置TTL(过期时间)

过期时间TTL表示消息有效存活的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置:

  • 通过队列属性设置,队列中所有消息都有相同的过期时间。
  • 对消息进行单独设置,每条消息TTL可以不同。

若上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的数值为准。消息在队列的生存时间一旦超过设置的TTL值,就变为dead message被投递到死信队列,消费者将无法再收到该消息。

2.2、死信队列

DLX,全称为Dead-Letter-Exchange,可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称之为死信队列。消息变成死信,可能是由于以下的原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度

DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可。

代码参数

channel.basicNack( deliveryTag, multiple, requeue)  一次拒绝0个或多个

参数:eliveryTag:该消息的index

	 multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。

	 requeue:被拒绝的是否重新入队列

channel.basicReject(deliveryTag, requeue)			 一次拒绝一个

参数:deliveryTag:该消息的index

     requeue:被拒绝的是否重新入队列

区别

  • 队列TTL:该模式下RabbitMQ快要过期的消息会在队列头部,RabbitMQ通过扫描队列头部,查询是否有过期消息,有则将其移除
  • 消息TTL:该模式下对消息的移除是惰性的,每个消息的TTL可能不同,要删除所有过期消息,势必要扫描整个队列,所以当消息即将被消费时才对该TTL消息进行判断

2.3、队列TTL

在这里插入图片描述

2.3.1、建立delay.exchange

在这里插入图片描述

这里Internal设置为NO,否则将无法接受dead letter,YES表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定

2.3.2、建立延时队列(delay.test.queue)

在这里插入图片描述

如上配置延时1min队列(x-message-ttl=60000)

x-max-length:最大积压的消息个数,可以根据自己的实际情况设置,超过限制消息不会丢失,会立即转向delay.exchange进行投递

x-dead-letter-exchange:设置为刚刚建立的delay.exchange,消息过期后会投递到delay.exchange

这里不需要配置"dead letter routing key"否则会覆盖掉消息发送时携带的routingkey,导致后面无法路由到刚刚创建的delay.exchange

2.3.3、配置延时路由规则

需要延时的消息到exchange后先路由到指定的延时队列

创建delaysync.exchange通过Routing key将消息路由到延时队列

在这里插入图片描述

绑定好的效果:

在这里插入图片描述

配置delay.exchange 将消息投递到正常的消费队列

先创建一个普通队列

在这里插入图片描述

再配置delay.exchange

在这里插入图片描述

效果:

在这里插入图片描述

2.3.4、测试

生产者

/**
 * 生产者
 */
public class Producer {
   

    private final static String EXCHANGE_NAME = "delaysync.exchange";

    public static void main(String[] args) throws Exception {
   
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //发布消息
        String msg = System.currentTimeMillis() + "";
        channel.basicPublish(EXCHANGE_NAME,"dead.message",null,msg.getBytes());
        System.out.println("发送时刻:" + msg + "发送成功:" + System.currentTimeMillis());
        channel.close();
        connection.close();
    }
}

消费者

/**
 * 消费者
 */
public class Consumer {
   

    private final static String QUEUE_NAME = "test.queue";

    public static void main(String[] args) throws Exception {
   
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
   
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
   
                String msg = new 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值