「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
往期文章
- 《从零开始的毕业设计》-ELS中台快递物流调度系统(一)搭建项目环境 - 掘金 (juejin.cn)
- 《从零开始的毕业设计》-ELS中台快递物流调度系统(二)基本订单功能 - 掘金 (juejin.cn)
- 《从零开始的毕业设计》-ELS中台快递物流调度系统(三)基本物流地址功能 - 掘金 (juejin.cn)
前倾回顾
在上一篇文章中,我们完成了物流地址的数据准备部分,并且使用了Caffeine作为本地缓存,可以更快地响应。在本文中,我们将实现物流详情的。物流详情是本项目中的一个亮点。
物流服务
在ELS物流调度系统中,我们只负责物流订单的调度,还需要其他很多第三方的接入。比如快递系统、交易系统、电商系统等。因此我们需要封装好一个服务模板来给第三方实现,不仅仅是在接口层面的封装,而是抽象出一套服务模板template。以一种商品的形式展示。
比如我们短信服务中,提前定义好模板,然后让用户在我们的模板中填写自己自定义的部分。
在我们项目中的服务关系如下:
我们先抽象出服务模板,然后第三方快递公司根据接口封装成自己的模板,我们提前将模板录入到数据库中,后续可以加入缓存中,因为模板数据一般来说改动不大。
这里挖个坑,可以用策略模式和模板方法2种设计模式进行设计。能设计出更好的可扩展性,不需要大量if-else的语句,并且第三方快递公司如果有修改的话,也不需要更改原有的代码,符合开闭原则。
录入数据
像物流公司和运费模板等数据,前期我们可以提前自己输入进数据库就可以,不需要后期通过后端进行设置。能剩下很多功夫。如果后期公司业务做大了,可以设置一个接口专门用来添加运费模板和服务模板等信息。
物流详情
物流详情主要就是跟第三方快递公司交互的。里面包括物件的各种信息。当我们订单下达之后,就要生成物流详情,然后告诉第三方快递公司。具体的物流的运转我们不需要知道,只需要知道他的state即可。具体如下:
生成物流详情
生成物流详情,可以有很多种实现方法。这边列举作者能想到的几种,后续大伙可以补充。
物流订单直接调用
这个是最简单的,就是当我们物流订单创建完毕之后,通过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)。
消息队列
走网络IO太不够优雅了,order对象而是还不是一个小的对象,里面有很多字段,带来的网络消耗还是比较大的。有没有更好的办法呢?
我们可以网络IO太慢,我们走消息队列阿!
当订单创建完了之后,通过订阅/发布的消息模型,订单发布消息,然后物流详情消费消息,接收到了信息就生成物流详情。这样肯定是比直接订阅还快,而且QPS、吞吐量可以提升一个档次,物流订单也不会承受很大的压力。注意的是,我们消息传的是order的ID,这样我们后续再去物流订单中查询具体的order即可。
有的人可能就会问了: 这不还是要回去订单查询order?
与直接调用的区别
雀氏是要回去找,但是这个跟直接调用的方式可不是一样的哦。直接调用还是在生成订单那个方法中,并且里面有事务。如果一个方法中,太多的业务逻辑,就会影响该方法的RT。我们希望的是下单这个流程尽可能地高可用,因此要进行业务地解耦。并且我们是进行了微服务的拆分,消息队列其实就是将订单的压力,转为给物流详情了。
总结
简单的理解就是:订单可以创建,但是我不保证订单一创建,物流详情就创建。物流详情拿到了消息,可以慢慢地执行查询语句。但是订单必须进行持久化存储,防止丢失。之后就不需要再管了。
就好像电商系统中,一般购物会有3个环节:扣库存、下单、交易
如果你是下单——交易——扣库存。这样的的流程,那么你的系统的并发会很低。
但是如果你是预扣库存——下单——交易。跟前者就不是一个档次了。感兴趣可以开一篇讲讲,这里就不细说了,里面还涉及延时队列等等一系列的技术。
具体的实现,我就不贴代码了,因为我没采用这个办法,我采用了个更狠的!
延时队列
为什么最后还是没有采用消息队列呢?其实我消息队列的代码都写好了,写完之后调试发现,有一个很严重的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去查找,发现找不到对象。
原因是:具有时延性,数据库中还没创建好对象,那边就开始查的。
那我肯定不能再用个定时任务去一直轮询对象有没有创建好,而且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);
}
}
效果

结尾
在本文中已经完成了通过延时队列生成物流详情,并且整个系统的最基本业务流程: 生成订单——物流详情也完成了。后续就是对基本功能的一个完善何优化了。
本篇待完成
- 用设计模式去重构物流服务,关于模板那一部分
- 把物流地址那部分也加入进来
- 考虑是否写一个快递端和交易端,自己模拟快递的运转和交易信息。