订单付款倒计时实现方案
更多内容,移步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 消息队列的 TTL和 DLX这两个属性间接实现的。
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 的运维,复杂度和成本提高。
订单付款倒计时实现方案相关推荐
- Uniapp实现多个订单批量待付款倒计时
Uniapp实现多个订单批量待付款倒计时 思路 当客户不付款生成一个待付款的订单这个待付款的订单需要获取它30分钟后的时间然后保存到数据库 然后前台将当前时间减去从数据库拿到的时间进行倒计时 注意 必 ...
- SAP 采购订单显示含税价制作方案
SAP 采购订单显示含税价制作方案 轻松解决SAP系统采购信息计量中物料价格不能保存含税价问题 我们在和供应商谈价时,大部分国内供应商的报价都是含税的,然而我们现在在系统中维护采购信息记录时, 只能输 ...
- 分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...
前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...
- java订单到期自动取消_订单自动过期实现方案
需求分析: 24小时内未支付的订单过期失效. 解决方案 被动设置:在查询订单的时候检查是否过期并设置过期状态. 定时调度:定时器定时查询并过期需要过期的订单. 延时队列:将未支付的订单放入一个延时队列 ...
- 淘宝卖家店铺订单API接口同步方案
获取淘宝卖家店铺订单背景: 订单是卖家的核心数据,卖家的很多日常工作都是围绕着订单展开,应用的基本功能就是要保证订单实时.完整的展示在卖家面前.由于API请求依赖于网络,存在 着网络不稳定和同步时间长 ...
- 在表格中展示订单的倒计时定时器,用一个定时器显示多个倒计时
问题背景 项目中有个需求是要展示订单列表中待支付的订单显示倒计时,在订单支付后,或者 超时后刷新列表,更新状态 解决思路 项目使用的vue框架,就要运用vue的数据驱动试图这一特性,所以我们需要添加一 ...
- java分布式库存系统_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...
前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...
- uni-app实现订单支付倒计时,不会随着返回重新计时
uni-app实现订单支付倒计时,不会随着返回重新计时 uni-app实现订单支付倒计时 最近开发时有一个倒计时功能,一开始使用uni-app中自带的uni-countdown倒计时,可以实现普通倒计 ...
- 取消超时订单及延迟处理方案
使用场景 方案 优化 1.使用场景 12306订单30分钟自动取消? 淘宝订单超过2小时自动取消? 美团外卖订单超过30分钟自动取消? 抢购如何处理?被动更新 + crond 主动更新两种方式,因为是 ...
最新文章
- 把热带雨林搬进办公室!这样的互联网公司!我愿意加班至死!
- ckc交易什么意思_限价委托是什么意思?有限制的委托交易
- Linux时间子系统之(一):时间的基本概念【转】
- Word2013中怎样设置同一文档内粘贴选项
- 【Go API 开发实战 5】基础1:启动一个最简单的 RESTful API 服务器
- express-partials与express4.x不兼容问题
- 修改Win7远程桌面端口
- 带你全面掌握高级知识点!深入理解java虚拟机pdf下载
- python 连接 mysql
- JAVA 滑块拼图验证码
- android 指纹识别驱动 win10,win10怎么添加指纹识别?Win10 Windows Hello指纹登录设置教程...
- 在一张图片的某个特定位置添加另外一张图片
- Win11怎么不让软件联网?Win11禁止某个软件联网的方法
- 基于bing 搜索引擎和 Microsoft Academic Search 的高校申请指南的NABC分析
- 与计算机相关的潜在健康风险是什么,医疗安全与风险管理.新.ppt
- 记 [GXYCTF2019]Ping Ping Ping 1
- 数据标注这份工作,不是你想做就能做
- 冰河指南AI技术社区基于ChatGPT正式启动运营
- weka 执行结果MySQL_WEKA数据解析实验.doc
- 【网安自学】XSS漏洞防御