关于订单超时自动关闭的方案简单总结

订单超时关闭这类场景在业务中比较常见,典型的是用户下单之后,30分钟未付款,自动关闭订单;除此之外类似的还有用户预约抢购,抢购前10分钟发送消息通知;

通常一般有以下几种方案

  1. RocketMQ方案,消息方案,利用延迟消息的功能实现
  2. 分布式调度批处理任务方案,利用ScheduleX定时调度的能力,通过mapreduce能力为机器分片,每个机器查询一定范围的数据进行处理。

本文将简单介绍2种方案

延迟消息

RocketMQ是阿里的开源消息中间件,它支持18个等级的延迟投递,最大2个小时

投递等级(delay level) 延迟时间 投递等级(delay level) 延迟时间
1 1s 10 6min
2 5s 11 7min
3 10s 12 8min
4 30s 13 9min
5 1min 14 10min
6 2min 15 20min
7 3min 16 30min
8 4min 17 1h
9 5min 18 2h

开发成本很低

ini 复制代码
 Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
 message.setDelayTimeLevel(3);
 producer.send(message);

RocketMQ为每个延迟级别都设置了单独的队列,写入延迟消息的时候,将消息些到这些队列中;定时任务取出到期的消息,发送到原来的队列。

无论是延迟消息还是普通消息,RocketMQ broker都会先将消息数据写入到commitLog文件里去,并将索引offset写入到ComsumerQueue队列中去,ComsumerQueue相当于一个逻辑队列,并不保存数据,当消费者拉去消息时在从commitLog中取数据。

延迟消息不同的是,写入commitLog后,broker不会将offset写入到topic的ComsumerQueue里去,而是写入到特定Schedule Topic下的队列去,每个延迟级别都设置了单独的队列,broker会通过SchduleMessageService来拉队列,如果消息到期,再将消息回复到原来的topic和queueId写到commitLog里去,此时就和普通的消息一致了。

当然除了RocketMQ方案之外,我们还可以用分布式调度框架来解决

分布式调度

通过定时调度,查询表根据订单时间排序,查出出到期的订单,然后处理,这个也是一个很容易想到的方案,如果同一时间有大量数据,一次查询大量的数据处理会很慢。所以我们希望能在数据分片,集群中的每个机器只处理自己分片的数据

假如对于TPS4000的订单系统来说,订单超时关闭增量数据最多4000,每台woker处理的数量是(4000/机器数量),一半来说处理也可以发送到MQ中去,这一过程很快,即使2台机器,按秒调度也能有很不错的sla

如果自己实现数据的分片和任务下发成本比较高,我们可以使用分布式调度框架ScheduleX来完成,利用mapreduce模式很容易开发分布式任务。

ScheduleX的mapreduce使用

java 复制代码
public class OrderJobProcessor extends MapJobProcessor {
    private static final int pageSize = 1000;

    static class Task {
        private int startId;
        private int endId;
        public PageTask(int startId, int endId) {
             this.startId = startId;
             this.endId = endId;
        }
        public int getStartId() {
              return startId;
        }
        public int getEndId() {
              return endId;
        }
    }

    @Override
    public ProcessResult process(JobContext context) {
        String taskName = context.getTaskName();
        Object task = context.getTask();
        if (taskName.equals(WorkerConstants.MAP_TASK_ROOT_NAME)) {
            System.out.println("start root task");
            Pair<Integer, Integer> idPair = queryMinAndMaxId();
            int minId = idPair.getFirst();
            int maxId = idPair.getSecond();
            List<Task> taskList = Lists.newArrayList();
            int step = (int) ((maxId - minId) / pageSize); //计算分页数量
            for (int i = minId; i < maxId; i+=step) {
                long startId = i;
            	long endId = (i+PAGE_SIZE > maxId ? maxId : i+PAGE_SIZE);
                tasks.add(new Task(tableName, startId, endId));
            }
            return map(taskList, "Level1Dispatch");
        } else if (taskName.equals("Level1Dispatch")) {
            Task record = (Task)task;
            long startId = record.getStartId();
            long endId = record.getEndId();
            //TODO 处理查询数据,并处理
            return new ProcessResult(true);
        }
        return new ProcessResult(true);
    }

    @Override
    public void postProcess(JobContext context) {
        //TODO
    }

    private Pair<Integer, Integer> queryMinAndMaxId() {
        //TODO 
        return null;
    }
}

当schedule开始调度时,

  1. server触发jobInstance到某个worker,选为master。
  2. MapTaskMaster选择某个worker执行root任务,当执行map方法时,会回调MapTaskMaster。
  3. MapTaskMaster收到map方法,会把task持久化到server端。
  4. 同时MapTaskMaster还有个pull线程,不停拉取INIT状态的task,并派发给其他worker执行。
相关推荐
Goboy5 分钟前
开发者必读的日志管理技巧
后端·面试·架构
ErizJ8 分钟前
Golang | 集合求交
开发语言·后端·golang·集合·交集
独立开阀者_FwtCoder13 分钟前
8年磨一剑,Koa 3.0 正式发布!看看带来了哪些新功能
前端·javascript·后端
逸风尊者16 分钟前
开发也能看懂的大模型:降维和度量学习
后端
brzhang29 分钟前
宝藏发现:Sim Studio,一款让AI工作流搭建变简单的开源利器
前端·后端·github
这里有鱼汤31 分钟前
出大事了!0.1 + 0.2 居然不等于 0.3,Python我再也不敢用了…
后端·python
学了就忘41 分钟前
Axios 传参与 Spring Boot 接收参数完全指南
java·spring boot·后端·vue
这里有鱼汤1 小时前
我用Python做了个“灵犀剪贴”:可以自动记录复制的文本,然后保存到本地
后端·python
冼紫菜1 小时前
[特殊字符] SpringCloud项目中使用OpenFeign进行微服务远程调用详解(含连接池与日志配置)
java·后端·spring cloud
风象南1 小时前
SpringBoot中4种登录验证码实现方案
java·spring boot·后端