现在有一个场景:商品下订单成功后,需要在30分钟进行支付,若超过30分钟尚未支付则认为订单超时作废此订单。请你设计一个方案实现此功能。
首先这个问题是一个好问题,它里面涉及到了两个最常用的开发概念:PUSH和PULL。
解释一下
PUSH:外部进行信息推送,本地被动接受消息,进行数据处理。
PULL:本地进行数据的主动获取,通过接口,协议等方式,主动去获取是否有数据要进行处理。
对于上述的问题,那么久可以选取这两种方案来进行解决,可以比较一下实现的复杂度和执行效率。
PULL方案
设置一个定时运行的程序,每间隔XX秒(分钟),执行一次数据库查询数据库中数据超过30分钟尚未付款订单,进行更新订单状态为作废状态。
这个方案存在问题:你的定时任务的时间间隔设置为多少呢?60S?三分钟?订单超时是一个瞬间的状态,这样会出现一种情况,就是订单已经超时,但是,状态尚未改变,因为它需要定时任务执行一次,才能改变订单的状态。这样在业务上存在天然的缺陷。有人会说,我执行的频率改为1S一次,就解决了这个时间的问题。那么系统会不会添加很多的无效的扫描呢?数据库压力会不会增大呢?
PUSH方案(异步通知方案)
当系统添加一个订单的时候,向某个地方存储一个订单ID的信息,此信息可以设置定时的时间。本地系统存在一个监听器,当消息推送到当前系统时,根据订单ID,去检查订单是否已经支付,然后订单修改状态。
这样的异步通知可以使用下面两种方案:
1.RabbitMQ的延迟消息(死信队列&延迟队列)
2.使用redis的setex
其实还有一种方案可以实现此需求,那就是使用quartz定时调度。
当订单生成时候,在Quartz中生成一个30分钟的定时调度,将订单ID作为参数传递,等30分钟后,quartz调度此任务来执行检查。但是此方案有很大弊端,因为你的任务存储在内存中,假如系统任务数过多,会大量消耗系统的资源。
---------------------------------------------具体实现-----------------------------------------------------------------
1.RabbitMQ的延迟消息
rabbitMQ的延迟消息可以使用两种方式来做.延迟队列和死信队列,因为延迟队列需要安装插件(源码又在github上,很难搞到合适的插件,我有一份3.8.0的版本的插件,有需要的可以咨询),所以,当前方案使用死信队列来做延迟消息的处理。
环境:spring boot2.x,rabbitmq 3.8.4(erlang 23.X)
a.设置rabbitmq绑定关系的config配置
@Bean("oriUseQueue") public Queue queue() { Mapmap = new HashMap (); map.put("x-message-ttl", 10000); // 10秒钟后成为死信 map.put("x-dead-letter-exchange", "SYS_DEAD_LETTER_EXCHANGE"); // 队列中的消息变成死信后,进入死信交换机 return new Queue("ORI_USE_QUEUE", true, false, false, map); } @Bean("oriUseExchange") public DirectExchange oriUseExchange() { return new DirectExchange("ORI_USE_EXCHANGE", true, false, new HashMap<>()); } @Bean public Binding binding(@Qualifier("oriUseQueue") Queue queue,@Qualifier("oriUseExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("ori.use"); } @Bean("deatLetterExchange") public TopicExchange deadLetterExchange() { return new TopicExchange("SYS_DEAD_LETTER_EXCHANGE", true, false, new HashMap<>()); } @Bean("deatLetterQueue") public Queue deadLetterQueue() { return new Queue("SYS_DEAD_LETTER_QUEUE", true, false, false, new HashMap<>()); } @Bean public Binding bindingDead(@Qualifier("deatLetterQueue") Queue queue,@Qualifier("deatLetterExchange") TopicExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("#"); // 无条件路由 }
b.代码发送延迟消息
@Autowired AmqpTemplate amqpTemplate; public void lazyMessageSend() { long now = System.currentTimeMillis(); amqpTemplate.convertAndSend("ORI_USE_EXCHANGE", "ori.use", "-------- a direct msg,prepare to dead msg!,timestamp is:" + now); }
c.设置监听死信队列
@Component @RabbitListener(queues = "SYS_DEAD_LETTER_QUEUE") public class DeadLetterMessageReceiver { @RabbitHandler public void process(String msg) { long now = System.currentTimeMillis(); System.out.println("SYS_DEAD_LETTER_QUEUE 死信队列收到消息了!!!内容:" + msg); System.out.println("当前时间:" + now); } }
死信队列能发送延迟消息的图示:
2.使用redis的setex来实现延迟通知
redis是可以通过修改配置文件中配置信息来实现过期消息的通知。订单创建完毕之后,就可以在redis存储一个订单ID的key,设置这个ID的过期时间为30分钟,这样30分钟后,键过期后,就可以在监听程序中收到redis的通知!
a.修改redis配置
修改notify-keyspace-events的值为“EX”:
notify-keyspace-events "Ex"
修改完成后,需要重启redis服务。
b.在自己的spring_boot项目中配置监听容器:
@Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; }
c.编写监听程序:
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Component; @Slf4j @Component public class KeyExpireListener extends KeyExpirationEventMessageListener { @Autowired private StringRedisTemplate redisTemplate; public KeyExpireListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Override public void onMessage(Message message, byte[] pattern) { String expiredKey = message.toString(); System.out.println("message:" + message); log.info("expiredKey=========" + expiredKey); //获取过期key存储的数据 Object value = redisTemplate.opsForHash().get("value", expiredKey); log.info(value.toString()); // todo process programming //删除过期key存储的数据 redisTemplate.opsForHash().delete("value", expiredKey); } }
这样就可以在WEB系统中收到延迟消息了。