Java项目经验二:二手车系统

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: XX二手车的服务贯穿二手车交易各个环节,运用成熟的互联网技术,以海量、真实的二手车信息为基 础,坚持诚信、公正的准则,通过政策解析、价格评估、担保、置换和保险等服务,建立专业、严谨、使用 便捷的交易体系,推动中国二手车行业的良性发展。

1、项目简介

开发环境:IDEA+ MySQL + JDK1.8 + Git + Maven

使用技术:Spring Cloud + Mybatis Plus + MySQL + RocketMQ + Nginx + Nacos + Redis + MongoDB + ElasticSearch + Shiro

项目描述:

XX二手车的服务贯穿二手车交易各个环节,运用成熟的互联网技术,以海量、真实的二手车信息为基 础,坚持诚信、公正的准则,通过政策解析、价格评估、担保、置换和保险等服务,建立专业、严谨、使用 便捷的交易体系,推动中国二手车行业的良性发展。

项目服务流程:

用户注册登录,卖家会通过客服对车辆进行信息采集和估价在平台展示信息。买家根据 当前城市和对车的需求,在平台中查找自己想要的车辆,找到后进行线下试车,从而完成交易。同时平台还 会定期进行二手车抢购福利。

项目主要模块:

搜索模块,广告模块,抢购模块(Redis),车辆智能推送(Redis),订单库存模块(分布式 事务),评论模块(MongoDB),在线聊天模块(WebSocket),车辆详情页模块(RocketMQ+多线程),车辆 评估模块(多线程),用户模块等主要模块。

二、项目介绍:

  • 项目介绍
  1. 我参与研发的是一个二手车的项目,项目的初衷就是秉承着买卖双方互惠互利和诚信经营的宗旨来做的,因为考虑到现在生活水平的上升,实现一户一车也是大势所趋,车子的存在其实就是为了便利我们生活的,所以有些家庭其实对车的要求就是无非是出行方便,冬暖夏凉的基本要求,这个时候二手车就成了不错的选择,性价比高,还能满足需求。
  2. 我们项目的主要的业务流程就是在用户第一次进入首页后,会根据热度高,新上架,比较优质的车辆来推荐展示,用户再次进入还会根据用户上次浏览多的车辆进行展示,我们还有订单、抢购、车辆评估等服务。
  3. 因为当时我们组的人比较少,所以我负责的模块相对多一些,有广告投放模块,搜索模块,订单模块,支付模块,抢购模块,车辆评估等都是我做的。
  4. 我们这个项目的主题就是卖家将车辆估价后我们运营人员审核完以后再挂到平台上进行售卖,然后买家进行购买,付款成功后我们抽取一定的手续费用这样的一个流程。
  5. 这个项目当时是和我们老大一起搭建的,所以我也比较了解,要不我就跟您说一下我们项目的架构吧。

三、项目架构

 

四、架构分析

1、首先,我们的项目使用的是Spring Cloud微服务架构,之所以选择Spring Cloud,是因为它可以使各个服务之间解耦合,每一个服务都可以独立的开发部署, 只关注一个业务功能,改善故障隔离,即使一个服务宕机也不会影响其他的服务的正常运行,而且开发起来方便快捷。(提供了一站式解决方案)(可能会问springcloud和dubbo的区别)

2、然后,我们选择了nacos作为注册中心,因为它不但可以作为服务的注册中心,还可以作为服务的配置中心,服务的配置中心分为config server和config client,由于nacos本身就是一个配置中心服务,因此我们只需在微服务中搭建config client即可,使用起来比较方便。(可能会问其他注册中心,注意nacos和zookeeper以及eureka的区别)

入口层

首先是入口层。当时考虑到项目上线后,用户量和访问量会越来越大,肯定会出现高并发的情况,所以呢,为了解决高并发,我们使用了nginx作为项目的入口,实现反向代理和负载均衡,还利用了它的限流功能(漏桶算法限流)来限制用户的访问频率。另外为了防止单点故障,我们给nginx做了一主一备。(可能会问为什么nginx的优势或nignx的负载均衡是怎么实现的或者负载均衡策略)

网关层

紧接着是网关层。使用zuul网关是为了更好的高并发限流处理,因为nginx作为项目的统一入口,它的限流是粗粒度的,所以必须通过网关加以弥补(也就是令牌桶算法二次限流)。我们还使用网关对整个微服务集群进行统一的校验和过滤,当nginx将用户请求转发到zuul网关后,由网关统一转发给对应的服务。然后为了保证网关服务的高可用,给Zuul网关也配置了集群。(zuul网关解决的问题,令牌桶和漏桶算法的区别)

服务层

接下来是服务层。当各个服务启动后,会注册到nacos上。然后各个服务间的调用是通过feign接口来实现的。由于feign集成了ribbon和hystrix,所以它具有熔断的能力,可以分摊我们的访问压力。当服务请求失败时还可以快速做出响应,防止出现服务雪崩的现象。并且可以通过zipkin链路追踪定位到出现问题的地方。(feign和ribbon的区别,服务熔断的作用,服务降级)

数据层

再往后就是数据层。我们用的是mysql数据库。为了提高数据的安全性,我们给mysql做了主从同步。当然了,对于一些热数据(查新比较多更新比较少),我们使用的是redis缓存来处理的,因为它不但速度快,而且可以减轻数据库的压力。同时,为了提高redis的可用性,我们给它搭建了集群。(mysql主从同步,读写分离,mycat分库分表)

用户评论

另外,用户评论这一块由于数据量比较大而且结构松散,所以我们使用了mongodb数据库。同样的,为了提高可用性和系统性能,我们搭建了mongodb副本集。(mongodb副本集:主、从、仲裁)

搜索

还有就是在搜索方面我们使用了es索引库,因为es的实时性还是比较高的而且支持分布式,数据库同步这方面使用的是logstash,实现数据源数据的统一。(es近实时,倒排索引,与solr的区别,logstash的同步原理)

中间件

最后在一些需要异步处理的场景中,我们使用的是rocketmq消息中间件。并且我们也是使用rocketmq的事务消息来解决我们项目中的分布式事务,比如订单支付、扣减库存等。(分布式事务解决方案和事务消息流程)

我们这个项目的整体架构大概就是这样,您看还有什么想要了解的?

当然除了架构以外我还参与了抢购、广告、限时特惠、比如:我们的抢购是.......

个人负责模块一:搜索服务

搜索服务一:

搜索是一个并发量相当高的服务,并且一次搜索会关联到许多张表。因此我们如果是走数据库的话我们后台压力可能会非常大,所以这边我采用的是搜索引擎es索引库而不是从数据库直接查询。

搜索服务二:

首先简单说明一下,es是java编写的,它提供了简单易用的RestfulApi,我们可以通过RestfulApi轻松实现搜索功能,不需要面对lucene的复杂性,从而使全文搜索变得简单。其次他还支持横向扩展,支持pb级的结构化或非结构化的海量结构处理;简单的说就是通过增加机器来解决存储容量问题。(这边也可以穿插个为什么不用solr)。至于es和数据库的同步问题,这边是采用的是logstash进行的同步,工作原理里很简单:就是定时执行配置文件中我们定义的sql。这边需要两个插件,一个是读取mysql数据的插件,一个是同步es的插件。

Es相对于mysql有这么几个优势:

1.首先es用的是倒排索引,倒排索引维护的是字典表,也就是key,key是我们搜索的关键词,而value是包含关键词的数据id;我们只需对比字典表就知道哪几条数据有我们查询的关键字,然后拿到这些数据的id,一下找到数据;

2.而mysql用的是正排索引,就是从头到尾把数据查出来,看那些数据包含我们查询的关键字,效率比较低。

3.其次就是mysql索引只存储字段内容,没有分词。而es存储的是分词以后的索引;

4.两者精确匹配没什么区别,但是一旦用到%在词的左边,mysql查询会特别慢;es只需查搜索词包含的文档id。

个人负责模块二:千人千面(猜你喜欢)

1、在做这个功能之前,我们考虑到每个用户喜欢的车辆都不一样,所以给每个用户展示的车辆是不同的,为了给用户良好的体验,我们根据用户浏览车辆的标签来进行定向推送。

2、当用户未登录或者第一次访问的时侯会直接推荐当下最热门、评分最高的车辆信息。

3、用户登录以后会随着不断浏览车辆信息,我们会直接在首页中定向推荐用户最近访问、频繁访问的相关车辆。

4、具体实现:首先平台会为车辆定下许多标签,运营人员在录入车辆时需要为车辆选定三个标签。然后用户在浏览车辆时,我们会将车辆单个标签存放到redis中,使用redis的zset数据类型,因为zset更擅长对权重值的控制。

5、以用户id+浏览量标识(num)为key,以标签名为value,以1为分数,进行存放;存放之前首先判断缓存中是否有该标签,如果有就将分数加1,如果没有则将分数设为1,(该缓存负责统计各个标签浏览量),然后再以用户id+时间标识(time)为key,以标签名为value,将当前时间毫秒值为分数放入redis(该缓存负责统计各个标签访问的时间)。

6、此时我们设置定时器每半个小时查询这两个zset ,然后获取浏览量排名前三的标签。然后将这三个标签以zset类型放入redis缓存,分数分别为3,2,1,然后获取用户最近访问的三个标签也放入这个redis缓存,分数统一设定为3,如果该标签已存在则将原有的分数加3(这个缓存是用来定向推荐的,每半小时更新一次)。当用户再次访问首页时获取用来定向推荐的缓存中的标签,然后进行多字段查询es,在首页中进行展示。

个人负责模块三:车辆回收

我们为卖家提供了一个二手车估价的功能,用户卖车的时候可以通过我们的车辆评估功能来对自己需要转手的车辆进行估价,用户点击首页的车辆评估按钮,会弹出填写车辆信息的弹框(包括所在城市、车辆品牌,车系、车款、车龄、行驶里程六个维度),用户填写完车辆评估信息之后,将评估信息提交到后台业务逻辑方法,后台会先通过车辆品牌,车系、车款这三个维度去调用京东智联云的一个评估的api接口,返回车辆的原始价格,将其存放到redis中,方便我们的取用,然后根据原始价格通过车龄、行驶里程、磨损程度进行一个折损计算,计算方法如下有

三个模块:

  1. 第一个就是检测报告:

重大问题检测、车辆外观检测、内饰功能检测、底盘悬架项检测、发动机舱检测、动态路试检测。0表示无损伤、1代表轻微损伤、2代表中度损伤、3代表重度损伤。

轻微损伤折旧5%、中度损伤折旧15%、重度损伤折旧30%。

  1. 然后是公里数:

目前国内里程评估法就一个,54321法: 5+4+3+2+1=15,即汽车开6万公里时,折旧5/15;

当开到第二个6万公里时,也就是开了12万公里,再折旧4/15,此时二手车价格为新车裸车价的1-5/15-4/15=6/15。

  1. 还有一个就是年份:

运用重置成本法将汽车寿命规定为15年,为了估值精确,将其处成180个月,使用了多少个月就把使用月份减掉,然后把剩余月份的残值计算出来。

二手车价格=新车价格X(180-已使用月份)÷180

------------------------------------

这时呢,我们考虑到运算速度和用户体验度方面,我们决定利用多线程的CallableCountDownLatch分别去并发执行运算任务,对这几个运算任务进行提速,根据每个维度计算出的对应的折损价格返回,当计算完成之后,分别调用CountDownLatch的countDown方法将计数器减1,当所有的线程都已执行完之后,这时候计数器的值为零,表示我们的三个线程都已执行完毕,再恢复计算总价格的线程任务,并将最后的估价结果返回给前台,这样可以提升查询效率,将结果快速地显示给用户,提高用户体验度。

------------------------------------

(Callable、ConuntDownLath

Callable:实现了callable接口的线程可以将执行结果返回,并且它的call方法可以允许异常的抛出。

CountDownLatch工作原理:

CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。 CountDownLatch作用:能够使一个线程等待其他线程完成各自的工作后再执行。CountDownLatch 底层是通过一个计数器来实现的, 计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。

当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。)

------------------------------------

然后我们每一辆车都有它对应的保值率,如果计算得出的价格低于它对应的保值率,我们就会把保值率的价格提供给用户,如果不低于对应的保值率就将最后的计算得出的价格提供给用户进行参考。

个人负责模块四:新人特惠

1、用户在注册完成之后,我们会默认发放一张2000元的优惠券,然后会给优惠券一个30天的过期时间,在我们实际的项目中,当有大量用户的未使用优惠券要过期时,肯定不能人工去操作,这时候就需要一个任务调度框架,帮我们自动去执行这些程序。我们使用的就是Quartz框架。

2、Quartz是OpenSymphony开源组织在Job scheduling领域一个开源项目, 是完全由java代码开发的一个开源的任务日程管理系统,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。Quartzh提供了极为广泛的特性如持久化任务,集群和分布式任务等。

3、当用户注册完成之后,后台就会向数据库插入一条注册时间数据,通过每天查询25天之前注册的用户,去判断用户的优惠券是否快要过期,一旦优惠券过期,我们就通过Quartz中的Trigger(触发器)去通知Scheduler(调度程序)调用JavaMail中的方法给用户邮箱发送一个过期提醒。

个人负责模块五:限时特惠

1、我们这个限时特惠就是让用户在规定的时间去抢购他要买的车辆,首先我们后台人员将需要抢购的一些车辆添加到限时特惠车辆表,然后在前台的限时特惠区进行展示,这里我们通过定时任务将抢购车辆放入到redis缓存中,每周一早上10点给我们的抢购车辆进行更新,不管有没有被抢购完,都会清除redis中的相关数据。

2、其实对于抢购来说,最重要的也就是抗并发和防止超卖的情况,为了防止这两种情况的发生,我是借助redis的decr来做的,首先它是可以保证原子性的, 我们提前将需要抢购的车辆库存数量放到redis中,通过车辆的id作为key, 当用户点击抢购的时候发送请求到服务端, 根据传递过来的车辆id去redis中查询库存和抢购的开始时间,如果当前时间小于抢购开始时间,直接返回抢购未开始提示,这样可以防止用户拿到接口直接调用的问题。

3、然后判断库存,如果库存小于等于0直接给用户返回抢购失败,这样的话后面的大量请求不会给系统带来压力。

4、然后如果这些条件都满足的话,执行decr操作,decr操作的话会将库存减一并且返回当前的值,然后再次判断当前值是否小于0,如果小于0,则返回抢购失败。否则mq异步的方式生成订单并且返回给前端一个成功标记。

5、为什么用mq? )

这里用到了mq的异步处理。说到rocketmq那么他在这里的作用就是给用户带来更好的体验度,因为后台的代码逻辑是非常复杂的,如果要执行的话需要很长时间,前台如果不做处理的话,那么用户将会等待很长的时间,这对于限时特惠这样的业务是不符合要求的,所以我们会使用rokcetmq直接给用户返回一个成功的消息,如果发送不成功,那么就返回失败,这里如果mq发送失败的也会加入死信队列来处理,这样也利用的mq的事务消息的原子性,保证了事务执行的成功。

6、还有一个问题就是如果有大批用户同时点击抢购,可能就会产生高并发。因为分布式环境下不同的线程需要对共享资源进行同步,那么使用Java中的锁机制就无法实现了,所以要借助分布式锁来解决共享资源同步的问题,我们使用的是Redis这种解决方案。

7、使用redis实现分布式锁)

1.1、在使用redis时, 需要用到几个核心的方法,:

setIfAbsent(key,value): 该方法会判断缓存中是否存在该key,如果有返回false, 如果没有,设置value值,并返回true。

getAndSet(key,value): 该方法每次只允许一个线程操作,它会获取key的上一个value值,并将当前value值放入缓存。

1.2、在具体的实现中, 首先需要设置一个唯一的标识作为key,还需要将当前时间和有效时间相加得出锁的过期时间作为value,当多个线程调用该方法时,会先通过setIfAbsent(key,value)方法判断缓存中是否存在该key, 如果没有, 说明目前还没有线程拿到锁, 此时, 可以将key和value存入缓存, 返回一个true, 表示该线程拿到锁,当线程执行完相关逻辑之后, 调用解锁方法, 将该线程的缓存删除。

1.3、那么, 这个时候, 有可能出于某种原因, 会使线程长时间还没有解锁, 那为了防止锁超时, 也就是防止死锁的产生,新进的线程第一次没有拿到锁,但可以获取上一个锁的过期时间也就是value和当前系统时间比较, 如果当前时间大于上一个锁的过期时间, 那么就说明上一个锁运行超时了,此时, 可以通过getAndSet(key,value)方法获取上一个锁的时间,并且将当前时间和过期时间相加得出过期时间作为value放入缓存。

1.4、为了防止多个线程同时获取到锁,就对比当前线程获取的时间和上一次锁的到期时间是否相同, 如果相同说明该线程是最先执行的, 那只有该线程拿到锁, 返回一个true, 其他线程返回false。

1.5、那么, 通过这样一个设计就基本上可以解决分布式锁的问题,在实际项目开发当中,就比如在抢购、广告投放等业务中就可以利用这种锁的应用,这一个设计其实也是一个非公平锁。

个人负责模块六:订单服务

车辆达到用户的预期,进行平台的支付定金,为了减库存和订单生成的最终一致性,也就是分布式事务的最终一致性,我采用的是rocketmq事务消息;因为rocketmq中的broker和producer有双信通信能力,使得我们的broker天生可以作为一个协调者的存在,从而确保了本地事务执行与消息发送的原子性问题,并且rocketmq本身提供存储机制,使得我们的事务消息有一个是持久化的能力;rocketmq的这种高可用机制以及可靠消息设计,则为我们的事务消息发生异常时,依然能保证事务最终一致性的达成。

事务消息的流程:

  1、我们的事务发送方首先发送一个半主题给我们的mq(消费者不可见),发送方发送消息成功后,然后执行本地事务;根据本地事务执行的结果返回conmmit或者rollback。

   2、如果是commit消息,mq将事务消息从半主题中提出并生成索引存入业务topic(对消费者可见),如果是rockback,不生成索引(对消费者不可见);

如果执行本地事务中发生异常(超时或挂掉),mq会不停的回查;收到回查消息后,根据本地事务的状态,重新返回commit或rollback。

3、这个流程保证了发送方与本地事务消息的原子性,而我们的消费者消费确是利用我们的rocketmq本身自带的ack消息重试机制来确保消息消费成功,只有消费方明确返回消费成功,rocket明确才认为消息消费成功,否则就会发起重试.重试最多次数是16次。我们通常设置为三次,就手动返回一个success 然后存到我们的死信队列表里,后续让我们的运维进行人为修改.这边消息重试也会遇到一个小问题就是消息消费成功,这边又发送了重试,我这边用的是redis 日志记录来解决的,记录已经消费成功的messageid,如果传来的messageid已经在我们的日志表中,那我们就放过不处理,以上就是车辆减库存和订单生成最终一致性的解决方案。

 

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
监控 Java API
如何使用Java语言快速开发一套智慧工地系统
使用Java开发智慧工地系统,采用Spring Cloud微服务架构和前后端分离设计,结合MySQL、MongoDB数据库及RESTful API,集成人脸识别、视频监控、设备与环境监测等功能模块,运用Spark/Flink处理大数据,ECharts/AntV G2实现数据可视化,确保系统安全与性能,采用敏捷开发模式,提供详尽文档与用户培训,支持云部署与容器化管理,快速构建高效、灵活的智慧工地解决方案。
|
17天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
89 26
|
29天前
|
XML Java 测试技术
从零开始学 Maven:简化 Java 项目的构建与管理
Maven 是一个由 Apache 软件基金会开发的项目管理和构建自动化工具。它主要用在 Java 项目中,但也可以用于其他类型的项目。
46 1
从零开始学 Maven:简化 Java 项目的构建与管理
|
26天前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
27天前
|
Java
Java项目中高精度数值计算:为何BigDecimal优于Double
在Java项目开发中,涉及金额计算、面积计算等高精度数值操作时,应选择 `BigDecimal` 而非 `Double`。`BigDecimal` 提供任意精度的小数运算、多种舍入模式和良好的可读性,确保计算结果的准确性和可靠性。例如,在金额计算中,`BigDecimal` 可以精确到小数点后两位,而 `Double` 可能因精度问题导致结果不准确。
|
1月前
|
Java Android开发
Eclipse 创建 Java 项目
Eclipse 创建 Java 项目
44 4
|
1月前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
45 3
|
1月前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
98 3
|
1月前
|
前端开发 Java 数据库
如何实现一个项目,小白做项目-java
本教程涵盖了从数据库到AJAX的多个知识点,并详细介绍了项目实现过程,包括静态页面分析、数据库创建、项目结构搭建、JSP转换及各层代码编写。最后,通过通用分页和优化Servlet来提升代码质量。
56 1
|
1月前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
41 1