抢单和派单(超卖问题;ES、Mysql、Redis同步问题;责任链模式)

目录

抢单和派单

一、订单分流

二、抢单

1)订单查询

2)抢单

3)抢单结果同步

具体实现

1.抢单池同步

2.抢单查询

3.抢单

4.抢单结果同步

三、派单

责任链模式

1)定义规则接口

2)定义距离近者优先规则

3)定义接单数少者优先规则

4)方法中创建链对象并发起处理

利用设计模式优化代码

1)定义抽象规则类

2)修改实际规则类

3)定义策略接口

4)定义策略抽象类

5)定义距离优先策略类

6)定义最少接单优先策略

具体实现


抢单和派单

一、订单分流

在我们项目中有一个订单分流的动作,也就是根据订单服务开始时间与当前时间的间隔,将订单分为两种:

  • 待抢单:只要是支付成功的订单就可以进入到抢单池中,此时服务者就可以进行抢单

  • 待派单:如果距离订单服务开始不足3小时(可配)了,就会将这个单子复制到派单池中,然后强行派给服务者

阅读代码发现,在分流方法当中会通过远程Feign调用方法查询当前区域的配置(目的是为了获得区域配置当中的diversionInterval指定间隔时间,也就是不同的地方有不同的时间触发强制派单)

java 复制代码
    @Override
    public void diversion(Orders orders) {
        log.debug("订单分流,id:{}",orders.getId());
        // 1.当前时间已超过服务预约时间则不再分流
        if (orders.getServeStartTime().compareTo(DateUtils.now()) < 0) {
            log.debug("订单{}当前时间已超过服务预约时间则不再分流", orders.getId());
            return;
        }
        ConfigRegionInnerResDTO configRegion = regionApi.findConfigRegionByCityCode(orders.getCityCode());
        ServeAggregationResDTO serveAggregationResDTO = serveApi.findById(orders.getServeId());
        //订单分流数据存储
        owner.diversionCommit(orders,configRegion,serveAggregationResDTO);
    }

随后封装字段插入抢单表并判断是否要放到派单池

java 复制代码
@Transactional(rollbackFor = Exception.class)
    public void diversionCommit(Orders orders, ConfigRegionInnerResDTO configRegion, ServeAggregationResDTO serveAggregationResDTO) {
        //流间隔(单位分钟),即当前时间与服务预计开始时间的间隔
        Integer diversionInterval = configRegion.getDiversionInterval();

        //当前时间与服务预约时间的间隔
        Duration between = DateUtils.between(DateUtils.now(), orders.getServeStartTime());
        //服务类型名称
        String serveTypeName = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeTypeName);
        //服务类型id
        Long serveTypeId = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeTypeId);
        //服务项名称
        String serveItemName = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeItemName);
        //服务项图片
        String serveItemImg = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeItemImg);
        //用于排序,服务预约时间戳加订单号后5位
        long sortBy = DateUtils.toEpochMilli(orders.getServeStartTime()) + orders.getId() % 100000;
        OrdersSeize ordersSeize = OrdersSeize.builder()
                .id(orders.getId())
                .ordersAmount(orders.getRealPayAmount())
                .cityCode(orders.getCityCode())
                .serveTypeId(serveTypeId)
                .serveTypeName(serveTypeName)
                .serveItemId(orders.getServeItemId())
                .serveItemName(serveItemName)
                .serveItemImg(serveItemImg)
                .ordersAmount(orders.getRealPayAmount())
                .serveStartTime(orders.getServeStartTime())
                .serveAddress(orders.getServeAddress())
                .lon(orders.getLon())
                .lat(orders.getLat())
                .paySuccessTime(DateUtils.now())
                .paySuccessTime(orders.getPayTime())
                .sortBy(sortBy)
                .isTimeOut(BooleanUtils.toInt(between.toMinutes() < diversionInterval))
                .purNum(orders.getPurNum()).build();
        ordersSeizeMapper.insert(ordersSeize);
        //当前时间与服务预约时间的间隔 小于指定间隔则插入派单表
        if (between.toMinutes() < diversionInterval) {
            OrdersDispatch ordersDispatch = OrdersDispatch.builder()
                    .id(orders.getId())
                    .ordersAmount(orders.getRealPayAmount())
                    .cityCode(orders.getCityCode())
                    .serveTypeId(serveTypeId)
                    .serveTypeName(serveTypeName)
                    .serveItemId(orders.getServeItemId())
                    .serveItemName(serveItemName)
                    .serveItemImg(serveItemImg)
                    .ordersAmount(orders.getRealPayAmount())
                    .serveStartTime(orders.getServeStartTime())
                    .serveAddress(orders.getServeAddress())
                    .lon(orders.getLon())
                    .lat(orders.getLat())
                    .purNum(orders.getPurNum()).build();
            ordersDispatchMapper.insert(ordersDispatch);
        }
    }

接下来就是设定定时任务调用Feign接口方法查询所有区域的相关配置信息并传入seizeTimeoutIntoDispatchPool方法,该方法才是实现逻辑的具体方法:

java 复制代码
    /**
     *当前时间距离服务预约时间间隔小于配置值时进入派单池,以城市为单位进行处理
     * @param cityCode
     * @param timeoutInterval
     */
    @Transactional(rollbackFor = Exception.class)
    public void seizeTimeoutIntoDispatchPool(String cityCode, Integer timeoutInterval) {
        // 1.查询满足条件的且未处理的抢单列表
        List<OrdersSeize> ordersSeizes = ordersSeizeService.queryTimeoutSeizeOrders(cityCode, timeoutInterval);
        if (CollUtils.isEmpty(ordersSeizes)) {
            return;
        }

        // 2.修改抢单超时标记并,派单
        List<Long> ids = ordersSeizes.stream().map(OrdersSeize::getId).collect(Collectors.toList());
        List<OrdersDispatch> ordersDispatches = BeanUtils.copyToList(ordersSeizes, OrdersDispatch.class);
        //2.1.指定同步到派单池
        ordersDispatchService.saveOrUpdateBatch(ordersDispatches,100);
        // 2.2.标记订单抢单超时
        ordersSeizeService.batchTimeout(ids);
    }

可以看到,在这个方法当中才开始查询订单表当中符合强制派单条件的订单,然后插入到派单池并且修改抢单池的抢单是否超时字段isTimeOut为true

二、抢单

用户支付成功的订单都会进入到抢单池中,然后个人服务端和机构服务端就可以进行抢单了,二者抢单的区别是:

  • 个人服务端抢单成功后订单状态为待服务,下一步直接去服务就可以了

  • 机构服务端抢单成功后订单状态为待分配,下一步需要将订单分配给机构下的服务人员

服务人员抢单成功或系统派单成功将生成订单对应的服务单,服务单状态如下:

  • 服务单初始状态:待分配或待服务(机构抢单成功:待分配,服务人员抢单成功:待服务)

  • 开始服务:待服务--->服务中

  • 服务完成:服务中--->服务完成

  • 服务人员或者机构取消订单:待分配--->已取消 待服务--->已取消

  • 运营人员取消订单:服务中(服务完成)--->已取消

1)订单查询

查询订单就是查询出跟服务者相匹配(技能、城市、距离等)的订单供服务者去抢

由于需要按照距离搜索订单,所以这里需要将订单同步到ES中,然后借助ES的地理位置语法查询订单资源

2)抢单

抢单就有可能多个人争抢同一个订单,这里存在超卖问题,我们使用Redis+Lua的技术解决超卖问题

也就是执行Lua脚本完成抢单,具体包括:扣减库存(每个订单的库存就是1)、抢单成功写入同步队列

3)抢单结果同步

这一步需要使用定时任务实现,根据抢单结果创建服务单,并且更新订单的状态,当然还要删除抢了的资源

  1. 创建服务单:服务单记录了服务人员进行家政服务的信息(初始状态:机构-待分配,服务人员-待服务)

  2. 更新订单状态:将订单中状态修改为待服务或者待分配(服务人员-待服务,机构-待分配)

  3. 删除抢单数据:删除数据库抢单池中对应的订单信息,ES抢单池中对应的订单记录,Redis中该订单的库存信息和抢单同步队列中的记录

具体实现

1.抢单池同步

抢单的前提是需要将抢单池的数据先同步的es,并将库存写入到redis

  1. 同步到es是为了利用ES的地理坐标搜索功能实现订单的距离查询

  2. 同步到Redis是为了后续可以从Redis中扣减库存

首先要建立ES索引结构、Redis抢单池库存结构(抢单池库存信息存储在Redis,在抢单时通过Redis扣减库存):

随后编写代码从MQ中读取数据并同步增加或删除ES、Redis当中的数据:

java 复制代码
package com.jzo2o.canal.listeners;

import com.jzo2o.canal.constants.FieldConstants;
import com.jzo2o.canal.constants.OperateType;
import com.jzo2o.canal.core.CanalDataHandler;
import com.jzo2o.canal.model.CanalMqInfo;
import com.jzo2o.canal.model.dto.CanalBaseDTO;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.common.utils.NumberUtils;
import org.springframework.amqp.core.Message;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public abstract class AbstractCanalRabbitMqMsgListener<T> implements CanalDataHandler<T> {

    public void parseMsg(Message message) throws Exception {

        try {
            // 1.数据格式转换
            CanalMqInfo canalMqInfo = JsonUtils.toBean(new String(message.getBody()), CanalMqInfo.class);
            // 2.过滤数据,没有数据或者非插入、修改、删除的操作均不处理
            if (CollUtils.isEmpty(canalMqInfo.getData()) || !(OperateType.canHandle(canalMqInfo.getType()))) {
                return;
            }

            if (canalMqInfo.getData().size() > 1) {
                // 3.多条数据处理
                batchHandle(canalMqInfo);
            } else {
                // 4.单条数据处理
                singleHandle(canalMqInfo);
            }
        } catch (Exception e) {
            //出现错误延迟1秒重试
            Thread.sleep(1000);
            throw new RuntimeException(e);
        }
    }

    /**
     * 单条数据处理
     *
     * @param canalMqInfo
     */
    private void singleHandle(CanalMqInfo canalMqInfo) {
        // 1.数据转换
        CanalBaseDTO canalBaseDTO = BeanUtils.toBean(canalMqInfo, CanalBaseDTO.class);
        Map<String, Object> fieldMap = CollUtils.getFirst(canalMqInfo.getData());
        canalBaseDTO.setId(parseId(fieldMap));
        canalBaseDTO.setFieldMap(fieldMap);
        canalBaseDTO.setIsSave(canalMqInfo.getIsSave());

        Class<T> messageType = getMessageType();
        if (messageType == null) {
            return;
        }
        if (canalBaseDTO.getIsSave()) {
            T t1 = JsonUtils.toBean(JsonUtils.toJsonStr(canalBaseDTO.getFieldMap()), messageType);
            List<T> ts = Arrays.asList(t1);
            batchSave(ts);
        } else {
            Long id = canalBaseDTO.getId();
            List<Long> ids = Arrays.asList(id);
            batchDelete(ids);
        }
    }


    private void batchHandle(CanalMqInfo canalMqInfo) {
        Class<T> messageType = getMessageType();
        if (messageType == null) {
            return;
        }

        if(canalMqInfo.getIsSave()){
            List<T> collect = canalMqInfo.getData().stream().map(fieldMap -> {
                CanalBaseDTO canalBaseDTO = CanalBaseDTO.builder()
                        .id(parseId(fieldMap))
                        .database(canalMqInfo.getDatabase())
                        .table(canalMqInfo.getTable())
                        .isSave(canalMqInfo.getIsSave())
                        .fieldMap(fieldMap).build();
                return JsonUtils.toBean(JsonUtils.toJsonStr(canalBaseDTO.getFieldMap()), messageType);
            }).collect(Collectors.toList());
            batchSave(collect);
        }else{
            List<Long> ids = canalMqInfo.getData().stream().map(fieldMap -> {
                return parseId(fieldMap);
            }).collect(Collectors.toList());

            batchDelete(ids);
        }

    }

    private Long parseId(Map<String, Object> fieldMap) {
        Object objectId = fieldMap.get(FieldConstants.ID);
        return NumberUtils.parseLong(objectId.toString());
    }

    /**
     * 批量保存
     *
     * @param data
     */
    public abstract void batchSave(List<T> data);

    /**
     * 批量删除
     *
     * @param ids
     */
    public abstract void batchDelete(List<Long> ids);


    //获取泛型参数
    public Class<T> getMessageType() {
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superClass;
            Type[] typeArgs = parameterizedType.getActualTypeArguments();
            if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
                return (Class<T>) typeArgs[0];
            }
        }
        return null;
    }
}
java 复制代码
package com.jzo2o.orders.seize.handler;

import com.jzo2o.canal.listeners.AbstractCanalRabbitMqMsgListener;
import com.jzo2o.common.model.Location;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.es.core.ElasticSearchTemplate;
import com.jzo2o.orders.base.constants.RedisConstants;
import com.jzo2o.orders.base.model.domain.OrdersSeize;
import com.jzo2o.orders.base.utils.RedisUtils;
import com.jzo2o.orders.seize.model.domain.OrdersSeizeInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;

import static com.jzo2o.orders.base.constants.EsIndexConstants.ORDERS_SEIZE;

/**
 * 抢单池同步类
 */
@Component
@Slf4j
public class OrdersSeizeSyncHandler extends AbstractCanalRabbitMqMsgListener<OrdersSeize> {

    @Resource
    private ElasticSearchTemplate elasticSearchTemplate;

    @Resource
    private RedisTemplate redisTemplate;


    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "canal-mq-jzo2o-orders-seize"),
            exchange = @Exchange(name = "exchange.canal-jzo2o", type = ExchangeTypes.TOPIC),
            key = "canal-mq-jzo2o-orders-seize"),
            concurrency = "1"
    )
    public void onMessage(Message message) throws Exception {
        parseMsg(message);
    }

    @Override
    public void batchSave(List<OrdersSeize> ordersSeizes) {
        // 1.es中添加抢单信息
        List<OrdersSeizeInfo> ordersSeizeInfos = ordersSeizes.stream().map(ordersSeize -> {
            OrdersSeizeInfo ordersSeizeInfo = BeanUtils.toBean(ordersSeize, OrdersSeizeInfo.class);
            //得到服务开始时间(yyMMddHH)
            String serveTimeString = DateTimeFormatter.ofPattern("yyMMddHH").format(ordersSeize.getServeStartTime());
            ordersSeizeInfo.setServeTime(Integer.parseInt(serveTimeString));
            ordersSeizeInfo.setLocation(new Location(ordersSeize.getLon(), ordersSeize.getLat()));
            ordersSeizeInfo.setKeyWords(ordersSeize.getServeTypeName() + ordersSeize.getServeItemName() + ordersSeize.getServeAddress());
            return ordersSeizeInfo;
        }).collect(Collectors.toList());

        Boolean result = elasticSearchTemplate.opsForDoc().batchInsert(ORDERS_SEIZE, ordersSeizeInfos);
        if (!result){
            throw new RuntimeException("同步抢单池加入es失败");
        }
        // 2.写入库存
        ordersSeizeInfos.stream().forEach(ordersSeizeInfo -> {
            String redisKey = String.format(RedisConstants.RedisKey.ORDERS_RESOURCE_STOCK, RedisUtils.getCityIndex(ordersSeizeInfo.getCityCode()));
            // 库存默认1
            redisTemplate.opsForHash().putIfAbsent(redisKey, ordersSeizeInfo.getId(), 1);
        });
    }

    @Override
    public void batchDelete(List<Long> ids) {
        log.info("抢单删除开始,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));
        Boolean result = elasticSearchTemplate.opsForDoc().batchDelete(ORDERS_SEIZE, ids);
        if (!result){
            throw new RuntimeException("同步抢单池加入es失败");
        }
        log.info("抢单删除结束,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));

    }
}
2.抢单查询
java 复制代码
@Override
    public OrdersSeizeListResDTO queryForList(OrdersSerizeListReqDTO ordersSerizeListReqDTO) {

        // 1.校验是否可以查询(认证通过,开启抢单)
        ServeProviderResDTO detail = serveProviderApi.getDetail(UserContext.currentUserId());
        // 验证设置状态
        if (detail.getSettingsStatus() != 1 || !detail.getCanPickUp()) {
            return OrdersSeizeListResDTO.empty();
        }
        // 2.查询准备 (距离、技能,时间冲突)
        // 距离
        Double serveDistance = ordersSerizeListReqDTO.getServeDistance();
        if(ObjectUtils.isNull(ordersSerizeListReqDTO.getServeDistance())) {
            // 区域默认配置配置
            ConfigRegionInnerResDTO configRegionInnerResDTO = regionApi.findConfigRegionByCityCode(detail.getCityCode());
            serveDistance = (detail.getType() == UserType.INSTITUTION)
                    ? configRegionInnerResDTO.getInstitutionServeRadius().doubleValue() : configRegionInnerResDTO.getStaffServeRadius().doubleValue();
        }
        // 技能
        List<Long> serveItemIds = serveSkillApi.queryServeSkillListByServeProvider(UserContext.currentUserId(), UserContext.currentUser().getUserType(), detail.getCityCode());
        if(CollUtils.isEmpty(serveItemIds)) {
            log.info("当前机构或服务人员没有对应技能");
            return OrdersSeizeListResDTO.empty();
        }


        // 3.查询符合条件的抢单列表id
        List<OrdersSeizeListResDTO.OrdersSeize> ordersSeizes = getOrdersSeizeId(
                serveItemIds, detail.getLon(), detail.getLat(), serveDistance, detail.getCityCode(), ordersSerizeListReqDTO);

        return new OrdersSeizeListResDTO(CollUtils.defaultIfEmpty(ordersSeizes, new ArrayList<>()));
    }

简单概括一下,首先调用Feign接口方法获取该接单人员是否开启接单、相关的技能是否匹配、时间上是否冲突、是否在接单范围内;获得这些之后全部丢给getOrdersSeizeId这个封装好的方法,其内部就是将这些条件封装到ES的bool查询当中,最终从ES获取数据返回

3.抢单

执行Lua脚本在Redis中完成抢单,具体包括:在库存队列中扣减库存、抢单成功写入同步队列

这里注意抢单成功队列的值包含三部分内容:服务者id,服务者类型,是人工还是机器抢单

java 复制代码
@Override
    @Lock(formatter = RedisConstants.RedisFormatter.SEIZE, time = 300)
    public void seize(Long id, Long serveProviderId, Integer serveProviderType, Boolean isMatchine) {

        // 1.抢单校验
        // 1.1.校验是否可以查询(认证通过,开启抢单)
        ServeProviderResDTO detail = serveProviderApi.getDetail(serveProviderId);
        if (!detail.getCanPickUp() || detail.getSettingsStatus() != 1) {
            throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_RECEIVE_CLOSED);
        }
        // 1.2.校验抢单是否存在
        OrdersSeize ordersSeize = ordersSeizeService.getById(id);
        // 校验订单是否还存在,如果订单为空或id不存在,则认为订单已经不在
        if (ordersSeize == null || ObjectUtils.isNull(ordersSeize.getId())) {
            throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
        }
        ConfigRegionInnerResDTO configRegionInnerResDTO = regionApi.findConfigRegionByCityCode(detail.getCityCode());


        // 城市编码最后1位序号
        int index = RedisUtils.getCityIndex(detail.getCityCode());
        // 1.3.校验时间冲突
        // 服务时间状态redisKey
        String serveProviderStateRedisKey = String.format(SERVE_PROVIDER_STATE, index);
        int serveTime = ServeTimeUtils.getServeTimeInt(ordersSeize.getServeStartTime());
        if(serveProviderType == UserType.WORKER) {
            Object serveTimes = redisTemplate.opsForHash().get(serveProviderStateRedisKey, serveProviderId + "_times");
            if(ObjectUtils.isNotNull(serveTimes) && serveTimes.toString().contains(serveTime+"")){
                throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_SERVE_TIME_EXISTS);
            }
        }
        // 1.4.订单数量已达上限
        // 接单数量上限
        int receiveOrderMax = (serveProviderType == UserType.INSTITUTION) ? configRegionInnerResDTO.getInstitutionReceiveOrderMax() : configRegionInnerResDTO.getStaffReceiveOrderMax();

        Object ordersNum = redisTemplate.opsForHash().get(serveProviderStateRedisKey, serveProviderId + "_num");
        if(ObjectUtils.isNotNull(ordersNum) && NumberUtils.parseInt(ordersNum.toString()) >= receiveOrderMax){
            throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_RECEIVE_ORDERS_NUM_OVER);
        }

        // 2.执行redis脚本

        // 2.1.redisKey
        // 抢单结果同步队列 redis key
        String ordersSeizeSyncRedisKey = RedisSyncQueueUtils.getQueueRedisKey(RedisConstants.RedisKey.ORERS_SEIZE_SYNC_QUEUE_NAME, index);
        // 库存redisKey
        String resourceStockRedisKey = String.format(ORDERS_RESOURCE_STOCK, index);

        log.debug("抢单key:{},values:{}", Arrays.asList(ordersSeizeSyncRedisKey, resourceStockRedisKey),
                Arrays.asList(id, serveProviderId,serveProviderType));
        // 2.2.执行lua脚本
        Object execute = redisTemplate.execute(seizeOrdersScript,
                // 序列化串行器
                new GenericJackson2JsonRedisSerializer(), new GenericJackson2JsonRedisSerializer(),
                Arrays.asList(ordersSeizeSyncRedisKey, resourceStockRedisKey, serveProviderStateRedisKey),
                id, serveProviderId,serveProviderType,isMatchine ? 1 : 0);
        log.debug("抢单结果 : {}", execute);

        // 3.处理lua脚本结果
        if (execute == null) {
            throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
        }
        // 4.抢单结果判断 大于0抢单成功,-1/-2:库存数量不足,-3:抢单失败
        long result = NumberUtils.parseLong(execute.toString());
        if(result < 0) {
            throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
        }
    }

服务人员登录服务端程序,进入抢单界面,查询抢单信息,点击"立即抢单"

在redis成功写入抢单同步记录

在redis成功扣除库存

4.抢单结果同步

抢单成功后根据抢单结果进行异步处理,具体内容如下:

  1. 创建服务单:服务单记录了服务人员进行家政服务的信息(初始状态:机构-待分配,服务人员-待服务)

  2. 更新订单状态:将订单中状态修改为待服务或者待分配(服务人员-待服务,机构-待分配)

  3. 删除抢单数据:删除数据库抢单池中对应的订单信息,ES抢单池中对应的订单记录,Redis中该订单的库存信息和抢单同步队列中的记录

java 复制代码
    @Override
    public void start(String queueName, int storageType, int mode, final Executor dataSyncExecutor) {
        //根据队列的数量循环,将每个队列的数据同步任务提交到线程池
        for (int index = 0; index < redisSyncProperties.getQueueNum(); index++) {
            try {

                if (dataSyncExecutor == null) {//使用默认线程池
                    //使用getSyncThread方法获取任务对象
                    DEFAULT_SYNC_EXECUTOR.execute(getSyncThread(queueName, index, storageType, mode));
                } else {//使用自定义线程池
                    dataSyncExecutor.execute(getSyncThread(queueName, index, storageType, mode));
                }
            } catch (Exception e) {
                log.error("同步数据处理异常,e:", e);
            }
        }
    }

这里定时任务就是通过getSyncThread从Redis当中获取任务,然后交给线程处理(这里可以是默认线程池也可以是我们的自定义线程池),处理方法如下:

java 复制代码
@Override
    public void singleProcess(SyncMessage<Object> singleData) {
        log.info("抢单结果同步开始 id : {}",singleData.getKey());
        // 抢单信息放在value中,内容格式:[serveProviderId,serveProviderType,isMatchine(0,表示人工抢单,1:表示机器抢单)]
        JSONArray seizeResult = JsonUtils.parseArray(singleData.getValue());
        // 服务人员或机构id
        Long serveProviderId = seizeResult.getLong(0);
        // 用户类型
        Integer serveProviderType = seizeResult.getInt(1);
        // 是否是机器抢单
        boolean isMatchine = seizeResult.getBool(2);

        // 抢单id
        Long seizeId = NumberUtils.parseLong(singleData.getKey());
        // 抢单不在无需继续处理
        OrdersSeize ordersSeize = ordersSeizeService.getById(seizeId);
        if (ordersSeize == null) {
            return;
        }

        // 处理抢单结果
        ordersSeizeService.seizeOrdersSuccess(ordersSeize, serveProviderId, serveProviderType, isMatchine);
        log.info("抢单结果同步结束 id : {}",singleData.getKey());
    }

这里主要是在做一些订单数据的封装与存在性校验,真正的操作内容如下:

java 复制代码
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void seizeOrdersSuccess(OrdersSeize ordersSeize, Long serveProviderId, Integer serveProviderType, Boolean isMatchine) {

        // 1.校验服务单是否已经生成
        OrdersServe ordersServeInDb = ordersServeService.findById(ordersSeize.getId());
        if(ordersServeInDb != null){
            return;
        }
        // 2.生成服务单,
        OrdersServe ordersServe = BeanUtils.toBean(ordersSeize, OrdersServe.class);
        ordersServe.setCreateTime(null);
        ordersServe.setUpdateTime(null);
        // 服务单状态 机构抢单状态:待分配;服务人员抢单状态:待服务
        int serveStatus = UserType.WORKER == serveProviderType ? ServeStatusEnum.NO_SERVED.getStatus() : ServeStatusEnum.NO_ALLOCATION.getStatus();
        // 服务单来源类型,人工抢单来源抢单,值为1;机器抢单来源派单,值为2
        int ordersOriginType = isMatchine ? OrdersOriginType.DISPATCH : OrdersOriginType.SEIZE;
        ordersServe.setOrdersOriginType(ordersOriginType);
        ordersServe.setServeStatus(serveStatus);
        ordersServe.setServeProviderId(serveProviderId);
        ordersServe.setServeProviderType(serveProviderType);
        if(!ordersServeService.save(ordersServe)){
            return;
        }

        // 3.当前订单数量
        serveProviderSyncService.countServeTimesAndAcceptanceNum(serveProviderId, serveProviderType);

        String resourceStockRedisKey = String.format(ORDERS_RESOURCE_STOCK, RedisUtils.getCityIndex(ordersSeize.getCityCode()));
        Object stock = redisTemplate.opsForHash().get(resourceStockRedisKey, ordersSeize.getId());
        if (ObjectUtils.isNull(stock) || NumberUtils.parseInt(stock.toString()) <= 0) {
            ordersDispatchMapper.deleteById(ordersSeize.getId());
            ordersSeizeService.removeById(ordersSeize.getId());
            redisTemplate.opsForHash().delete(resourceStockRedisKey, ordersSeize.getId());
        }

        //状态机修改订单状态
//        OrderSnapshotDTO orderSnapshotDTO = OrderSnapshotDTO.builder()
//                .ordersStatus(OrderStatusEnum.NO_SERVE.getStatus()).build();
        Orders orders = ordersMapper.selectById(ordersSeize.getId());
        orderStateMachine.changeStatus(orders.getUserId(),String.valueOf(ordersSeize.getId()), OrderStatusChangeEventEnum.DISPATCH);

    }

三、派单

用户下的订单首先是由服务人员和机构进行抢单,但是如果一个订单迟迟没人抢,而距离开始服务时间又比较近了

系统就会将其再转入派单池中,然后进行强行派单,这样做的目的就是防止出现流单,提升下单用户的体验感

派单就是根据订单自动匹配一个服务者的过程,具体流程如下:

  1. 首先获取派单池中的所有订单,然后遍历出每个订单执行下面步骤

  2. 根据订单的属性(包括:服务项目、地理位置、服务时间)去匹配服务者

  3. 根据派单策略对符合条件的服务者进行规则筛选,直到找一个服务者

  4. 最后系统会让匹配成功的这个服务者去调用抢单的接口进行机器抢单

  1. 我们要以订单为基准,搜索附近的服务人员,也就是说涉及到距离搜索问题,可以使用es实现,这就需要将需要将服务者的相关信息(经纬度坐标、接单状态、当前接单数)同步到es

  2. 如果派单失败每隔3分钟需要再次派单,可以在redis实现这个功能,这就需要将派单数据同步到Redis

  3. 派单策略可能会有多种,这里使用策略模式实现,提高系统扩展性

  4. 每个派单策略有多个匹配规则,这些规则构成一个匹配链,这里使用责任链模式,提高系统扩展性

  5. 派单程序将订单和服务人员匹配成功,为了防止出现超卖,需要调用抢单接口进行机器抢单

责任链模式

责任链模式允许请求沿着处理链进行传递,链中的每个处理者都要决定是自己处理,还是交给下一环处理

在这里有一个很经典的案例就是请假审批,比如我们有这样一个需求,就是当员工请假时,审批规则如下:

  1. 请假天数少于3天,组长审批

  2. 请假天数在3-5天,主管审批

  3. 请求天数大于5天,经理审批

此时我们就可以使用责任链模式来编写这个代码,这种设计模式有三个核心角色:

  • 抽象处理者:通常包含一个指向下一个处理者的引用和一个处理方法的声明

  • 具体处理者:指定下一个处理者是谁,并且要实现处理的方法

  • 客户端:创建处理者对象并组成责任链的结构,负责将请求发送给第一个处理者

接下来我们使用责任链模式来处理距离优先策略的代码,在这个策略中有两个规则

每个规则都应该定义为责任链的实现类,每个规则都应该有一个筛选服务人员的方法

1)定义规则接口
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;
import com.jzo2o.orders.dispatch.rules.IDispatchRule;
import lombok.Builder;
import lombok.ToString;

import java.util.List;

/**
 * 规则接口
 */
public interface IProcessRule {

    /**
     * 根据派单规则过滤服务人员
     *
     * @param serveProviderDTOS 服务人员集合
     * @return 服务人员集合
     */
    List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS);
}
2)定义距离近者优先规则
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;
import com.jzo2o.orders.dispatch.rules.IDispatchRule;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 规则: 距离近者优先
 */
public class DistanceRule implements IProcessRule {
    private IProcessRule next;

    public DistanceRule(IProcessRule next) {
        this.next = next;
    }

    @Override
    public List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS) {
        List<ServeProviderDTO> result = this.doFilter(serveProviderDTOS);

        //如果没有下一级规则,直接返回结果
        if (next == null) {
            return result;
        }
        if (CollUtils.size(result) <= 1) {
            // 选出唯一的服务者了,直接返回
            return result;
        }

        // 选不出唯一的,就调下一个规则进行筛选
        return next.filter(result);
    }

    //挑选距离最近的服务者列表返回
    public List<ServeProviderDTO> doFilter(List<ServeProviderDTO> serveProviderDTOS) {
        System.out.println("按距离排序");
        if (CollUtils.size(serveProviderDTOS) < 2) {
            return serveProviderDTOS;
        }
        // 2.按照比较器进行排序,排在最前方优先级最高
        serveProviderDTOS = serveProviderDTOS.stream()
                .sorted(Comparator.comparing(ServeProviderDTO::getAcceptanceDistance))
                .collect(Collectors.toList());

        // 3.遍历优先级最高一个数据
        ServeProviderDTO first = CollUtils.getFirst(serveProviderDTOS);

        //获取跟第一个相同级别的
        return serveProviderDTOS.stream()
                .filter(origin -> origin.getAcceptanceDistance().compareTo(first.getAcceptanceDistance()) == 0)
                .collect(Collectors.toList());
    }
}
3)定义接单数少者优先规则
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 规则: 接单数少者优先
 */
public class AcceptNumRule implements IProcessRule {
    private IProcessRule next;

    public AcceptNumRule(IProcessRule next) {
        this.next = next;
    }


    @Override
    public List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS) {
        List<ServeProviderDTO> result = this.doFilter(serveProviderDTOS);

        //如果没有下一级规则,直接返回结果
        if (next == null) {
            return result;
        }
        if (CollUtils.size(result) <= 1) {
            // 选出唯一的服务者了,直接返回
            return result;
        }

        // 选不出唯一的,就调下一个规则进行筛选
        return next.filter(result);
    }

    //挑选接单数最少的服务者列表返回
    public List<ServeProviderDTO> doFilter(List<ServeProviderDTO> serveProviderDTOS) {
        System.out.println("按接单数排序");
        if (CollUtils.size(serveProviderDTOS) < 2) {
            return serveProviderDTOS;
        }
        //  2.按照比较器进行排序,排在最前方优先级最高
        serveProviderDTOS = serveProviderDTOS.stream()
                .sorted(Comparator.comparing(ServeProviderDTO::getAcceptanceNum))
                .collect(Collectors.toList());
        
        // 3.遍历优先级最高一个数据
        ServeProviderDTO first = CollUtils.getFirst(serveProviderDTOS);

        //获取相同级别的数据
        return serveProviderDTOS.stream()
                .filter(origin -> origin.getAcceptanceNum().compareTo(first.getAcceptanceNum()) == 0)
                .collect(Collectors.toList());
    }
}
4)方法中创建链对象并发起处理

下边将规则组成一个链儿,调用链儿中第一个规则的filter方法,最终获取处理后的结果,

如果处理结果的数量大于1则随机选择一个,否则取出唯一的结果

java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;

import java.util.Arrays;
import java.util.List;

/**
 * 策略模式测试类
 */
public class RuleHandlerTest {

    public static void main(String[] args) {
        // 创建测试数据
        List<ServeProviderDTO> serveProviderDTOS = Arrays.asList(
                ServeProviderDTO.builder().id(3L).acceptanceDistance(10).acceptanceNum(2).build(),
                ServeProviderDTO.builder().id(4L).acceptanceDistance(10).acceptanceNum(3).build(),
                ServeProviderDTO.builder().id(5L).acceptanceDistance(13).acceptanceNum(1).build(),
                ServeProviderDTO.builder().id(6L).acceptanceDistance(13).acceptanceNum(1).build()
        );

        // 距离优先策略:构建责任链,先找距离小的,距离相同再找接单数少的
        IProcessRule chain = new DistanceRule(new AcceptNumRule(null));

        // 假设切换为接单优先策略:构建责任链,先找接单数少的,接单数相同再找距离小的
        // IProcessRule chain2 = new AcceptNumRule(new DistanceRule(null));


        // 发起处理请求
        List<ServeProviderDTO> list = chain.filter(serveProviderDTOS);
       
        //处理结果
        ServeProviderDTO result = null;

        // 唯一高优先级直接返回
        int size = 1;
        if ((size = CollUtils.size(list)) == 1) {
            result = list.get(0);
        }
        
        // 多个高优先级随机返回
        int randomIndex = (int) (Math.random() * size);
        result = list.get(randomIndex);
        
        System.out.println(result);
    }
}

可以看到这里是通过new对象的方式,逐层封装表示哪个规则优先,最后调用实现类重写的filter方法完成调用,这个时候我们发现filter方法和next方法是重复逻辑(模板模式),并且我们可以将new对象的过程封装起来成为两个可以直接调用的类供用户选择(比如什么优先由用户决定,这个时候只需要调用那个策略对象即可【策略模式】)

利用设计模式优化代码

上边的代码每个规则中filter方法和next方法都是重复一样的,我们创建抽象类提取

1)定义抽象规则类
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 规则抽象类
 */
public abstract class AbstractProcessRule implements IProcessRule {

    private IProcessRule next;

    public AbstractProcessRule(IProcessRule next) {
        this.next = next;
    }

    @Override
    public List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS) {
        List<ServeProviderDTO> result = this.doFilter(serveProviderDTOS);

        //如果没有下一级规则,直接返回结果
        if (next == null) {
            return result;
        }
        if (CollUtils.size(result) <= 1) {
            // 选出唯一的服务者了,直接返回
            return result;
        }

        // 选不出唯一的,就调下一个规则进行筛选
        return next.filter(result);
    }

    public abstract List<ServeProviderDTO> doFilter(List<ServeProviderDTO> serveProviderDTOS);
}
2)修改实际规则类

修改每个规则类:下边以DistanceRule举例。

java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;
import com.jzo2o.orders.dispatch.rules.IDispatchRule;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 规则: 距离近者优先
 */
public class DistanceRule extends AbstractProcessRule {
//    private IProcessRule next;

    public DistanceRule(IProcessRule next) {
        //this.next = next;
        super(next);
    }

//    @Override
//    public List<ServeProviderDTO> filter(List<ServeProviderDTO> serveProviderDTOS) {
//        List<ServeProviderDTO> result = this.doFilter(serveProviderDTOS);
//
//        //如果没有下一级规则,直接返回结果
//        if (next == null) {
//            return result;
//        }
//        if (CollUtils.size(result) <= 1) {
//            // 选出唯一的服务者了,直接返回
//            return result;
//        }
//
//        // 选不出唯一的,就调下一个规则进行筛选
//        return next.filter(result);
//    }

    //挑选距离最近的服务者列表返回
    public List<ServeProviderDTO> doFilter(List<ServeProviderDTO> serveProviderDTOS) {
        System.out.println("按距离排序");
        if (CollUtils.size(serveProviderDTOS) < 2) {
            return serveProviderDTOS;
        }
        // 2.按照比较器进行排序,排在最前方优先级最高
        serveProviderDTOS = serveProviderDTOS.stream()
                .sorted(Comparator.comparing(ServeProviderDTO::getAcceptanceDistance))
                .collect(Collectors.toList());

        // 3.遍历优先级最高一个数据
        ServeProviderDTO first = CollUtils.getFirst(serveProviderDTOS);

        //获取跟第一个相同级别的
        return serveProviderDTOS.stream()
                .filter(origin -> origin.getAcceptanceDistance().compareTo(first.getAcceptanceDistance()) == 0)
                .collect(Collectors.toList());
    }
}
3)定义策略接口
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;

import java.util.List;

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

import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.orders.dispatch.model.dto.ServeProviderDTO;
import com.jzo2o.orders.dispatch.rules.IDispatchRule;

import java.util.List;
import java.util.Objects;

/**
 * 抽象策略类
 */
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);
    }
}
5)定义距离优先策略类
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

/**
 * 距离优先策略: 先距离优先,距离相同再判断单数
 */
public class DistanceStrategyImpl extends AbstractStrategyImpl {
    @Override
    protected IProcessRule getRules() {
        //构建责任链, 先距离优先,距离相同再判断接单数
        return new DistanceRule(new AcceptNumRule(null));
    }
}
6)定义最少接单优先策略
java 复制代码
package com.jzo2o.orders.dispatch.strategys;

/**
 * 最少接单数优先策略: 先最少接单优先,接单相同, 再找距离近的
 */
public class LeastAcceptOrderStrategyImpl extends AbstractStrategyImpl {
    @Override
    protected IProcessRule getRules() {
        // 构建责任链,先接单数优先,接单数相同再判断距离
        return new AcceptNumRule(new DistanceRule(null));
    }
}

具体实现

java 复制代码
    /**
     * 派单分发任务
     */
    @XxlJob("dispatch")
    public void dispatchDistributeJob(){
        while (true) {
            Set<Long> ordersDispatchIds = redisTemplate.opsForZSet().rangeByScore(DISPATCH_LIST, 0, DateUtils.getCurrentTime(), 0, 100);
            log.info("ordersDispatchIds:{}", ordersDispatchIds);
            if (CollUtils.isEmpty(ordersDispatchIds)) {
                log.debug("当前没有可以派单数据");
                return;
            }

            for (Long ordersDispatchId : ordersDispatchIds) {
                dispatch(ordersDispatchId);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    //由于一个订单3分钟处理一次,所以加锁控制3分钟内只加入线程池一次
    @Lock(formatter = RedisConstants.RedisFormatter.JSONDISPATCHLIST,time = 180)
    public void dispatch(Long id) {
        dispatchExecutor.execute(() -> {
            ordersDispatchService.dispatch(id);
        });
    }
java 复制代码
    public void dispatch(Long id) {
        // 1.数据准备
        // 1.1.获取订单信息
        OrdersDispatch ordersDispatch = ordersDispatchService.getById(id);
        if (ordersDispatch == null) {
            // 订单不在直接删除
            redisTemplate.opsForZSet().remove(DISPATCH_LIST, id);
            return;
        }

        // 1.3.服务时间,格式yyyyMMddHH
        int serveTime = ServeTimeUtils.getServeTimeInt(ordersDispatch.getServeStartTime());
        // 1.4.区域调度配置
        ConfigRegionInnerResDTO configRegionInnerResDTO = regionApi.findConfigRegionByCityCode(ordersDispatch.getCityCode());
        // 1.5.获取派单规则
        DispatchStrategyEnum dispatchStrategyEnum = DispatchStrategyEnum.of(configRegionInnerResDTO.getDispatchStrategy());


        // 2.修改下次执行时间(默认3分钟),防止重复执行
        ConfigRegionInnerResDTO configRegion = regionApi.findConfigRegionByCityCode(ordersDispatch.getCityCode());
        redisTemplate.opsForZSet().incrementScore(DISPATCH_LIST, id, configRegion.getDispatchPerRoundInterval());
        // 2.获取派单人员或机构
        // 2.1.获取派单服务人员列表
        List<ServeProviderDTO> serveProvidersOfServe = searchDispatchInfo(ordersDispatch.getCityCode(),
                ordersDispatch.getServeItemId(),
                100,
                serveTime,
                dispatchStrategyEnum,
                ordersDispatch.getLon(),
                ordersDispatch.getLat(),
                10);
        // 2.3.机构和服务人员列表合并,如果为空当前派单失败
        log.info("派单筛选前数据,id:{},{}",id, serveProvidersOfServe);
        if (CollUtils.isEmpty(serveProvidersOfServe)) {
            log.info("id:{}匹配不到人",id);
            return;
        }

        // 3.派单过规则策略
        // 3.1.获取派单策略
        IDispatchStrategy dispatchStrategy = dispatchStrategyManager.get(dispatchStrategyEnum);
        // 3.2.过派单策略,并返回一个派单服务人员或机构
        ServeProviderDTO serveProvider = dispatchStrategy.getPrecedenceServeProvider(serveProvidersOfServe);
        log.info("id:{},serveProvider : {}",id, JsonUtils.toJsonStr(serveProvider));

//        // 4.机器抢单
        OrderSeizeReqDTO orderSeizeReqDTO = new OrderSeizeReqDTO();
        orderSeizeReqDTO.setSeizeId(id);
        orderSeizeReqDTO.setServeProviderId(serveProvider.getId());
        orderSeizeReqDTO.setServeProviderType(serveProvider.getServeProviderType());
        ordersSeizeApi.machineSeize(orderSeizeReqDTO);
    }
相关推荐
CoderYanger2 小时前
C.滑动窗口-求子数组个数-越长越合法——2962. 统计最大元素出现至少 K 次的子数组
java·数据结构·算法·leetcode·职场和发展
小满、2 小时前
Redis:高级数据结构与进阶特性(Bitmaps、HyperLogLog、GEO、Pub/Sub、Stream、Lua、Module)
java·数据结构·数据库·redis·redis 高级特性
嘟嘟w2 小时前
双亲委派的概念
java·后端·spring
SunnyDays10112 小时前
如何在 Java 中将 RTF 转换为 PDF (含批量转换)
java·rtf转pdf
IT_Octopus2 小时前
java <T> 是什么?
java·开发语言
月明长歌2 小时前
【码道初阶】Leetcode面试题02.04:分割链表[中等难度]
java·数据结构·算法·leetcode·链表
silence2502 小时前
Maven Central 上传(发布)JAR 包流程
java·maven·jar
qq_381454992 小时前
数据脱敏全流程解析
java·网络·数据库
郝学胜-神的一滴2 小时前
设计模式依赖于多态特性
java·开发语言·c++·python·程序人生·设计模式·软件工程