更多内容,移步IT-BLOG

当使用 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();}/*** 用于延迟队列内部比较排序,当前时间的延迟时间  -  比较对象的延迟时间*/@Overridepublic int compareTo(Delayed o) {return Long.valueOf(this.expTime).compareTo(Long.valueOf(((DelayItem)o).expTime));}/*** 获得延迟时间,过期时间-当前时间(单位ms)*/@Overridepublic long getDelay(TimeUnit unit) {return this.expTime-System.currentTimeMillis();}
}

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

public class DelayQueueTest implements Runnable{//创建一个延迟队列private    DelayQueue<Delayed> item = new DelayQueue<>();@Overridepublic 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<Task>
同时,启动一个 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";@Autowiredprivate RedisService redisService;// 将延迟对象推送至队列中public void add(Object obj, long seconds) {this.redisService.zadd(delayQueueKey, obj, getDelayTimeMills(seconds));}public void startMonitor() {Runnable runnable = new Runnable() {@Overridepublic 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<Object> tuple = this.redisService.zrangeFirst(delayQueueKey);// isCanPush 判断是否延迟if(isCanPush(tuple)) {//删除掉处理的延迟消息this.redisService.zremFirst(delayQueueKey);//释放锁releaseLock();}else {releaseLock();}}sleep();}}// 是否可推送private boolean isCanPush(TypedTuple<Object> tuple) {if(tuple == null) {return false;}long currentTimeMills = System.currentTimeMillis();//当前时间小于延迟时间时,获取对象进行业务逻辑处理if(currentTimeMills >= tuple.getScore()) {return true;}return false;}
}

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

四、RabbitMQ 实现


利用 RabbitMQ做延时队列是比较常见的一种方式,而实际上 RabbitMQ自身并没有直接支持提供延迟队列功能,而是通过 RabbitMQ 消息队列的 TTLDLX这两个属性间接实现的。

Time To Live(TTL):消息的存活时间,RabbitMQ可以通过 x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒

RabbitMQ 可以从两种维度设置消息过期时间,分别是队列和消息本身:
【1】设置队列过期时间,那么队列中所有消息都具有相同的过期时间。
【2】设置消息过期时间,对队列中的某一条消息设置过期时间,每条消息TTL都可以不同。
如果同时设置队列和队列中消息的TTL,则TTL值以两者中较小的值为准。而队列中的消息存在队列中的时间,一旦超过TTL过期时间则成为Dead Letter(死信)。

队列出现 Dead Letter的情况有:
【1】消息或者队列的TTL过期;
【2】队列达到最大长度;
【3】消息被消费端拒绝(basic.reject or basic.nack);

应用场景:一般应用在当正常业务处理时出现异常时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理;重试队列在重试16次(默认次数)将消息放入死信队列。利用 RabbitMQ 的死信队列(Dead-Letter-Exchage)机制实现,在 queueDeclare 方法中加入 “x-dead-letter-exchage”实现:

RabbitMQ的 Queue可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchage:过期消息路由转发(转发器类型)
x-dead-letter-routing-key:当消息达到过期时间由该 exchange 安装配置的 x-dead-letter-routing-key 转发到指定队列,最后被消费者消费

下边结合一张图看看如何实现超30分钟未支付关单功能,我们将订单消息A0001发送到延迟队列order.delay.queue,并设置x-message-tt消息存活时间为30分钟,当到达30分钟后订单消息A0001成为了Dead Letter(死信),延迟队列检测到有死信,通过配置x-dead-letter-exchange,将死信重新转发到能正常消费的队列,直接监听队列处理关闭订单逻辑即可。 

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

channel.queueDeclare("MAIN_QUEUE",true,false,false,null);
channel.queueBind("MAIN_QUEUE","amq.direct","MAIN_QUEUE");HashMap<String,Object> arguments = new HashMap<String,Object>();
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 的运维,复杂度和成本提高。

订单付款倒计时实现方案相关推荐

  1. Uniapp实现多个订单批量待付款倒计时

    Uniapp实现多个订单批量待付款倒计时 思路 当客户不付款生成一个待付款的订单这个待付款的订单需要获取它30分钟后的时间然后保存到数据库 然后前台将当前时间减去从数据库拿到的时间进行倒计时 注意 必 ...

  2. SAP 采购订单显示含税价制作方案

    SAP 采购订单显示含税价制作方案 轻松解决SAP系统采购信息计量中物料价格不能保存含税价问题 我们在和供应商谈价时,大部分国内供应商的报价都是含税的,然而我们现在在系统中维护采购信息记录时, 只能输 ...

  3. 分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...

    前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...

  4. java订单到期自动取消_订单自动过期实现方案

    需求分析: 24小时内未支付的订单过期失效. 解决方案 被动设置:在查询订单的时候检查是否过期并设置过期状态. 定时调度:定时器定时查询并过期需要过期的订单. 延时队列:将未支付的订单放入一个延时队列 ...

  5. 淘宝卖家店铺订单API接口同步方案

    获取淘宝卖家店铺订单背景: 订单是卖家的核心数据,卖家的很多日常工作都是围绕着订单展开,应用的基本功能就是要保证订单实时.完整的展示在卖家面前.由于API请求依赖于网络,存在 着网络不稳定和同步时间长 ...

  6. 在表格中展示订单的倒计时定时器,用一个定时器显示多个倒计时

    问题背景 项目中有个需求是要展示订单列表中待支付的订单显示倒计时,在订单支付后,或者 超时后刷新列表,更新状态 解决思路 项目使用的vue框架,就要运用vue的数据驱动试图这一特性,所以我们需要添加一 ...

  7. java分布式库存系统_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...

    前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...

  8. uni-app实现订单支付倒计时,不会随着返回重新计时

    uni-app实现订单支付倒计时,不会随着返回重新计时 uni-app实现订单支付倒计时 最近开发时有一个倒计时功能,一开始使用uni-app中自带的uni-countdown倒计时,可以实现普通倒计 ...

  9. 取消超时订单及延迟处理方案

    使用场景 方案 优化 1.使用场景 12306订单30分钟自动取消? 淘宝订单超过2小时自动取消? 美团外卖订单超过30分钟自动取消? 抢购如何处理?被动更新 + crond 主动更新两种方式,因为是 ...

最新文章

  1. 把热带雨林搬进办公室!这样的互联网公司!我愿意加班至死!
  2. ckc交易什么意思_限价委托是什么意思?有限制的委托交易
  3. Linux时间子系统之(一):时间的基本概念【转】
  4. Word2013中怎样设置同一文档内粘贴选项
  5. 【Go API 开发实战 5】基础1:启动一个最简单的 RESTful API 服务器
  6. express-partials与express4.x不兼容问题
  7. 修改Win7远程桌面端口
  8. 带你全面掌握高级知识点!深入理解java虚拟机pdf下载
  9. python 连接 mysql
  10. JAVA 滑块拼图验证码
  11. android 指纹识别驱动 win10,win10怎么添加指纹识别?Win10 Windows Hello指纹登录设置教程...
  12. 在一张图片的某个特定位置添加另外一张图片
  13. Win11怎么不让软件联网?Win11禁止某个软件联网的方法
  14. 基于bing 搜索引擎和 Microsoft Academic Search 的高校申请指南的NABC分析
  15. 与计算机相关的潜在健康风险是什么,医疗安全与风险管理.新.ppt
  16. 记 [GXYCTF2019]Ping Ping Ping 1
  17. 数据标注这份工作,不是你想做就能做
  18. 冰河指南AI技术社区基于ChatGPT正式启动运营
  19. weka 执行结果MySQL_WEKA数据解析实验.doc
  20. 【网安自学】XSS漏洞防御

热门文章

  1. js延迟加载的方式有哪些
  2. python作爱心词云图
  3. python123部分编程题
  4. 6.人工智能原理-隐藏层:神经网络为什么working?
  5. 达梦数据库表数据或者物理文件误删除或者损坏恢复方法
  6. BootStrap 入门教程学习摘要笔记
  7. php公众号推荐,推荐几个值得关注的微信公众号
  8. 实验五:MSI时序逻辑部件应用(彩灯流水电路的设计)
  9. 视频直播带货智能千面模板是怎么坑人的
  10. egg-swagger-doc 图形验证码解决方案