吊打面试官的Java项目经验一:物流系统

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: java面试一般分为两部分,技术面试和项目面试,相信大多数小伙伴们都刷过很多技术性的面试题,连博主本人也刷过很多无聊的面试题,但是对于项目经验的面试,可能很多刚入行小伙伴属于一个空白期,本文主要围绕一个物流系统,介绍了一些实际开发中遇到的业务和经验,我一共介绍了六个模块,供大家参考,但是实际开发中,一个人不会同时开发这么多模块,大家根据需要改造2-3个模块到自己的项目中就可以,切记一定不要完全照搬,其中模块二是最有技术参考性的,包含了分布式系统事务问题、分布式幂等性、和支付相关问题,大家可以着重参考

引言:

java面试一般分为两部分,技术面试和项目面试,相信大多数小伙伴们都刷过很多技术性的面试题,连博主本人也刷过很多无聊的面试题,但是对于项目经验的面试,可能很多刚入行小伙伴属于一个空白期,本文主要围绕一个物流系统,介绍了一些实际开发中遇到的业务和经验,我一共介绍了六个模块,供大家参考,但是实际开发中,一个人不会同时开发这么多模块,大家根据需要改造2-3个模块到自己的项目中就可以,切记一定不要完全照搬,其中模块二是最有技术参考性的,包含了分布式系统事务问题、分布式幂等性、和支付相关问题,大家可以着重参考

项目介绍:

开发环境:idea+JDK1.8+maven+MySQL+Windows10+Linux

软件框架:Spring Cloud+Spring Boot+Mybatis-plus+xxl-job+Shiro+RocketMQ+

ElasticSearch+Redis+MongoDB+Nginx

项目描述:该项目为一个物流为主的业务类型,有着不同的邮寄方式(上门取件,网点自寄),用户在浏览器上填写相关信息,并选择货车以及货物体积,服务端会校验该订单所属价格,用户点击支付后生成订单,快递员会进行上门取件,系统会通过订单的类型以及状态进行跟踪,等到达终点后,快递员通过发送短信的方式让收件人取件

面试回答:

物流最主要的就是将寄邮的物品从出发地点寄往终点站,所以我们的项目就 是从用户选择不同的邮寄方式开始(例如,普通邮寄、一站式包车和预约寄件) 通过不同的业务逻辑计算出物流订单的价格,然后用户通过下单并支付,真正的 生成订单,系统会根据不同的订单类型和订单状态,进行派单。

架构:

技术选型:

当时在开发这个项目的时候是我和几位同事一起弄得的因为考虑到网站的访问量会随着市场占有率的提升而增加,所以选用了市面上比较流行的微服务架构SpringCloud+Nacos,当然也考虑过dobbo+zookeeper(进行优势比较,SpringCloud和dobbo的区别),相比之下,

我们选择使用SpringCloud,因为SpringCloud提供了微服务场景下的一站式解决方案,有很多成熟组件供我们开发使用,比如gateway、feign等,而且调用方式使用的是更加轻量的RestFulAPI,开发效率高,各服务之间代码解耦合,因此我们选择使用SpringCloud微服务架构

由于考虑到我们项目在上线之后,用户量会越来越多,为了让我们的程序能够承担更大的并发请求,因此我们把 nginx作为项目的入口(nginx的好处和作用),并且为了防止 ngnix出现单点故障,我们使用两台 ngnix做主备,主节点提供一个公网的虚拟 ip,当主节点挂掉从节点继续做出响应。然后为了避免有的用户恶意刷端口,我们就用 nginx做了一个限流,限制用户的访问频率和系统的最大并发量。(漏桶算法的原理)

然后呢用户的请求就会通过 nginx转发到gateway服务网关集群,由于gateway网关作为我们整个系统的入口,负责对请求进行过滤、校验、路由分发,因此我们还基于gateway做了限流以应对高并发

服务统一注册到Nacos注册中心集群,之所以采用 Nacos是因为 Nacos的强大之处在于,不仅可以作为服务的注册中心,而且还可以作为服务的配置中心来使用(nacos与其他几大注册中心的区别)。

由于我们用的是微服务架构,当一个客户端请求需要调用多个服务才能完成时,因此我们在服务之间的调用就使用到了 feign,feign具有负载均衡和熔断的能力。可以让我们的访问压力分摊,当服务请求失败时还可以快速做出响应,防止出现服务雪崩的现象。(feign为什么具有负载均衡和熔断的能力)

像网站的首页您也知道是一个查询比较多但更新比较少的模块,所以我们采用的是

redis缓存以防止大量的请求对数据库造成压力,然后我们为了提高 redis的高可用,对

Redis做了一个集群。(redis为什么那么快,redis集群,redis的高可用架构)

然后呢在搜索方面我们使用 es索引库,由于 es的实时性比较高而且支持分布式,所以我们选择了使用 es索引库(es近实时,倒排索引,与 solr的区别,logstash的同步原理);在一些需要异步处理的场景,我们采用的是 rocketmq消息中间件。并且我们也是采用rocketmq的事务消息去解决我们项目中的分布式事务的问题(rocketMq事务消息,rocketMq的好处等)。

那么这些就是我们当时在设计微服务架构的时候给出方案。

个人负责开发模块一:寄件

用户点击下单之前,我们首先要判断用户是否登录,如果没登录,就提示用户登录,如果已经登录就允许用户填写数据并下单.

我们的寄件分为五种寄件模式:1.上门取件 2.网点自寄 3.委托别人寄4.公益寄件 5.同城 30分钟达。

如果是网点自寄的话,可以根据货物的体积,重量,选择货车的规格,然后就是去填写地址信息,寄件人信息和收件人的信息.它的价格,我们根据我们自己写的一个价格时效工具类估算订单的价格,实际费用收取由工作人员看到货物后进行收费.

如果是普通订单直接下单即可.如果是网约订单的话,我们提供上门取件服务,在当天用户可以预约明天的上午九点到下午五点的上班时间段,预约具体时间中间间隔半个小时(例如:上午 9点、9点 30、10点以此类推).当一个用户下单,把用户的下单信息存入数据库之后,返回该订单的数据,以便之后的支付使用.

如果是上门取件,我们也使用了一个定时任务(SpringQuartz)每 30分钟执行一次,这个时候我们查询订单信息中状态为未取件的订单以及该订单的取件地址,取件时间和发件人的信息,然后我们再查询出没有处于工作状态中的快递员以及手机号,依据订单的取件时间提前 30分钟通过 MQ异步发送短信的方式将该订单的地址以及发件人的信息发送给该快递员,该快递员上门取件.

派单的话,如果是站点订单我们使用的是一个 5分钟执行一次的定时任务查询订单是否是未处理订单,如果是未处理订单,然后查询出来没有处于工作中的快递员手机号,通过 MQ异步发送短信或邮件的方式,将该单派给该快递员,员工接单后,电话联系客户。

个人负责开发模块二:订单支付(阿里Pay)

用户对运输信息进行填写完毕后,并且生成相对应的订单,这时该笔支付状态为未支付状态,用户可以在订单页面查看到未支付的订单,并且选择支付方式

(支付方式为立即支付或者货到付款)。

在用户付款成功后,支付宝有两个支付回调,理论上是先执行同步回调再执行异步回调,但是实际上同步回调和异步回调是不分先后的。当用户扫码完成之后支付宝会直接返回给用户支付成功,执行同步回调,同步回调的值我们一般都是让他跳转到一个页面,提示用户已经支付成功了。

在支付成功的异步回调逻辑中,最重要的就是将订单的支付状态修改为已支付,并且将支付宝返回的支付信息如交易流水号添加到我们的订单表中,然后再根据用户实际支付金额以及应付金额对用户的积分进行修改。

由于支付模块以及积分模块是两个不同的服务,所以这时候就涉及到了分布式事务。

保证分布式事务一致性的方案有很多种,我们使用的是 rocketmq来保证事务的一致性。

RocketMQ的设计中呢,Producer端和broker具有双向通信能力,使得 Broker天生可以作为一个事务协调者存在;可以确保本地事务执行与消息发送的原子

性。而 RocketMQ本身提供的存储机制,使得事务消息具有了持久化能力;

RocketMQ的高可用机制以及可靠消息设计,使得事务消息即使在系统发生异常时,依然能够保证分布式事务的最终一致性达成。

我们在支付的异步回调中发送一个事务消息到 MQ,这个消息被叫做 prepare消息。这个消息不会被放入真正的业务 Topic中,而是先被放入 MQ内置的 Half

Topic中,这个消息不会被建立消息索引,这个消息对 Consumer端是不可见的。

MQ收到这个 prepare消息之后会给我们的 Product端一个反馈消息,告诉

Product端消息接收成功了。Product端接收反馈消息,需要实现

RocketMQLocalTransactionListener接口,然后重写里面的两个方法,我们要在其中一个方法中处理本地事务,也就是修改订单状态并且将支付信息保存到记录表中。

如果本地事务处理成功了,那么我们就返回给 MQ事务处理状态为 COMMIT,

此时 MQ就会将 Half Topic中的消息取出来并且生成消息索引,然后将这个消息转存到业务 Topic中,此时消息对消费者可见,消费者就开始处理增加积分的事务了。

如果本地事务处理失败了,那么我们就返回给 MQ事务处理状态为 ROLLBACK,此时 MQ不会生成消息索引,消息对消费者不可见,那么消费者也就不会增加积分。

如果本地事务处理超时或者宕机,那么 MQ就会不断询问其同组的所有

Product来回查事务处理状态。Producer收到回查消息后,到 Redis中检查回查消息对应的本地事务的状态,根据本地事务的状态,重新向 Broker返回 Commit或者 Rollback。

这个事务消息回查也很简单,我们要知道,事务消息的成功投递时需要经历三个 topic的,分别是 half topic、op topic和业务 topic。

首先呢,我们的 broker会维护一个死循环,默认每分钟执行一次,mq就通过 half topic和 op topic来存储事务消息的推进状态,其中 half topic中存放的就是 prepare消息,而 op topic中存放的呢是 prepare消息对应的状态也就是 commit或 rollback。mq呢就是通过对比这两个队列的差值来找到还没有提交事物处理结果的超时或者宕机的事物,然后调用 producer端相关方法来回查事物处理结果。

这个时候仅仅只是保证了本地事务与消息发送的原子性问题,也就是说,我们本地事务只要执行成功,那么消息就一定能够发送出去。

还有一点就是消费者端进行添加积分操作的时候,我们通过 ACK重试机制和死信队列人工干预,来确保消息肯定会被消费成功。Broker为了保证消息一定会被消费成功,只有当消费者明确返回了消息消费成功的信息,也就是返回了

CONSUMER_SUCCESS的时候,RocketMQ才会认为消息消费成功。当然,我们是对消息进行手动 ACK返回的操作。确保在中途断电、抛出异常等情况时都不会认为消息消费成功,即都会发起重试。

RocketMQ默认重试次数是 16次,当达到最大重试次数之后会将该消息投递到死信队列中。我们可以根据业务需要自定义消费的最大重试次数。当死信消息投递到死信队列中后,死信消息的业务就需要进行人工干预。其实重试个三五次就可以认为当前业务存在异常,继续重试下去也没有意义了,那么我们就将当前这条消息进行提交,返回 broker状态为 CONSUME_SUCCESS让消息不再重发,同时将该消息存入我们业务自定义的死信消息表(Redis)中,将业务参数入库,相关的运营通过查询死信消息表来进行对应的业务补偿操作。

正是因为有了消息重试,所以我们还要考虑消息被重复消费的问题。我们需要自定义一张 Redis日志表,将已经消费成功的消息的 id放在这个日志表中,如果新收到的消息的 id已经在日志表中,说明这条消息已经消费过了,就不再消费这条消息。

当用户长时间未支付时

如果用户选择线上支付却一直没有支付,针对于用户长时间不支付的问题。我们是限制用户支付时间是 24小时,在我们订单生成的时候我们通过 rocketMQ发送一个延迟消息,指定消费时间为 24小时,这个消息监听是在 24小时间才能进行消费,消费的时候拿到发送过来的订单号去查询数据的支付状态,如果当前的状态是未支付,我们会将修改为支付超时,然后给他发送短信或者是邮件提醒支付。如果货物送到准备签收前,还没有支付,站点可以打电话询问寄件方是否是货到付款,如果是就让收件方支付,修改他的支付方式为货到付款。如果不是他不选择货到付款就让他付款,成功之后才让取货。

分布式事务为什么要使用 rocketMQ:

因为其他的都具有强一致性,而 rocketMQ的效率相比其他而言来说比较快,而且我们之前团队一直使用的都是 rocketMQ,对它也比较熟悉;

个人负责模块三:积分模块

我们的换算方式很简单就是消费一元兑换一分.

物流达到目的地后,等待用户签收,签收成功后发送事务消息,增加用户总积分.

用户的积分可以用来兑换一些生活用品,当然都是包配送的,下单即可.

积分---同样也可以用来抢购一些商品,先是通过 gateway网关设置 datetime,限制一个抢购时间,也就是预热,防止高并发导致服务器崩溃.

并且我们的商品也都是提前存入 redis中的,这是为了防止超卖,借助 redis的 decr,首先他们是可以保证原子性的,通过商品 id作为 key,当用户点击抢购的时候发送请求到服务端,根据传递过来的商品 id进行查询 redis库存和抢购开始时间的查询,如果当前时间小于抢购开始时间,直接返回错误,防止用户拿到接口直接调用的问题.

然后判断库存,如果库存小于等于 0直接返回失败,这样的话后面的大量请求无需给系统带来压力.

如果当前库存还有的话,那么会根据当前的用户 id和商品 id进行 redis查询,我们是在抢购成功时通过 redis给当前用户做了一个标记,目的是避免一个账户重复抢购的情况.

然后如果库存充足,且无重复抢购情况,执行 decr操作,decr操作的话会执行减一并且返回当前的值,然后再次进行判断当前值是否小于 0,如果是则返回失败.否则调用 redis通过用户 id和商品 id做 key,值是默认给的一个 success代表抢购成功(就是刚刚所说的重复抢购问题),然后 mq异步的方式生成订单并且返回前端一个成功标记.

个人负责模块四:订单搜索

我们通过订单查询的时候,需要到数据库中进行多表联查,然后展示到前台页面。但是在搜索的时候考虑到用户量并发和连表联查可能会有几个问题:

首先会增加数据库连接数,给数据库造成压力;

其次联查的数据量大的话查询效率会很低(引入正排索引和倒排索引)

所以我们将联查出来的数据信息保存到 ES中,也考虑过 solr ,虽然都是基于Lucene实现的,但是 solr建立索引时候,搜索效率下降,实时搜索效率不高,es实时搜索效率相对比较高,而且 es支持分布式,节点对外表现对等,加入节点自动均衡,使用 ES的话可以实现模糊搜索、分词查询、高亮显示、复合查询等。

索引库更新,关于这个索引库的更新我们使 n用的是 logstash进行数据库和 ES的数据同步,他的工作原理很简单,就是定时执行我们配置文件中所定义的 sql语句,他需要两个插件一个是读取 msql数据的插件,一个是同步 ES的插件。

在使用 ES搜索的时候,还需要注意分词器的问题。 elasticSearch默认是自带分词的,但是 es是国外的产品,他们只对英文分词,而我们汉语词语他会将每一个汉字分成一个词语,因此我当时选择了咱们国内常用的 IK分词器(听说过庖丁分词器)查询高亮: ES查询的时候,我们可以设置高亮查询,指定高亮样式和高亮字段进行高亮显示将符合条件的订单信息搜索出来进行分页展示(ES在大数量情况下如何提高查询效率)

个人负责模块五:客服模块(es、mongodb、websocket)

现在的客服分为两种,一种是自动回复,一种是人工客服.

自动回复,我们通过es的ik分词器,将一些关键词分词后存入es中,通过用户传入的信息,

来进行自动回复.比如用户输入快递,就会出现(快递几天发货,快递几天送达,发送什么快递)

等一系列信息,用户在根据自己的需要进行查询,最后将聊天记录保存到mongodb中.

人工客服,我们通过调用websocket(搜客特),实现点对点聊天,当用户需要人工干预时,会创建一个session会话作为唯一标识,然后将这个唯一标识发送到指定的客服机器上,建立用户与客服的通话通道,直至对话关闭,将聊天记录保存到mongodb中.

Websocket是一种用于H5浏览器的实时通讯协议,可以做到数据的实时推送,可适用于广泛的工作环境,例如客服系统、物联网数据传输系

个人负责模块六:交接单模块

交接单模块:我们在进行装货前,根据目的地配送点的数量生成相应的交接单。比如该车货物需要去五个配送点,则需要产生五个交接单,每个交接单根据装货车辆列出。

我们当时考虑到交接单会产生大量的数据,并且数据结构是比较松散的,我们就想到使用 MongoDB来进行保存交接单信息,订单状态在运送过程中会不断更新,以 MongoDB内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。

之所以使用MongoDB是因为 MongoDB是 NoSQL的非关系型数据库,易于扩展,可以进行分布式文件存储因此我们在项目中使用它来存储交接单信息(如订单 id、站点 id、内容、配送人、订单状态、配送员电话等等),并且为了提高可用性和高并发,当时我们采用了 3台服务器做了 mongodb的副本集,其中一台作为主节点,另外两台作为副本节点,这样在任何一台 mongodb服务器宕机时就会自动进行故障转移,不会影响应用程序对 mongodb的操作,为了减轻主节点的读写压力过大的问题,我还对 mongodb副本集做了读写分离,使写操作在主节点进行,读取操作在副本节点进行。

目录
相关文章
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
5天前
|
XML Java 测试技术
从零开始学 Maven:简化 Java 项目的构建与管理
Maven 是一个由 Apache 软件基金会开发的项目管理和构建自动化工具。它主要用在 Java 项目中,但也可以用于其他类型的项目。
21 1
从零开始学 Maven:简化 Java 项目的构建与管理
|
3天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
26 14
|
20天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
25天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
15天前
|
Java Android开发
Eclipse 创建 Java 项目
Eclipse 创建 Java 项目
35 4
|
20天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
33 3
|
21天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
43 4
|
22天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
24天前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
59 3