《从零开始的毕业设计》-ELS中台快递物流调度系统(四)基本物流详情功能

767 阅读8分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

往期文章

前倾回顾

在上一篇文章中,我们完成了物流地址的数据准备部分,并且使用了Caffeine作为本地缓存,可以更快地响应。在本文中,我们将实现物流详情的。物流详情是本项目中的一个亮点。

物流服务

在ELS物流调度系统中,我们只负责物流订单的调度,还需要其他很多第三方的接入。比如快递系统、交易系统、电商系统等。因此我们需要封装好一个服务模板来给第三方实现,不仅仅是在接口层面的封装,而是抽象出一套服务模板template。以一种商品的形式展示。

比如我们短信服务中,提前定义好模板,然后让用户在我们的模板中填写自己自定义的部分。

image.png

在我们项目中的服务关系如下:

image.png

我们先抽象出服务模板,然后第三方快递公司根据接口封装成自己的模板,我们提前将模板录入到数据库中,后续可以加入缓存中,因为模板数据一般来说改动不大。

这里挖个坑,可以用策略模式模板方法2种设计模式进行设计。能设计出更好的可扩展性,不需要大量if-else的语句,并且第三方快递公司如果有修改的话,也不需要更改原有的代码,符合开闭原则。

录入数据

像物流公司和运费模板等数据,前期我们可以提前自己输入进数据库就可以,不需要后期通过后端进行设置。能剩下很多功夫。如果后期公司业务做大了,可以设置一个接口专门用来添加运费模板和服务模板等信息。

image.png

image.png

物流详情

物流详情主要就是跟第三方快递公司交互的。里面包括物件的各种信息。当我们订单下达之后,就要生成物流详情,然后告诉第三方快递公司。具体的物流的运转我们不需要知道,只需要知道他的state即可。具体如下:

img

生成物流详情

生成物流详情,可以有很多种实现方法。这边列举作者能想到的几种,后续大伙可以补充。

物流订单直接调用

这个是最简单的,就是当我们物流订单创建完毕之后,通过restTemplate调用物流详情,告诉他要生成物流详情。

    @PostMapping("/order")
    public ResultVO creatOrder(@RequestBody LogisticsOrder order) {
        ResultVO result = new ResultVO();
        result.setCode(200);
        result.setMsg("true");
        logger.info("开始创建订单");
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                try {
                    return service.save(order);
                } catch (Exception e) {
                    logger.error("创建订单失败,id:{}", order.getId(), e);
                    transactionStatus.setRollbackOnly();
                    return false;
                }
            }
        });
        //直接调用
        restTemplate.postForObject("http://logistics-services/logistics-service/test", order,LogisticsOrder.class);
        return result;
    }

能完成最基本的业务需求,但是优化的空间还是很大的。要注意的是用的是编程式事务,用了transactionTemplate,如果是直接用声明式事务,那么这个事务中就要承受网络IO的压力,有可能就会产生大事务,然后导致主从复制延迟等一系列问题。具体想要了解编程式事务可以看我以前的一篇有具体的介绍 《从零开始的毕业设计》-ELS中台快递物流调度系统(二)基本订单功能 - 掘金 (juejin.cn)

消息队列

image.png

走网络IO太不够优雅了,order对象而是还不是一个小的对象,里面有很多字段,带来的网络消耗还是比较大的。有没有更好的办法呢?

我们可以网络IO太慢,我们走消息队列阿!

当订单创建完了之后,通过订阅/发布的消息模型,订单发布消息,然后物流详情消费消息,接收到了信息就生成物流详情。这样肯定是比直接订阅还快,而且QPS、吞吐量可以提升一个档次,物流订单也不会承受很大的压力。注意的是,我们消息传的是order的ID,这样我们后续再去物流订单中查询具体的order即可。

有的人可能就会问了: 这不还是要回去订单查询order?

image.png

与直接调用的区别

雀氏是要回去找,但是这个跟直接调用的方式可不是一样的哦。直接调用还是在生成订单那个方法中,并且里面有事务。如果一个方法中,太多的业务逻辑,就会影响该方法的RT。我们希望的是下单这个流程尽可能地高可用,因此要进行业务地解耦。并且我们是进行了微服务的拆分,消息队列其实就是将订单的压力,转为给物流详情了。

总结

简单的理解就是:订单可以创建,但是我不保证订单一创建,物流详情就创建。物流详情拿到了消息,可以慢慢地执行查询语句。但是订单必须进行持久化存储,防止丢失。之后就不需要再管了。

就好像电商系统中,一般购物会有3个环节:扣库存、下单、交易

如果你是下单——交易——扣库存。这样的的流程,那么你的系统的并发会很低。

但是如果你是预扣库存——下单——交易。跟前者就不是一个档次了。感兴趣可以开一篇讲讲,这里就不细说了,里面还涉及延时队列等等一系列的技术。

具体的实现,我就不贴代码了,因为我没采用这个办法,我采用了个更狠的!

image.png

延时队列

为什么最后还是没有采用消息队列呢?其实我消息队列的代码都写好了,写完之后调试发现,有一个很严重的bug,就是数据的不一致性

消息队列的缺点

   @PostMapping("/order")
    public ResultVO creatOrder(@RequestBody LogisticsOrder order) {
        ResultVO result = new ResultVO();
        result.setCode(200);
        result.setMsg("true");
        logger.info("开始创建订单");
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                try {
                    return service.save(order);
                } catch (Exception e) {
                    logger.error("创建订单失败,id:{}", order.getId(), e);
                    transactionStatus.setRollbackOnly();
                    return false;
                }
            }
        });
        //发送消息
        redisTemplate.convertAndSend(ELSConstants.REDIS_ORDER_SERVICE_QUEUE, order.getId());
        return result;
    }

我们在创建订单完毕之后,在redis中发送消息给物流详情。然后物流详情拿着id去查找,发现找不到对象。

image.png

原因是:具有时延性,数据库中还没创建好对象,那边就开始查的。

那我肯定不能再用个定时任务去一直轮询对象有没有创建好,而且Redis中,消息只能被一次消费,消费了之后就没了。这个办法不行。这时候就要靠我们最后的主角Redis的延时队列了。

具体实现

物流订单:

@PostMapping("/order")
public ResultVO creatOrder(@RequestBody LogisticsOrder order) {
    ResultVO result = new ResultVO();
    result.setCode(200);
    result.setMsg("true");
    logger.info("开始创建订单");
    transactionTemplate.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus transactionStatus) {
            try {
                return service.save(order);
            } catch (Exception e) {
                logger.error("创建订单失败,id:{}", order.getId(), e);
                transactionStatus.setRollbackOnly();
                return false;
            }
        }
    });
    //存入延时队列
    redisTemplate.opsForZSet().add(ELSConstants.REDIS_DELAY_QUEUE_ORDER, String.valueOf(order.getId()), System.currentTimeMillis());
    //把对象也存入里面,物流详情可以先通过redis获取,如果获取不到,再进行http调用
    redisTemplate.opsForValue().set(String.valueOf(order.getId()), order, 12000 + new Random().nextInt(6000), TimeUnit.MILLISECONDS);
    return result;
}

物流详情:

物流详情这边需要有一个监视器去轮询Redis的队列,去查看是否有数据。

@Component
@Slf4j
public class RedisDelayQueueRunner implements CommandLineRunner {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private LogisticsDetailServiceImpl service;

    @Override
    public void run(String... args) throws Exception {
        new Thread(() -> {
            while (true) {
                Set set = redisTemplate.opsForZSet().rangeByScore(ELSConstants.REDIS_DELAY_QUEUE_ORDER, 0, System.currentTimeMillis());
                if (set.isEmpty()) {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                log.info("Redis延时队列有数据了");
                Iterator<String> iterator = set.iterator();
                while(iterator.hasNext()){
                    //取出的数据
                    String data= iterator.next();
                    //todo 处理数据
                    handleData(data);
                    //用完之后,将key从zset处删除,避免重复
                    redisTemplate.opsForZSet().remove(ELSConstants.REDIS_DELAY_QUEUE_ORDER,data);
                    log.info("执行完一条任务id {},并成功删除",data);
                }
            }
        }).start();
        log.info("Redis延迟队列启动成功");
    }

    public void handleData(String id){
        LogisticsOrder order = JSON.parseObject(JSON.toJSONString(redisTemplate.opsForValue().get(id)), LogisticsOrder.class);
        if(order==null){
            //redis里面没有数据了,要去物流订单自己找
            //todo: 调用物流订单的
        }

        LogisticsDetail detail = new LogisticsDetail();
        //深拷贝
        BeanUtils.copyProperties(order,detail);
        //todo:可以改为事务
        detail.setOuterOrderType(order.getOrderType());
        service.save(detail);
    }
}

效果

image-20220131145438006

结尾

在本文中已经完成了通过延时队列生成物流详情,并且整个系统的最基本业务流程: 生成订单——物流详情也完成了。后续就是对基本功能的一个完善何优化了。

本篇待完成

  • 用设计模式去重构物流服务,关于模板那一部分
  • 把物流地址那部分也加入进来
  • 考虑是否写一个快递端和交易端,自己模拟快递的运转和交易信息。