#淘宝七天自动确认收货,让你实现,怎么设计?
目前非常多应用软件都有消息延迟推送的需求,例如:
- 淘宝购物在我们签收商品后,如不点击收货确认,系统会在七天后自动确认收货,并通知支付系统打款给商家,这个过程持续七天。
- 物联网系统定时向终端下发命令,如果一段时间没有应答,就需要将终端设置为超时。
- 12306购票支付确认页面。我们选好票点击确认跳转到支付页面,倒计时30分钟如果不支付则会自动取消订单。其实在下单的那一刻系统就发送一个延时消息,时效30分钟,如果30分钟内支付订单,则不取消订单,如没有支付,则取消订单。
上述场景,如使用传统方法,无疑将大大降低系统整体性能和吞吐量:
- 使用传统数据库轮询来判断数据库表中订单状态,这大大增加IO次数,性能极低。
- 使用Redis给订单设置过期时间,最后判断Redis中是否还有该订单来决定订单是否生效。这种解决方案比数据库轮询效率高,但低于消息延迟推送,因为Redis存储于内存中,数据量较大、恶意刷单等情况会给系统带来巨大冲击。
- 使用JVM原生DelayQueue,同样会占用大量内存,且没有持久化策略,一旦重启或宕机,则数据丢失。
#RabbitMQ延迟队列实现消息延迟推送功能
RabbitMQ3.6前可采用死信队列+TTL。
RabbitMQ3.6后,官方提供了rabbitmq_delayed_message_exchange延迟队列插件,本文采取这种方式。
#Rabbitmq_delayed_message_exchange插件下载安装
RabbitMQ插件放到根目录的plugins文件夹下,如D:\Program Files\RabbitMQ\rabbitmq_server-3.9.5\plugins。
①将rabbitmq_delayed_message_exchange-3.9.0.ez下载到plugins文件夹下,安装后也不能删除。
官方下载地址:Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub
② 双击RabbitMQ Command Prompt (sbin dir)或CMD到RabbitMQ的sbin目录中,输入rabbitmq-plugins enable rabbitmq_delayed_message_exchange安装延时队列插件。
③启动RabbitMQ,通过网址http://localhost:15672/ 进入登录界面(前提开启WEB管理插件,参考上一篇文章),可见Exchanges中新增加了一种全新的队列x-delayed-message,安装成功!
#RabbitMQ消息延迟推送delayed_message代码实现
①Maven依赖及application.propertie基础配置可参考上一篇文章:
淘宝购买商品后系统如何处理?认识消息服务之RabbitMQ基础,打造Direct、Fanout、Topic三种消息队列。(精选长文)_m0_59562547的博客-CSDN博客
②配置消息延迟队列,在TopicExchange交换机声明中设置exchange.setDelayed(true)来开启延迟队列。
package com.example.demohelloworld.RabbitMq.Lazy;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitLazyConfig {
public final static String lazyname="my-lazyname";
@Bean
TopicExchange topicExchangeLazy(){
TopicExchange exchange= new TopicExchange(lazyname,true,false);
exchange.setDelayed(true);//开启延时队列
return exchange;
}
@Bean
Queue queueLazy(){
return new Queue("lazy");
}
@Bean
Binding bindingLazy(){
return BindingBuilder.bind(queueLazy())
.to(topicExchangeLazy())
.with("lazy.#");
}
}
也可以声明通用交换机CustomExchange,type类型为x-delayed-message,消息类型args设置为("x-delayed-type","topic")来开启延迟队列。setDelayed(true)底层也是通过这种方式来实现的。TopicExchange和CustomExchange两种方式二选一,源码参考如下:
@Bean
CustomExchange customExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "topic");
return new CustomExchange(lazyname,"x-delayed-message", true, false, args);
}
@Bean
Queue queueLazy(){
return new Queue("lazy");
}
@Bean
BindingBuilder.GenericArgumentsConfigurer binding() {
return BindingBuilder.bind(queueLazy())
.to(customExchange())
.with("lazy.#");
}
③配置消息消费端,因为是精简代码,所以暂时不含有Confirm和Returns监听,如需要可参看上一篇文章。我们配置一个"lazy"队列监听,指定一个消息消费方法logger日志输出,将所接收到的消息内容在后台输出(生产环境中则为具体执行的业务逻辑,比如淘宝订单消息消费逻辑,自动确认收货;12306订单消息消费逻辑,关闭订单)。
package com.example.demohelloworld.RabbitMq.Lazy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RabbitLazyService {
Logger logger= LoggerFactory.getLogger(this.getClass());
@RabbitListener(queues = "lazy")
public void Handler(Message message)throws IOException{
logger.info("LazyReceiverHandler:"+ new String(message.getBody()));
}
}
④配置实现类来模拟5秒钟后执行延迟队列,CorrelationData是传入方法执行时的时间戳,声明消息处理类MessagePostProcessor是通过Message->对象的Header属性来设置消息延迟时间,这里我们采用了lamda写法,setHeader("x-delay",5000)表明这是一个延迟推送队列,时间为5秒。
rabbitTemplate.convertAndSend中分别写入交换机名称,队列名称,消息内容、消息处理类MessagePostProcessor以及方法执行时间戳CorrelationData。
package com.example.demohelloworld.RabbitMq.Lazy;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class RabbitLazyController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/lazy")
public void lazy(){
CorrelationData correlationData=new CorrelationData("12345678909"+new Date());
MessagePostProcessor messagePostProcessor= message -> {
message.getMessageProperties().setHeader("x-delay",5000);
return message;
};
rabbitTemplate.convertAndSend(RabbitLazyConfig.lazyname,"lazy.send","hello lazy!",messagePostProcessor,correlationData);
}
}
⑤最后启动项目开始测试,打开浏览器输入http://localhost:8080/lazy,等待5秒钟后,我们收到了消息消费端发来的消息。
#总结
回到最开始的问题,当我们在淘宝下单签收商品后,立即生成时间为七天的延时消息队列,在这七天内,如果不点击收货确认,系统会在七天后自动推送消息使订单确认收货并通知支付系统打款给商家,如已经点击收货确认,则该延时消息队列失效。