架构师之超时未支付的订单进行取消操作的几种解决方案

今天给大家上一盘硬菜,并且是支付中非常重要的一个技术解决方案,有这块业务的同学注意自己尝试一把哈!

一、需求如下:

  • 生成订单30分钟未支付,自动取消

  • 生成订单60秒后,给用户发短信

对上述的需求,我们给一个专业的名字来形容,那就是延时任务。你可能会问延时任务和定时任务有啥区别呢?

一共有以下几点区别

  • 定时任务有明确的触发时间,延时任务没有

  • 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期

  • 定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

二、解决方案

(1)数据库轮询

该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作

1)引入依赖

<dependency><groupId>org.quartz-scheduler</groupId><artifactId>quartz</artifactId><version>2.2.2</version>
</dependency>

2)创建Demo类实现

public class MyJobDemo implements Job {public void execute(JobExecutionContext context)throws JobExecutionException {System.out.println("我去访问数据库啦。。。");}public static void main(String[] args) throws Exception {// 创建任务JobDetail jobDetail = JobBuilder.newJob(MyJobDemo.class).withIdentity("job1", "group1").build();// 创建触发器 每3秒钟执行一次Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group3").withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever()).build();Scheduler scheduler = new StdSchedulerFactory().getScheduler();// 将任务及其触发器放入调度器scheduler.scheduleJob(jobDetail, trigger);// 调度器开始调度任务scheduler.start();}
}

3)运行结果每3秒输出:

我去访问数据库啦。。。

优点:简单易行,支持集群操作

缺点:

(1)对服务器内存消耗大

(2)存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟

(3)假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大

(2)JDK的延迟队列

该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。

  • Poll():获取并移除队列的超时元素,没有则返回空

  • take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。

1)定义一个类OrderDelay实现Delayed

public class OrderDelay implements Delayed {private String orderId;private long timeout;OrderDelay(String orderId, long timeout) {this.orderId = orderId;this.timeout = timeout + System.nanoTime();}public int compareTo(Delayed other) {if (other == this)return 0;OrderDelay t = (OrderDelay) other;long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));return (d == 0) ? 0 : ((d < 0) ? -1 : 1);}// 返回距离你自定义的超时时间差值public long getDelay(TimeUnit unit) {return unit.convert(timeout - System.nanoTime(),TimeUnit.NANOSECONDS);}void print() {System.out.println(orderId+"编号的订单即将删除啦。。。。");}
}

2)运行的测试Demo为,我们设定延迟时间为3秒

public class DelayQueueDemo {public static void main(String[] args) {  List<String> list = new ArrayList<String>();  list.add("00000001");  list.add("00000002");  list.add("00000003");  list.add("00000004");  list.add("00000005");  DelayQueue<OrderDelay> queue = newDelayQueue<OrderDelay>();  long start = System.currentTimeMillis();  for(int i = 0;i<5;i++){  //延迟三秒取出queue.put(new OrderDelay(list.get(i),  TimeUnit.NANOSECONDS.convert(3,TimeUnit.SECONDS)));  try {  queue.take().print();  System.out.println("After " +  (System.currentTimeMillis()-start) + " MilliSeconds");  } catch (InterruptedException e) {}  }  }  
}

3)输出如下:

00000001编号的订单即将删除啦。。。。
After 3003 MilliSeconds
00000002编号的订单即将删除啦。。。。
After 6006 MilliSeconds
00000003编号的订单即将删除啦。。。。
After 9006 MilliSeconds
00000004编号的订单即将删除啦。。。。
After 12008 MilliSeconds
00000005编号的订单即将删除啦。。。。
After 15009 MilliSeconds

优点:效率高,任务触发时间延迟低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4)代码复杂度较高

(3)时间轮算法

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。

这样可以看出定时轮由个3个重要的属性参数

  • ticksPerWheel(一轮的tick数)

  • tickDuration(一个tick的持续时间)

  • timeUnit(时间单位)

例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。

如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)

具体实现(使用Netty的HashedWheelTimer来实现):

1)引依赖:

<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.24.Final</version>
</dependency>

2)创建HashedWheelTimerTest测试:

public class HashedWheelTimerTest {static class MyTimerTask implements TimerTask{boolean flag;public MyTimerTask(boolean flag){this.flag = flag;}public void run(Timeout timeout) throws Exception {System.out.println("我去数据库删除订单了。。。。");this.flag =false;}}public static void main(String[] argv) {MyTimerTask timerTask = new MyTimerTask(true);Timer timer = new HashedWheelTimer();timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);int i = 1;while(timerTask.flag){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("过去了"+i+"秒");i++;}}
}

3)输出如下:

过去了1秒
过去了2秒
过去了3秒
过去了4秒
过去了5秒
我去数据库删除订单了。。。。
过去了6秒

优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4)redis缓存

思路一

利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值

相关的命令操作:

  • 添加元素:ZADD key score member [[score member] [score member] …]

  • 按顺序查询元素:ZRANGE key start stop [WITHSCORES]

  • 查询元素score:ZSCORE key member

  • 移除元素:ZREM key member [member …]

具体实现:我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时,具体如下图所示

 1)代码实现:

public class AppTest {private static final String ADDR = "127.0.0.1";private static final int PORT = 6379;private static JedisPool jedisPool = new JedisPool(ADDR, PORT);public static Jedis getJedis() {return jedisPool.getResource();}//生产者,生成5个订单放进去public void productionDelayMessage(){for(int i=0;i<5;i++){//延迟3秒Calendar cal1 = Calendar.getInstance();cal1.add(Calendar.SECOND, 3);int second3later = (int) (cal1.getTimeInMillis() / 1000);AppTest.getJedis().zadd("OrderId",second3later,"OID0000001"+i);System.out.println(System.currentTimeMillis()+"ms:redis生成了一个订单任务:订单ID为"+"OID0000001"+i);}}//消费者,取订单public void consumerDelayMessage(){Jedis jedis = AppTest.getJedis();while(true){Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 1);if(items == null || items.isEmpty()){System.out.println("当前没有等待的任务");try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}continue;}int  score = (int) ((Tuple)items.toArray()[0]).getScore();Calendar cal = Calendar.getInstance();int nowSecond = (int) (cal.getTimeInMillis() / 1000);if(nowSecond >= score){String orderId = ((Tuple)items.toArray()[0]).getElement();jedis.zrem("OrderId", orderId);System.out.println(System.currentTimeMillis() +"ms:redis消费了一个任务:消费的订单OrderId为"+orderId);}}}public static void main(String[] args) {AppTest appTest =new AppTest();appTest.productionDelayMessage();appTest.consumerDelayMessage();}
}

2)输出的时候会看到,几乎都是3秒后进行订单的消费,然而它有一个致命的伤,高并发条件下,多消费者会取到同一个订单号,也就是我们常说的超卖问题,显然,出现了多个线程消费同一个资源的情况。

针对这个问题的解决方案是:

(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。

(2)对ZREM的返回值进行判断,只有大于0的时候,才消费数据,于是consumerDelayMessage()方法里的

if(nowSecond >= score){String orderId = ((Tuple)items.toArray()[0]).getElement();jedis.zrem("OrderId", orderId);System.out.println(System.currentTimeMillis()+"ms:redis消费了一个任务:消费的订单OrderId为"+orderId);
}

修改为:

if(nowSecond >= score){String orderId = ((Tuple)items.toArray()[0]).getElement();Long num = jedis.zrem("OrderId", orderId);if( num != null && num>0){System.out.println(System.currentTimeMillis()+"ms:redis消费了一个任务:消费的订单OrderId为"+orderId);}
}

修改后代码输出即为正常。

思路二

该方案使用redis的Keyspace Notifications,利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。值得注意的是redis版本要在2.8以上。

具体实现:

1)向redis.conf中,加入一条配置

notify-keyspace-events Ex

2)代码实现:

public class RedisTest {private static final String ADDR = "127.0.0.1";private static final int PORT = 6379;private static JedisPool jedis = new JedisPool(ADDR, PORT);private static RedisSub sub = new RedisSub();public static void init() {new Thread(new Runnable() {public void run() {jedis.getResource().subscribe(sub, "__keyevent@0__:expired");}}).start();}public static void main(String[] args) throws InterruptedException {init();for(int i =0;i<10;i++){String orderId = "OID000000"+i;jedis.getResource().setex(orderId, 3, orderId);System.out.println(System.currentTimeMillis()+"ms:"+orderId+"订单生成");}}static class RedisSub extends JedisPubSub {public void onMessage(String channel, String message) {System.out.println(System.currentTimeMillis()+"ms:"+message+"订单取消");}}
}

3)输出体现3秒过后,订单取消了

redis的pub/sub机制存在一个硬伤,官网内容如下:

Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost.

直译过来的意思:

Redis的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。

故,这个方案不太推荐使用。如你对可靠性要求不是很高时,可以使用。

优点:

(1)由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。

(2)做集群扩展相当方便

(3)时间准确度高

缺点:

需要额外进行redis维护

(5)使用消息队列

可以采用RabbitMQ的延时队列。RabbitMQ具有以下两个特性,可以实现延迟队列

RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter

lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。

优点: 

高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。

缺点:

本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/624640.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【算法与数据结构】63、LeetCode不同路径 II

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;参考【算法与数据结构】62、LeetCode不同路径的题目&#xff0c;可以发现本题仅仅是多了障碍物。我们还…

springboot下载图片的简单处理方式

参考地址 springboot&#xff1a;各种下载文件的方式_springboot下载文件-CSDN博客 开箱即用实战 GetMapping("/t1")public void down1(HttpServletResponse response) throws Exception {response.reset();response.setContentType("application/octet-strea…

STM32F103标准外设库——认识STM32(一)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…

MySQL存储引擎索引事务

一.存储引擎 1.创建的存储引擎 MyISAM 不支持事务、也不支持外键&#xff0c;索引采用非聚集索引&#xff0c;其优势是访问的速度快&#xff0c;对事务完整性没有要求&#xff0c;以 SELECT 、 INSERT 为主的应用基本上都可以使用这个存储引擎来创建表。 MyISAM 的表在磁盘上…

从零学Java 线程的状态

Java 线程的状态 文章目录 Java 线程的状态线程的基础状态1 常见方法1.1 休眠1.2 放弃1.3 加入1.4 优先级1.5 线程打断1.6 守护线程1.7 线程的状态 - 等待 2 线程安全问题2.1 线程同步: 同步代码块2.2 线程同步: 同步方法2.3 同步规则2.4 线程的状态 - 阻塞2.5 特殊现象: 死锁 …

新版网易滑块

突然发现脸皮厚根本没用&#xff0c;大冬天的&#xff0c;风吹过来还是会冷。 大哥们多整件衣裳&#xff0c;好冷&#xff01;&#xff01;&#xff01;&#xff01; 网易更新了&#xff0c;这俩 dt跟f值。 dt为 这里返回的&#xff0c;忽略掉他。 data参数中的d值&#xff…

未来气膜体育馆的发展趋势是什么?

未来气膜体育馆的发展趋势是多方面的&#xff0c;以下是其中几个方面的趋势。 起初&#xff0c;随着人们对体育运动的需求不断增加&#xff0c;气膜体育馆的建设和使用将成为一种趋势。气膜体育馆具有灵活性和可移动性的特点&#xff0c;可以快速搭建和拆除&#xff0c;能够适…

低代码平台,新型应用程序开发神器

目前低代码平台如火如荼。这一新兴技术为企业提供了一种高效、灵活、快速开发应用程序的方法&#xff0c;并在短时间内取得了巨大成功。然而&#xff0c;我们不得不面对低代码平台的优劣以及其所带来的挑战。本文将深入探讨低代码平台在不同情况下的优劣势&#xff0c;并与您分…

AI能否真的取代程序员?

一个Java程序员从接到需求到最终开发完功能的流程大致如下 首先是理解原型图&#xff0c;设计表结构根据表结构编写增删改查代码与前端联调&#xff0c;完成功能开发 那么以上步骤AI能不能完成呢&#xff1f; 让我们试一下&#xff1a; 第一步&#xff0c;使用 AI助手 解析流…

设计模式之状态模式【行为型模式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某…

postman环境变量全局变量设置

postman环境变量、全局变量设置 在公司中&#xff0c;一般会存在开发环境、测试环境、线上环境等&#xff0c;如果需要在不 同的环境下切换做接口测试&#xff0c;显然我们需要把所有接口的域名进行修改&#xff0c;如果接 口测试用例较多&#xff0c;那么修改会非常费力&…

Redis基础系列-哨兵模式

Redis基础系列-哨兵模式 文章目录 Redis基础系列-哨兵模式1. 引言2. 什么是哨兵模式&#xff1f;3. 哨兵模式的配置4. 哨兵模式的启动和验证4.1 主master宕机&#xff0c;看会出现什么问题4.2 重启6379主机 5. 哨兵模式的工作原理和选举原理5.1. SDown主观下线&#xff08;Subj…

基于Python实现地标景点识别

目录 前言简介地标景点识别的背景 地标景点识别的原理卷积神经网络&#xff08;CNN&#xff09;的基本原理地标景点识别的工作流程 使用Python实现地标景点识别的步骤数据收集数据预处理构建卷积神经网络模型模型训练 参考文献 前言 简介 地标景点识别是一种基于计算机视觉技术…

Qt SDL2播放Wav音频

这里介绍两种方法来实现Qt播放Wav音频数据。 方法一&#xff1a;使用QAudioOutput pro文件中加入multimedia模块。 #include <QApplication> #include <QFile> #include <QAudioFormat> #include <QAudioOutput>int main(int argc, char *argv[]) {…

欧姆龙plc学习NJ系列CJ系列资料Sysmac Studio编程软件视频教程

Sysmac Studio是欧姆龙公司开发的一款集成开发环境&#xff08;IDE&#xff09;&#xff0c;用于编程和配置欧姆龙PLC&#xff08;可编程逻辑控制器&#xff09;和其他自动化设备。Sysmac Studio支持以下型号的欧姆龙PLC&#xff1a;1. NJ系列&#xff1a;NJ501、NJ301、NJ101、…

【算法】算法(模拟、指针等)解决字符串类题目(C++)

文章目录 1. 前言2. 解决 字符串类算法题14.最长公共前缀5.最长回文子串67.二进制求和43.字符串相乘 1. 前言 字符串题目有很多种&#xff0c;这里筛选几个考察模拟、双指针等的题目&#xff0c;并用相关算法解决。 2. 解决 字符串类算法题 14.最长公共前缀 思路 题意分析&…

【固态钽表面贴装电容】 MIL-PRF-55365 美军标

钽电解电容器是体积效率、电气参数稳定、高可靠性和长使用寿命是主要考虑因素的应用的首选。 钽/氧化钽/二氧化锰系统的稳定性和对高温的耐受性使固体钽消泡器成为当今表面贴装组装技术的合适选择。 钽不是纯净状态的。 相反&#xff0c;它通常存在于许多氧化物矿物中&#xf…

编曲混音FL Studio21.2对电脑有什么配置要求

FL Studio 21是一款非常流行的音乐制作软件&#xff0c;它可以帮助音乐人和制作人创作出高质量的音乐作品。然而&#xff0c;为了保证软件的稳定性和流畅性&#xff0c;用户需要知道FL Studio 21对电脑的配置要求。本文将介绍FL Studio 21的配置要求&#xff0c;以帮助用户选择…

SQL基础知识4

一、内置函数 1、数值函数 1.1基本函数 1.2时间函数 二、存储过程 1、概述 2、分类 3、创建 4、总结 多了很多传递参数 三、存储函数 说明定义 例子 案例1 方法一 方法二 案例2 例子3 对比 复习

【贪心】受标签影响的最大值

/** 贪心&#xff1a;要想使子集分数最大&#xff0c;应选取values中较大的值。* 思路&#xff1a;使用一个哈希表来记录每个标签使用的次数&#xff0c;相同标签使其不超过useLimit限制* 对values进行降序排序&#xff0c;并对其进行遍历只要标签使用次数不超过限制就…