【项目总结】易到家家政服务平台 —— 派单调度(7)

派单调度需求分析

在抢单业务中,用户下单成功由服务人员或机构进行抢单,抢单成功服务人员上门服务,除了抢单业务系统还设计了派单业务,由系统根据用户订单的特点自动派给合适的服务人员。

流程如下:

  1. 首先获取待分配的订单。
  2. 根据订单的属性,包括:地理位置、服务项目等去服务提供池中匹配服务提供者。
  3. 根据派单策略对符合条件的服务提供者进行规则匹配,每个派单策略通常有多个规则,从第一个规则逐个匹配其它规则。
  4. 最后获取匹配成功的服务提供者,系统进行机器抢单。
  5. 机器抢单成功,派单成功。

系统设计

  1. 涉及到距离搜索,参考抢单业务需要借助Elasticsearch搜索服务人员,所以需要将服务人员同步到Elasticsearch。根据搜索匹配的条件:服务技能,服务时间、接单范围,需要将服务人员和机构的相关信息同步到Elasticsearch。
    同步的方式使用Canal+MQ实现。
  2. 如果为订单派单失败每隔3分钟再次对订单进行派单,考虑性能问题在redis创建派单池,调度程序扫描派单池获取订单,所以需要将MySQL中派单池的数据同步到Redis。
  3. 根据需求,派单策略有三种,后期还可能扩展,这里使用策略模式实现,提高系统扩展性。
  4. 根据需求,每个派单策略有多个规则,按规则逐个去匹配,只要匹配成功或规则使用完成,这里使用责任链模式,提高系统扩展性。
  5. 派单程序将订单和服务人员匹配成功,接下来调用抢单接口进行机器抢单,这样也体现公平性,因为派单的同步有服务人员也在抢单,这里是机器(平台)和人工(服务人员)在共同抢单。

服务提供池索引结构

根据设计,第一步需要向Elasticsearch中同步服务提供者的数据。

将customer数据库的serve_provider_sync表和orders数据库的serve_provider_sync表同步到ES的服务提供池。

customer数据库的serve_provider_sync表中除了evaluation_score字段以外,其它字段是由服务人员设置服务技能、接单开关、接单范围保存的信息

evaluation_score字段存储服务人员的评分,用户通过评价系统评分,由评价系统同步到此字段中。

orders数据库的serve_provider_sync表中记录了服务人员接单数统计,当服务人员或机构抢单成功后进行统计得到。

Redis派单池

  1. 订单分流处理
    在订单分流中对于距离服务开始时间在120分钟(默认值可修改)以内时将订单写入orders数据库的派单池表。
  2. 定时任务处理
    抢单池的订单没有人抢,距离服务开始时间在120分钟以内时将订单写入orders数据库的派单池表。
    派单池表的结构如下:

Redis的派单池用什么数据结构呢?

根据需求,订单派单失败每隔3分钟再次对失败的订单进行派单,有什么办法可以在查询订单时将失败的订单过滤掉,并且还能根据时间对早进入派单池的订单进行优先派单,这里涉及到排序,很自然我们想到了Redis的SortedSet结构

value为是订单id、score为进入派单池的时间(时间戳,double类型),当派单失败我们score加3分钟 (提升订单的优先级),第一次查询SortedSet查询score小于当前时间的订单

八股🌟:redis数据结构、zset

责任链模式🌟 ------ 派单规则

责任链模式是一种行为型设计模式,它允许你将请求沿着处理者链进行传递,直到有一个处理者处理请求为止。每个处理者都可以决定是否将请求传递给下一个处理者。

根据派单的需求,根据订单信息从服务提供池中获取师傅及机构的信息,通过距离优先规则、评分优先规则等最终获取一个要派单的服务或机构。

数据处理规则的接口:

java 复制代码
public interface IProcessRule {

    /**
     * 根据派单规则过滤服务人员
     * @param serveProviderDTOS
     * @return
     */
    List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS);

    /**
     * 获取下一级规则
     *
     * @return
     */
    IProcessRule next();
}

实际上接口的两个方法实现对于每个具体规则来说都是一样的,只有doFilter过滤规则不同,我们可以抽象出一个抽象类让具体的规则来继承。

java 复制代码
@Setter
public abstract class AbstractIDispatchRule implements IDispatchRule {
    /**
     * 下一条规则
     */
    private IDispatchRule next;

    public AbstractIDispatchRule(IDispatchRule next) {
        this.next = next;
    }

    public abstract List<ServeProviderDTO> doFilter(List<ServeProviderDTO> originServeProviderDTOS);

    @Override
    public List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS) {
        List<ServeProviderDTO> result = this.doFilter(serveProviderDTOS);
        if(CollUtils.size(result) > 1 && next != null) {
            return next.filter(result);
        }else {
            return result;
        }
    }

    @Override
    public IDispatchRule next() {
        return next;
    }
}

对于每个具体的规则,只需要继承这个抽象类,然后实现 doFilter 规则即可。

java 复制代码
@Setter
@Slf4j
public class AcceptNumDispatchRule extends AbstractIDispatchRule {

    public AcceptNumDispatchRule(IDispatchRule next) {
        super(next);
    }

    @Override
    public List<ServeProviderDTO> doFilter(List<ServeProviderDTO> originServeProviderDTOS) {
        // 1.判断originServeProviderDTOS列表是否少于2,少于2直接返回
        if (CollUtils.size(originServeProviderDTOS) < 2) {
            return originServeProviderDTOS;
        }
        //  2.按照比较器进行排序,排在最前方优先级最高
        originServeProviderDTOS = originServeProviderDTOS.stream().sorted(Comparator.comparing(ServeProviderDTO::getAcceptanceNum)).collect(Collectors.toList());
        // 3.遍历优先级最高一批数据
        ServeProviderDTO first = CollUtils.getFirst(originServeProviderDTOS);

        //获取相同级别的
        return originServeProviderDTOS.stream()
                .filter(origin -> Comparator.comparing(ServeProviderDTO::getAcceptanceNum).compare(origin, first) == 0)
                .collect(Collectors.toList());
    }
}

具体应用:

这个代码显然是可以继续优化的,不然每次有新的策略就要重复写如下的代码,不利于拓展。

java 复制代码
        // 策略1:构建责任链,先距离优先,距离相同再判断评分
       IProcessRule rule = new AcceptNumRule(null);
        IProcessRule ruleChain = new DistanceRule(rule);
        // 策略2:构建责任链,先评分优先,评分相同再判断接单数
        IProcessRule rule = new AcceptNumRule(null);
        IProcessRule ruleChain = new ScoreRule(rule);
        // 策略3:构建责任链,先接单数优先,接单数相同再判断评分
        IProcessRule rule = new ScoreRule(null);
        IProcessRule ruleChain = new AcceptNumRule(rule);
java 复制代码
/**
 * 最少接单优先策略
 * 先根据接单计算得分,每接1单等于1分,分值越高优先级越低,如果最高优先级数量大于1时按最少接单数规则计算得分,依次类推,算规则执行顺序如下:
 * 按最少接单数规则->按评分计算规则
 */
@Component("leastAcceptOrderDispatchStrategy")
@DispatchStrategy(DispatchStrategyEnum.LEAST_ACCEPT_ORDER)
public class LeastAcceptOrderDispatchStrategyImpl extends AbstractDispatchStrategyImpl {
    @Override
    protected IDispatchRule getRules() {
        // 按评分计算规则,评分越高优先级越高
        IDispatchRule evaluationDispatchRule = new EvaluationScoreDispatchRule(null);
        // 按最少接单数规则,接单数量越少优先级越高
        IDispatchRule acceptNumDispatchRule = new AcceptNumDispatchRule(evaluationDispatchRule);
        return acceptNumDispatchRule;
    }
}

策略模式🌟 ------ 定义派单策略

通过将不同的规则进行组合,来决定具体要使用什么模式来派单。

根据需求我们平台支持距离优先策略、评分优先策略、最少接单优先策略**,针对上边的代码我们可以基于策略模式定义不同的策略去优化。**

策略接口
java 复制代码
public interface IProcessStrategy {
    /**
     * 从服务人员/机构列表中获取高优先级别的一个,如果出现多个相同优先级随机获取一个
     *
     * @param serveProviderDTOS 服务人员/机构列表
     * @return
     */
    ServeProviderDTO getPrecedenceServeProvider(List<ServeProviderDTO> serveProviderDTOS);
}
抽象的策略实现类

每个策略类都需要实现getPrecedenceServeProvider(List serveProviderDTOS)方法,其逻辑是一样的,每个策略类组装的责任链是不同的,我们定义抽象类将共同的方法写在抽象类中

java 复制代码
public abstract class AbstractStrategyImpl implements IProcessStrategy {

    private final IProcessRule processRule;

    public AbstractStrategyImpl() {
        this.processRule = getRules();
    }

    /**
     * 设置派单规则
     *
     * @return
     */
    protected abstract IProcessRule getRules();

    @Override
    public ServeProviderDTO getPrecedenceServeProvider(List<ServeProviderDTO> serveProviderDTOS) {
        // 1.判空
        if (CollUtils.isEmpty(serveProviderDTOS)) {
            return null;
        }

        // 2.根据优先级获取高优先级别的
        serveProviderDTOS = processRule.filter(serveProviderDTOS);

        // 3.数据返回
        // 3.1.唯一高优先级直接返回
        int size = 1;
        if ((size = CollUtils.size(serveProviderDTOS)) == 1) {
            return serveProviderDTOS.get(0);
        }
        // 3.2.多个高优先级随机返回
        int randomIndex = (int) (Math.random() * size);
        return serveProviderDTOS.get(randomIndex);
    }
}
具体的策略实现类

距离优先策略类

java 复制代码
public class DistanceStrategyImpl extends AbstractStrategyImpl  {
    @Override
    protected IProcessRule getRules() {
        //构建责任链,先距离优先,距离相同再判断接单数
        IProcessRule acceptNumRule = new AcceptNumRule(null);
        IProcessRule ruleChain = new DistanceRule(rule);
        return ruleChain;
    }
}
测试及应用
java 复制代码
//获取距离优先策略
        IProcessStrategy processStrategy = new DistanceStrategyImpl();
        //通过策略bean进行匹配处理
        ServeProviderDTO precedenceServeProvider = processStrategy.getPrecedenceServeProvider(serveProviderDTOS);

总体流程总结

  1. 从redis派单池查询订单
  2. 将派单任务放入线程池
  3. 执行派单任务:从服务提供者池找到符合条件的服务人员、获取派单策略、通过责任链模式匹配规则,匹配一个服务人员
  4. 匹配成功调用抢单接口进行机器抢单(相当于是平台和服务人员都在抢单,因为此时还有其他的机构或者服务人员在抢)
  5. 机器抢单成功则派单成功。
相关推荐
呀啊~~8 分钟前
【前端框架与库】「React 全面解析」:从 JSX 语法到高阶组件,深度剖析前端开发中的核心概念与最佳实践
前端·javascript·学习·react.js·前端框架
S-X-S8 分钟前
Java面试题-Spring Boot
java·开发语言·spring boot
灵魂画师向阳12 分钟前
白嫖RTX 4090?Stable Diffusion:如何给线稿人物快速上色?
java·大数据·人工智能·ai作画·stable diffusion
185的阿平23 分钟前
MVCC面试怎么答
java·mysql
Excuse_lighttime29 分钟前
堆排序
java·开发语言·数据结构·算法·排序算法
陈老师还在写代码31 分钟前
SpringBoot的单机模式是否需要消息队列?分布式应用中消息队列如何和服务的发现与注册、配置中心、SpringMVC相配合
java·spring boot·后端
等什么君!34 分钟前
spring 学习(spring-Dl补充(注入不同类型的数据))
java·学习·spring
CodeCraft Studio1 小时前
3D文档控件Aspose.3D实用教程: 在 Java 中创建 FBX 文件并无缝将圆柱体转换为网格
java·3d
派森先生1 小时前
《麻省理工公开课:线性代数》 中文学习笔记
笔记·学习·线性代数
RainbowJie11 小时前
线程池-抢票系统性能优化
java·spring·性能优化