淘宝七天自动收货,设计与实现
淘宝的七天自动收货,在我们签收商品后,物流系统会在七天之后延时发送一个消息给支付系统,通知系统将款打给商家,这个过程会持续七天-------------因为使用了消息中间件的延迟推送功能。
在比如说12306购票支付确认页面,我们在选好票点击确定跳转的页面中往往都会有倒计时,代表着30分钟内订单不确认的话,将会自动取消订单-------------其实在下订单的那一刻开始购票业务系统就会发送一个延时消息给订单系统,延时30分钟,就告诉订单系统未完成,如果我们在30分钟内完成了订单的话,则可以通过逻辑代码判断来忽略收到的消息。
————————————————————在上面的两种场景中,我们可以使用两种传统方案降低系统的整体性能和吞吐量————————————————————————
1.使用redis给订单设置过期时间,最后通过判断redis中是否还有该订单来解决订单是否完成。但是这种消息的延迟推送性能较低,因为redis都是储存在内存中的,如果我们遇到恶意刷单或者恶意下单的将会给内存带来巨大的压力。
2.如果使用传统的数据库的话,轮询来判断数据库表中的订单的状态,这样的性能极低。
3.如果使用JVM原生的DelayQueue,也是大量的占用内存,并且没有持久化策略,如果遇到系统宕机或者重启都会丢失订单信息。
消息延迟推送的实现
在RabbitMQ 3.6.x之前一般采用的是死信队列+TTL过期时间来实现
在RabbitMQ 3.6.x开始,我们可以使用延迟队列的插件
首先我们创建交换机和消息队列,application.properties中配置
1 import org.springframework.amqp.core.*; 2 import org.springframework.context.annotation.Bean; 3 import org.springframework.context.annotation.Configuration; 4 5 import java.util.HashMap; 6 import java.util.Map; 7 8 @Configuration 9 public class MQConfig { 10 11 public static final String LAZY_EXCHANGE = "Ex.LazyExchange"; 12 public static final String LAZY_QUEUE = "MQ.LazyQueue"; 13 public static final String LAZY_KEY = "lazy.#"; 14 15 @Bean 16 public TopicExchange lazyExchange(){ 17 //Map<String, Object> pros = new HashMap<>(); 18 //设置交换机支持延迟消息推送 19 //pros.put("x-delayed-message", "topic"); 20 TopicExchange exchange = new TopicExchange(LAZY_EXCHANGE, true, false, pros); 21 exchange.setDelayed(true); 22 return exchange; 23 } 24 25 @Bean 26 public Queue lazyQueue(){ 27 return new Queue(LAZY_QUEUE, true); 28 } 29 30 @Bean 31 public Binding lazyBinding(){ 32 return BindingBuilder.bind(lazyQueue()).to(lazyExchange()).with(LAZY_KEY); 33 } 34 }
我们在Exchange的声明中可以设置exchange.setDelayed(true)来开启延时队列,当然也可以使用以下内容传入交换机的方法中,因为第一种方式的底层就是通过这种方式实现的。
1 //Map<String, Object> pros = new HashMap<>(); 2 //设置交换机支持延迟消息推送 3 //pros.put("x-delayed-message", "topic"); 4 TopicExchange exchange = new TopicExchange(LAZY_EXCHANGE, true, false, pros);
发送消息我们需要指定的延迟推送的时间,我们在这里发送消息的方法中传入参数 new MessagePostProcessor() 是为了获得Message()对象,因为需要借助Message对象的api来设置时间。
1 import com.anqi.mq.config.MQConfig; 2 import org.springframework.amqp.AmqpException; 3 import org.springframework.amqp.core.Message; 4 import org.springframework.amqp.core.MessageDeliveryMode; 5 import org.springframework.amqp.core.MessagePostProcessor; 6 import org.springframework.amqp.rabbit.connection.CorrelationData; 7 import org.springframework.amqp.rabbit.core.RabbitTemplate; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.stereotype.Component; 10 11 import java.util.Date; 12 13 @Component 14 public class MQSender { 15 16 @Autowired 17 private RabbitTemplate rabbitTemplate; 18 19 //confirmCallback returnCallback 代码省略,请参照上一篇 20 21 public void sendLazy(Object message){ 22 rabbitTemplate.setMandatory(true); 23 rabbitTemplate.setConfirmCallback(confirmCallback); 24 rabbitTemplate.setReturnCallback(returnCallback); 25 //id + 时间戳 全局唯一 26 CorrelationData correlationData = new CorrelationData("12345678909"+new Date()); 27 28 //发送消息时指定 header 延迟时间 29 rabbitTemplate.convertAndSend(MQConfig.LAZY_EXCHANGE, "lazy.boot", message, 30 new MessagePostProcessor() { 31 @Override 32 public Message postProcessMessage(Message message) throws AmqpException { 33 //设置消息持久化 34 message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); 35 //message.getMessageProperties().setHeader("x-delay", "6000"); 36 message.getMessageProperties().setDelay(6000); 37 return message; 38 } 39 }, correlationData); 40 } 41 }
我们可以观察setDelay(Integer i)底层代码,也是在header中设置x-delay。等同于我们手动设置
1 message.getMessageProperties().setHeader("x-delay", "6000"); 2 /** 3 * Set the x-delay header. 4 * @param delay the delay. 5 * @since 1.6 6 */ 7 public void setDelay(Integer delay) { 8 if (delay == null || delay < 0) { 9 this.headers.remove(X_DELAY); 10 } 11 else { 12 this.headers.put(X_DELAY, delay); 13 } 14 }
——————————————————————————消化端进行消费————————————————————
1 import com.rabbitmq.client.Channel; 2 import org.springframework.amqp.rabbit.annotation.*; 3 import org.springframework.amqp.support.AmqpHeaders; 4 import org.springframework.stereotype.Component; 5 6 import java.io.IOException; 7 import java.util.Map; 8 9 @Component 10 public class MQReceiver { 11 12 @RabbitListener(queues = "MQ.LazyQueue") 13 @RabbitHandler 14 public void onLazyMessage(Message msg, Channel channel) throws IOException{ 15 long deliveryTag = msg.getMessageProperties().getDeliveryTag(); 16 channel.basicAck(deliveryTag, true); 17 System.out.println("lazy receive " + new String(msg.getBody())); 18 19 } 20 ``` 21 22 ## 测试结果[#](https://www.cnblogs.com/haixiang/p/10966985.html#3724420099) 23 24 ```java 25 import org.junit.Test; 26 import org.junit.runner.RunWith; 27 import org.springframework.beans.factory.annotation.Autowired; 28 import org.springframework.boot.test.context.SpringBootTest; 29 import org.springframework.test.context.junit4.SpringRunner; 30 31 @SpringBootTest 32 @RunWith(SpringRunner.class) 33 public class MQSenderTest { 34 35 @Autowired 36 private MQSender mqSender; 37 38 @Test 39 public void sendLazy() throws Exception { 40 String msg = "hello spring boot"; 41 42 mqSender.sendLazy(msg + ":"); 43 } 44 }
我们在6秒收到了消息 lazy receive hello spring boot