1. 限时订单?
在各种电商网站下订单后会保留一个时间段,时间段内未支付则自动将订单状态设置为已过期。
2. 限时订单现象
在我们生活中处处可见限时订单的现象,如:在淘宝购物下单后没有付款,会提示多长时间订单失效;春季过年回家买火车 票,下了订单后半个小时不付款改订单就会取消;点外卖。。。
3. 解决方法一
轮询数据库:到实现一个定时器,每隔一段时间去检查一遍数据库里的所有订单,查看其状态是否是未支付并且已经期。并修改这些数据的状态为已过期。
优点:方法简单,容易实现
缺点:订单状态处理不及时,轮询数据库的次数中可能很多都并没有修改订单,数据库频繁多次被连接浪费数据库资源开销,因为数据库资源非常宝贵。
因此以上方式实际开发中基本不予采用。
4. 解决方法二
1.采用延时队列
采用延时队列并且与时间有关系的延时队列DelayQueue。
实现原理:
- 用户下单,保存订单到数据库的同时,将该订单以及订单的过期时间推入DelayQueue;
- 启动一个检查订单到期的线程,该线程使用delayQueue的take()方法获取到期订单,该方法为阻塞方法,如果当前没有到期订单,该方法会一直阻塞等待,直到获取到订单后继续往下执行;
- 当take()获取到一个到期订单后,该线程按获取到的订单的id去数据库查询订单并去检查订单状态,如果为未支付,则将状态修改为已关闭;
- 当项目重启后,DelayQueue中的信息都没有了。所以项目启动扫描所有过期未支付的订单并修改为已关闭状态,扫描所有未过期未支付的订单到DelayQueue中。
2. 代码实现
延时队列实体bean:
/**
*
* 延时队列实体Delayed
* @author reyco
*
*/
public class DelayedVo<T> implements Delayed{
/**
* 过期时长/单位毫秒
*/
private Long expireTime;
/**
* 目标对象
*/
private T target;
public DelayedVo(Long expireTime, T target) {
super();
this.expireTime = expireTime+System.currentTimeMillis();
this.target = target;
}
@Override
public int compareTo(Delayed o) {
return (int)(this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}
public T getTarget() {
return this.target;
}
}
延时订单:
/**
* 延时订单....
* @author reyco
*
*/
@Service
public class DelayOrderService{
/**
* 订单状态 1未支付 2已付款 3订单关闭 4订单完成
*/
/**
* 未支付
*/
private final static Integer UNPAID = 1;
/**
* 订单关闭
*/
private final static Integer CLOSE = 3;
@Autowired
private OrderDao orderDao;
private static DelayQueue<DelayedVo<OrderEntity>> delayQueue = new DelayQueue<DelayedVo<OrderEntity>>();
/**
* 添加订单到DelayQueue
* @param orderEntity
* @param expireTime
*/
public void save(OrderEntity orderEntity,Long expireTime) {
DelayedVo<OrderEntity> delayedVo = new DelayedVo<>(expireTime, orderEntity);
delayQueue.put(delayedVo);
System.out.println("订单【超时时间:"+expireTime+"毫秒】被推入延时队列,订单详情:"+orderEntity);
}
/**
* 异步线程处理DelayQueue
* @author reyco
*
*/
class OrderTask implements Runnable{
@Override
public void run() {
try {
while(true) {
DelayedVo<OrderEntity> delayedVo = delayQueue.take();
OrderEntity orderEntity = (OrderEntity)delayedVo.getTarget();
OrderEntity selOrderEntity = orderDao.get(orderEntity.getId());
//判断数据库中订单是否未支付
if(selOrderEntity.getState()==UNPAID) {
selOrderEntity.setState(CLOSE);
System.out.println("订单关闭:order="+selOrderEntity);
orderDao.update(selOrderEntity);
}else {
System.out.println("订单已处理:orderEntity="+selOrderEntity);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 启动异步线程
*/
@PostConstruct
public void init() {
new Thread(new OrderTask() ).start();
}
/**
* 启动修改过期未支付订单为已关闭状态
* 启动扫描数据库中的订单未过期未支付到DelayQueue
*/
@PostConstruct
public void initDelayOrder() {
//1. 处理过期未支付的订单...
Integer count = orderDao.updateExpire();
System.out.println("系统启动,扫描处理【"+count+"】个过期未支付的订单...");
//2. 获取未过期未支付的订单
List<OrderEntity> orders = orderDao.listOrderNoExpire();
System.out.println("系统启动,发现【"+orders.size()+"】个未过期未支付的订单...");
//3. 未过期未支付的订单推入延时队列
if(null!=orders && orders.size()>0) {
for (OrderEntity order : orders) {
long expireTime = order.getGmtExpire().getTime()-(new Date().getTime());
DelayedVo<OrderEntity> delayedVo = new DelayedVo<>(expireTime, order);
delayQueue.put(delayedVo);
System.out.println("订单【超时时间:"+expireTime+"毫秒】被推入延时队列,订单详情:"+order);
}
}
}
}
订单Service:
/**
* 订单Service
* @author reyco
*
*/
@Service
public class OrderServiceImpl implements OrderService{
@Autowired
private OrderDao orderedDao;
@Autowired
private DelayOrderService delayOrderService;
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void save(OrderEntity orderEntity) {
//订单号
Long no = new SnowFlake(2,3).nextId();
//超时时长
long expireTime = 1000 * 60 * 1;
Date gmtExpire = new Date();
gmtExpire.setTime(System.currentTimeMillis() + expireTime);
orderEntity = new OrderEnitiyBuilder()
.builderNo(no.toString())
.builderContent("500块钱的羽绒服。。。")
.builderState(1)
.builderGmtExpire(gmtExpire)
.builderGmtDesc("备注")
.builder();
// 保存到数据库
orderedDao.save(orderEntity);
//
delayOrderService.save(orderEntity, expireTime);
}
}
订单controller:
@RequestMapping("/api/order")
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("save")
public String save(@RequestBody OrderEntity orderEntity) {
orderService.save(orderEntity);
return "ok";
}
}
5. 解决方法三
1.使用MQ的死信队列:
实现原理:
1.生产者发送消息到交换机(order_delay_exchange)并指定路由键order_delay_route_key),消息到达队列(order_delay_queue);
2.队列(order_delay_queue)没有消费者,消息到期后,如果这个包含死信的队列配置了 (dead-letter-exchange=order_dead_letter_exchange)属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机(order_dead_letter_exchange)中,而这个交换机称为死信交换机;如果同时配置了(dead-letter-routing-key =order_dead_route_key)参数,则消息会由死信交换机发送到指定的队列(order_dead_queue),也就是死信队列(order_dead_queue)。
3.消费者只需要订阅这个死信队列(order_dead_queue)就可以完成消费了。
2.代码实现
1)配置初始化交换机、队列:
public class MqConstant {
/**
* 正常交换机
*/
public static final String ORDER_DELAY_EXCHANGE = "order_delay_exchange";
/**
* 死信交换机
*/
public static final String ORDER_DEAD_EXCHANGE = "order_dead_letter_exchange";
/**
* 延迟队列
*/
public static final String ORDER_DELAY_QUEUE = "order_delay_queue";
/**
* 死信队列
*/
public static final String ORDER_DEAD_QUEUE = "order_dead_queue";
/**
* 正常路由key
*/
public static final String ORDER_DELAY_ROUTE_KEY = "order_delay_route_key";
/**
* 死信路由key
*/
public static final String ORDER_DEAD_ROUTE_KEY = "order_dead_route_key";
}
@Configuration
public class RabbitConfig {
@Bean
public DirectExchange orderDeladExchange() {
return new DirectExchange(MqConstant.ORDER_DELAY_EXCHANGE, true, false);
}
@Bean
public DirectExchange orderDeadExchange() {
return new DirectExchange(MqConstant.ORDER_DEAD_EXCHANGE, true, false);
}
@Bean
public Queue orderDeadQueue() {
return new Queue(MqConstant.ORDER_DEAD_QUEUE, true, false, false);
}
@Bean
public Queue orderDelayQueue() {
Map<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", MqConstant.ORDER_DEAD_EXCHANGE);
map.put("x-dead-letter-routing-key", MqConstant.ORDER_DEAD_ROUTE_KEY);
Queue queue = new Queue(MqConstant.ORDER_DELAY_QUEUE, true, false, false, map);
return queue;
}
@Bean
public Binding orderDelayQueueBinding() {
return BindingBuilder.bind(orderDelayQueue()).to(orderDeladExchange()).with(MqConstant.ORDER_DELAY_ROUTE_KEY);
}
@Bean
public Binding orderQueueDeadBinding() {
return BindingBuilder.bind(orderDeadQueue()).to(orderDeadExchange()).with(MqConstant.ORDER_DEAD_ROUTE_KEY);
}
}
2)生产者:
/**
* 生产者
* @author reyco
*
*/
@Service
public class MqProducrService {
private static final Logger logger = LoggerFactory.getLogger(MqProducrService.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg, long time, String delayQueueName) {
// rabbit默认为毫秒级
long times = time * 1000;
// 后置处理器,设置过期时间
MessagePostProcessor processor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
logger.info("设置过期时间:"+times);
message.getMessageProperties().setExpiration(String.valueOf(times));
return message;
}
};
logger.info("【发送消息】:"+msg);
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
if(ack) {
logger.info("【发送消息】消息发送成功");
}else {
logger.info("【发送消息】消息发送失败:");
}
});
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey)->{
logger.info("返回消息回调:{} 应答代码:{} 回复文本:{} 交换器:{} 路由键:{}", message, replyCode, replyText, exchange, routingKey);
});
rabbitTemplate.convertAndSend(MqConstant.ORDER_DELAY_EXCHANGE, MqConstant.ORDER_DELAY_ROUTE_KEY, msg, processor);
}
}
3.消费者
/**
* 消费者
* @author reyco
*
*/
@Service
public class MqOrderConsumerService {
private static final Logger logger = LoggerFactory.getLogger(MqOrderConsumerService.class);
@Autowired
private OrderDao orderDao;
@RabbitHandler
@RabbitListener(queues = MqConstant.ORDER_DEAD_QUEUE)
public void process(String msg, Channel channel, Message message) throws InterruptedException {
try {
logger.info("【消息消费】延迟时间到,开始执行, {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
Order order = (Order) JsonUtils.jsonToObj(msg, Order.class);
if (order != null && order.getState() == 1) {
order.setState(3);
orderDao.update(order);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
logger.info("【消息消费】消息消费成功, {},{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),msg);
} catch (IOException e) {
logger.info("【消息消费】消息消费失败, {},{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),msg);
}
}
}