阅读 191

订单支付超时未支付关闭订单的解决方案

双11抢单是最常见的场景,抢单不支付会占据大批量资源,如商品库存。如何取消过期订单是架构师必须面对的问题。主要有以下几种方案:

扫表实现

原理

  • 通过定时任务轮询扫描订单表,超时的批量修改状态

优点

  • 实现非常简单

缺点

  • 大量数据集,对服务器内存消耗大。    

  • 数据库频繁查询,订单量大的情况下,IO是瓶颈。

  • 存在延迟,间隔短则耗资源,间隔长则时效性差,两者是一对矛盾。

  • 不易控制,随着定时业务的增多和细化,每个业务都要对订单重复扫描,引发查询浪费

java延迟队列实现

原理

  • 通过DelayQueue,每下一单,放入一个订单元素并实现getDelay()方法,方法返回该元素距离失效还

剩余的时间,当<=0时元素就失效,就可以从队列中获取到。启用线程池对数据监听,一旦捕获失效订 单,取出之后,调用取消逻辑进行处理 image.png

优点

  • 基于jvm内存,效率高,任务触发时间延迟低

缺点

  • 存在jvm内存中,服务器重启后,数据全部丢失

  • 依赖代码硬编码,集群扩展麻烦

  • 依赖jvm内存,如果订单量过大,无界队列内容扩充,容易出现OOM

  • 需要代码实现,多线程处理业务,复杂度较高

  • 多线程处理时,数据频繁触发等待和唤醒,多了无谓的竞争

消息队列实现

原理

  • 设置两个队列,每下一单放一条进延迟队列,设定过期时间。消息一旦过期,获取并放入工作队列,由

consumer获取,唤起超时处理逻辑 image.png

  • 如果采用的是RabbitMQ,其本身没有直接支持延迟队列功能,可以针对Queue和Message设置 x-message-ttl,用消息的生存时间,和死信队列来实现,具体有两种手段

    • A: 通过队列属性设置,队列中所有消息都有相同的过期时间,粗粒度,编码简单

    • B: 对消息进行单独设置,每条消息TTL可以不同,细粒度,但编码稍微复杂

  • RocketMQ不支持自定义延时时间设置

        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");         producer.setNamesrvAddr("192.168.10.11:9876");         producer.start();         Message msg1 = new Message(                 JmsConfig.TOPIC,                 "订单001".getBytes());         msg1.setDelayTimeLevel(2);//延迟5秒         Message msg2 = new Message(                 JmsConfig.TOPIC,                 "订单001".getBytes());         msg2.setDelayTimeLevel(4);//延迟30秒         SendResult sendResult1 = producer.send(msg1);         SendResult sendResult2 = producer.send(msg2         producer.shutdown(); 复制代码

RocketMQ 不支持任意时间自定义的延迟消息,仅支持内置预设值的18种延迟时间间隔的延迟消息

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; 复制代码

优点

  • 可以随时在队列移除,实现实时取消订单,及时恢复订单占用的资源(如商品)

  • 消息存储在mq中,不占用应用服务器资源

  • 异步化处理,一旦处理能力不足,consumer集群可以很方便的扩容

缺点

  • 可能会导致消息大量堆积

  • mq服务器一旦故障重启后,持久化的队列过期时间会被重新计算,造成精度不足

  • 死信消息可能会导致监控系统频繁预

redis实现

原理:

  • 利用redis的notify-keyspace-events,该选项默认为空,改为Ex开启过期事件,配置消息监听。每下一单在redis中放置一个key(如订单id),并设置过期时间

优点

  • 消息都存储在Redis中,不占用应用内存

  • 外部redis存储,应用down机不会丢失数据

  • 做集群扩展相当方便

  • 依赖redis超时,时间准确度高

缺点

  • 订单量大时,每一单都要存储redis内存,需要大量redis服务器资源

实现

1、redis 开启key过期通知

notify-keyspace-events Ex 复制代码

2、 key过期Listener

@Component public class RedisKeyExpiredListener extends JedisPubSub {     private final static Logger logger = LoggerFactory.getLogger(RedisKeyExpiredListener.class);     @Autowired     private PushMsgService pushMsgService;     private static final String PREFIX_EVENT_KEY = "xxx:event:sms:";     @Override     public void onMessage(String channel, String message) {         if (message.startsWith(PREFIX_EVENT_KEY)) {             pushMsgService.pushSmsForSms(Long.parseLong(message.substring(message.lastIndexOf(":") + 1)));         }     } } 复制代码

3、注册redis key过期Listener

@PostConstruct private void init() { // 因为此方法阻塞线程 new Thread(new Runnable() {     @Override     public void run() { // 订阅redis key过期时间,需要reids 服务器配置notify-keyspace-events Ex jedisPool.getResource().subscribe(redisKeyExpiredListener, "__keyevent@2__:expired");     } }).start(); } 复制代码

keyevent@2:expired

  • __keyevent必须以此开头;

  • @2 表示监听第二个数据库;

  • :expired 表示过期事件

4、设置过期时间

Jedis jedis = jedisPool.getResource(); jedis.select(2); jedis.set(PREFIX_EVENT_KEY + teaching.getId(), String.valueOf(teaching.getId())); jedis.expire(PREFIX_EVENT_KEY + teaching.getId(), expireTime.intValue()); 复制代码

被动取消

原理

  • 在每次用户查询订单的时候,判断订单时间,超时则同时完成订单取消业务

优点

  • 实现极其简单

  • 不会有额外的性能付出

  • 不依赖任何外部中间件,只是应用逻辑的处理

缺点

  • 延迟度不可控,如果用户一直没触发查询,则订单一直挂着,既不支付也未取消,库存也就被占着


作者:silly8543
链接:https://juejin.cn/post/6996088295612481572


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐