订单付款倒计时实现方案

当使用 12306 抢票成功后,就会进入付款界面,这个时候就会出现一个订单倒计时,下面我们就对付款倒计时的功能实现,进行深入学习和介绍,界面展示如下:

 如何实现付款及时呢,首先用户下单后,存储用户的下单时间。下面介绍四种系统自动取消订单的方案:

一、DelayQueue 延时无界阻塞队列


 我们的第一反应是用 数据库轮序+任务调度 来实现此功能。但这种高效率的延迟任务用任务调度(定时器)实现就得不偿失。而且对系统也是一种压力且数据库消耗极大。因此我们使用 Java 延迟队列 DelayQueue 来实现,DelayQueue 是一个无界的延时阻塞队列(BlockingQueue),用于存放实现了 Delayed 接口的对象,队列中的对象只能在其到期时才能从队列中取走。这种队列是有序的,既队头对象的延迟到期时间最长。

//加入delayQueue的对象,必须实现Delayed接口,同时实现如下:compareTo和GetDelay方法 
static class DelayItem implements Delayed{
     //过期时间(单位:分钟)
     private long expTime;
     private String orderCode;
		
     public DelayItem(String orderCode,long expTime,Date createTime) {
          super();
          this.orderCode=orderCode;
          this.expTime=TimeUnit.MILLISECONDS.convert(expTime, TimeUnit.MINUTES)+createTime.getTime();
     }
     /**
     * 用于延迟队列内部比较排序,当前时间的延迟时间  -  比较对象的延迟时间
     */
     @Override
     public int compareTo(Delayed o) {
          return Long.valueOf(this.expTime).compareTo(Long.valueOf(((DelayItem)o).expTime));
     }
		
     /**
     * 获得延迟时间,过期时间-当前时间(单位ms)
     */
     @Override
     public long getDelay(TimeUnit unit) {
          return this.expTime-System.currentTimeMillis();
     }
}

将未付款的订单都 add 到延迟队列中,并通过线程池启动多个线程不断获取延迟队列的内容,获取到后进行状态的修改,进行业务逻辑处理。具体代码如下:

public class DelayQueueTest implements Runnable{
	//创建一个延迟队列
	private	DelayQueue item = new DelayQueue<>();

	@Override
	public void run() {
	     while(true) {
		  try {
			//只有当到期了才会获取到此对象
			DelayItem delayed = (DelayItem) item.take();
		        //获取到之后修改状态
		   } catch (InterruptedException e) {
	                e.printStackTrace();
		   }
	     }
	}

        //添加数据调用的方法
	public void orderTimer(DelayItem delayItem) {
		//向队列汇总添加数据
		item.add(delayItem);
	}
	
	public static void main(String[] args) {
	      //创建一个线程池
	      ExecutorService executor = Executors.newCachedThreadPool();
	      //多线程执行程序
	      executor.execute(new DelayQueueTest());
	}
}

 这种方案的缺点:1)、代码复杂度较高,大量消息堆积,性能不能保证,且很容易触发OOM。
        2)、需要考虑分布式的实现、存在单点故障。

二、环形队列


58同城架构沈剑提供一种基于时间轮的环形队列算法,在他的分享中,一个高效延时消息,包含两个重要的数据结构:
    1)、环形队列,例如可以创建一个包含3600个 slot 的环形队列(本质是个数组)
    2)、任务集合,环上每一个 slot 是一个 Set
同时,启动一个 timer ,这个 timer 每隔一秒,在上述环形队列中移动一格,有一个 Current Index 指针来标识正在检测的 slot。环形队列分为 3600 个长度,每秒移动一格,移动 3600 秒正好一个小时。比如一个任务需要在60秒后执行,那这个任务应该放在那个槽位的集合里呢?假设当前指针移动到 slot 的位置为2,那么60秒后的槽位就是62,所以数据应该放在索引为 62 的那个槽位圈数为0。如果这个任务要70分钟,70*60+2=4202,4202-3600=602,减了一次3600,所以应该放在第二圈的602槽位,既放在队列索引为602槽位的集合,且圈数为1,代表运行一圈后才执行这个任务。
       这种方案效率高,任务触发时间延迟时间比 delayQueue 低,代码复杂度 delayQueue 低,但没有公开源码,不过通过次思路可以实现次组件,当然缺点和 delayQueue 相同。

三、使用 Redis 实现


通过 Redis ZSet 类型及操作命令实现一个延迟队列,用时间戳(当前时间+延迟的分钟数)作为元素的 score 存入ZSet。只需获取zset中的第一条记录,即最早时间下单数据,如果该记录未超时支付,剩下的订单必然未超时。

public class DelayQueueComponent {
	private final static String delayQueueKey = "delay:queue";

	@Autowired
	private RedisService redisService;

	// 将延迟对象推送至队列中
	public void add(Object obj, long seconds) {
		this.redisService.zadd(delayQueueKey, obj, getDelayTimeMills(seconds));
	}

	public void startMonitor() {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				monitorQueue();
			}
		};
		System.out.println("start monitor delay queue.");
		new Thread(runnable).start();
		System.out.println("finish start monitor delay queue.");
	}

	private void monitorQueue() {
		while(true) {
			if(lock()) {
				//从延迟队列中拿一个最旧的
				TypedTuple tuple = this.redisService.zrangeFirst(delayQueueKey);
				// isCanPush 判断是否延迟
				if(isCanPush(tuple)) {
					//删除掉处理的延迟消息
					this.redisService.zremFirst(delayQueueKey);
					//释放锁
					releaseLock();
				}else {
					releaseLock();
				}
			}
			sleep();
		}
	}

	 // 是否可推送
	private boolean isCanPush(TypedTuple tuple) {
		if(tuple == null) {
			return false;
		}
		long currentTimeMills = System.currentTimeMillis();
		//当前时间小于延迟时间时,获取对象进行业务逻辑处理
		if(currentTimeMills >= tuple.getScore()) {
			return true;
		}
		return false;
	}
} 
  

这种方案的缺点:1)、消息处理失败,不能恢复处理。
      2)、数据量大时,zset 性能有问题,多定义几个 zset,增加了内存和定时器去读的复杂度。

四、RabbitMQ 实现


利用 RabbitMQ 的死信队列(Dead-Letter-Exchage)机制实现,在 queueDeclare 方法中加入 “x-dead-letter-exchage”实现:

x-dead-letter-exchage:过期消息路由转发(转发器类型)
x-dead-letter-routing-key:当消息达到过期时间由该 exchange 安装配置的 x-dead-letter-routing-key 转发到指定队列,最后被消费者消费

 我们需要两个队列,一个用来做主队列,真正的投递消息;另一个用来延迟处理消息。

channel.queueDeclare("MAIN_QUEUE",true,false,false,null);
channel.queueBind("MAIN_QUEUE","amq.direct","MAIN_QUEUE");

HashMap arguments = new HashMap();
arguments.put("x-dead-letter-exchange","amq.direct");
arguments.put("x-dead-letter-routing-key","MAIN_QUEUE");

channel.queueDeclare("DELAY_QUEUE",true,false,false,arguments);

放入延迟消息(DeliveryMode 等于 2 说明这个消息是 persistent 的):

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
AMQP.BasicProperties properties = builder.expiration(
                    String.valueOf(task.getDelayMillis())).deliveryMode(2).build();
channel.basicPublish("","DELAY_QUEUE",properties,SerializationUtils.serialize(task));

这种方案的缺点:1)、笔者之前做 MQ 性能测试时,在公司的服务器上单机 TPS 接近 3W,如果是中小型企业级应用基本满足。但如果大量的消息积压得不到投递,性能仍然是个问题。
       2)、依赖于 RabbitMQ 的运维,复杂度和成本提高。

--------------------------------Game Over---------------------------Thank you--------------------

你可能感兴趣的:(面试)