【Java】如何设计一个支持5 亿用户规模的网约车系统?

2024-06-03 19:04

本文主要是介绍【Java】如何设计一个支持5 亿用户规模的网约车系统?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、问题解析

网约车的官方定义是:“以互联网技术为依托,构建服务平台,整合供需信息,使用符合条件的车辆和驾驶员,提供非巡游的预约出租汽车服务的经营活动。”通俗地说就是:利用互联网技术平台,将乘客的乘车信息发送给合适的司机,由司机完成接送乘客的服务。网约车包含专车、快车、拼车等多种形式。

中国目前网约车用户规模约5亿,我们准备开发一个可支撑目前全部中国用户使用的网约车平台,应用名称为“Udi”。

16.1 需求分析

Udi是一个网约车平台,核心功能是将乘客的叫车订单发送给附近的网约车司机,司机接单后,到上车点接乘客并送往目的地,到达后,乘客支付订单。根据平台的分成比例,司机提取一部分金额作为收益,用例图如下:

Udi平台预计注册乘客5亿,日活用户5千万,平均每个乘客1.2个订单,日订单量6千万。平均客单价30元,平台每日总营收18亿元。平台和司机按3:7的比例进行分成,那么平台每天可赚5.4亿元。

另外,平台预计注册司机5千万,日活司机2千万。

16.2 概要设计

网约车平台是共享经济的一种,目的就是要将乘客和司机撮合起来,所以需要开发两个App应用,一个是给乘客的,用来叫车;一个是给司机的,用来接单。Udi整体架构如下图:

相应的,Udi的系统也可以分成两个部分,一个部分是面向乘客的。乘客通过手机App注册成为用户,然后就可以在手机上选择出发地和目的地,进行叫车了。乘客叫车的HTTP请求首先通过一个负载均衡服务器集群,到达网关集群,再由网关集群调用相关的微服务,完成请求处理,如下图:

网关处理叫车请求的过程是:网关首先调用订单微服务,为用户的叫车请求创建一个订单,订单微服务将订单记录到数据库中,并将订单状态设置为“创建”。然后网关调用叫车微服务,叫车微服务将用户信息、出发地、目的地等数据封装成一个消息,发送到消息队列,等待系统为订单分配司机。

Udi系统的另一部分是面向司机的,司机需要不停将自己的位置信息发送给平台,同时,还需要随时接收来自平台的指令。因此,不同于用户通过HTTP发送请求给平台,司机App需要通过TCP长连接和平台服务器保持通信,如下图:

Udi司机App每3秒向平台发送一次当前的位置信息,包括当前车辆经纬度,车头朝向等。位置信息通过TCP连接到达平台的TCP连接服务器集群,TCP连接服务器集群的作用类似网关,只不过是以TCP长连接的方式向App端提供接入服务。TCP连接服务器将司机的位置信息更新到地理位置服务。

对于前面已经写入到消息队列的乘客叫车订单信息,分单子系统作为消息消费者,从消息队列中获取并处理。分单子系统首先将数据库中的订单状态修改为“派单中”,然后调用派单引擎进行派单。派单引擎根据用户的上车出发地点,以及司机上传的地理位置信息进行匹配,选择最合适的司机进行派单。派单消息通过一个专门的消息推送服务进行发送,消息推送服务利用TCP长连接服务器,将消息发送给匹配到的司机,同时分单子系统更新数据库订单状态为“已派单”。

16.3 详细设计

关于Udi的详细设计,我们将关注网约车平台一些独有的技术特点:长连接管理、派单算法、距离计算。此外,因为订单状态模型是所有交易类应用都非常重要的一个模型,所以我们也会在这里讨论Udi的订单状态模型。

16.3.1 长连接管理

因为司机App需要不断向Udi系统发送当前位置信息,以及实时接收Udi推送的派单请求,所以司机App需要和Udi系统保持长连接。因此,我们选择让司机App和Udi系统直接通过TCP协议进行长连接。

TCP连接和HTTP连接不同。HTTP是无状态的,每次HTTP请求都可以通过负载均衡服务器,被分发到不同的网关服务器进行处理,正如乘客App和服务器的连接那样。也就是说,HTTP在发起请求的时候,无需知道自己要连接的服务器是哪一台。

而TCP是长连接,一旦建立了连接,连接通道就需要长期保持,不管是司机App发送位置信息给服务器,还是服务器推送派单信息给司机App,都需要使用这个特定的连接通道。也就是说,司机App和服务器的连接是特定的,司机App需要知道自己连接的服务器是哪一台,而Udi给司机App推送消息的时候,也需要知道要通过哪一台服务器才能完成推送。

所以,司机端的TCP长连接需要进行专门管理,处理司机App和服务器的连接信息,具体架构如下图。

处理长连接的核心是TCP管理服务器集群。司机App会在启动时通过负载均衡服务器,与TCP管理服务器集群通信,请求分配一个TCP长连接服务器。

TCP管理服务器检查ZooKeeper服务器,获取当前可以服务的TCP连接服务器列表,然后从这些服务器中选择一个,返回其IP地址和通信端口给司机App。这样,司机App就可以直接和这台TCP连接服务器建立长连接,并发送位置信息了。

TCP连接服务器启动的时候,会和ZooKeeper集群通信,报告自己的状态,便于TCP管理服务器为其分配连接。司机App和TCP连接服务器建立长连接后,TCP连接服务器需要向Redis集群记录这个长连接关系,记录的键值对是<司机ID, 服务器名>。

当Udi系统收到用户订单,派单引擎选择了合适的司机进行派单时,系统就可以通过消息推送服务给该司机发送派单消息。消息推送服务器通过Redis获取该司机App长连接对应的TCP服务器,然后消息推送服务器就可以通过该TCP服务器的长连接,将派单消息推送给司机App了。

长连接管理的主要时序图如下:

如果TCP服务器宕机,那么司机App和它的长连接也就丢失了。司机App需要重新通过HTTP来请求TCP管理服务器为它分配新的TCP服务器。TCP管理服务器收到请求后,一方面返回新的TCP服务器的IP地址和通信端口,一方面需要从Redis中删除原有的<司机ID, 服务器名>键值对,保证消息推送服务不会使用一个错误的连接线路推送消息。

16.3.2 距离计算

乘客发起一个叫车请求时,Udi需要为其寻找合适的司机并进行派单,所谓合适的司机,最主要的因素就是距离。在[第9讲]的交友系统设计中,我们已经讨论过GeoHash算法,Udi就是直接使用Redis的GeoHash进行邻近计算。司机的位置信息实时更新到Redis中,并直接调用Redis的GeoHash命令georadius计算乘客的邻近司机。

但是Redis使用跳表存储GeoHash,Udi日活司机两千万,每3秒更新一次位置信息,平均每秒就需要对跳表做将近7百万次的更新,如此高并发地在一个跳表上更新,是系统不能承受的。所以,我们需要将司机以及跳表的粒度拆得更小。

Udi以城市作为地理位置的基本单位,也就是说,每个城市在Redis中建立一个GeoHash的key,这样,一个城市范围内的司机存储在一个跳表中。对于北京这样的超级城市,还可以更进一步,以城区作为key,进一步降低跳表的大小和单个跳表上的并发量。

16.3.3 派单算法

前面说过,派单就是寻找合适的司机,而合适的主要因素就是距离,所以最简单的派单算法就是直接通过Redis获取距离乘客上车点最近的空闲网约车即可。

但是这种算法效果非常差,因为Redis计算的是两个点之间的空间距离,但是司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。

因此,我们必须用行驶距离代替空间距离,即Udi必须要依赖一个地理系统,对司机当前位置和上车点进行路径规划,计算司机到达上车点的距离和时间。事实上,我们主要关注的是时间,也就是说,派单算法需要从Redis中获取多个邻近用户上车点的空闲司机,然后通过地理系统来计算每个司机到达乘客上车点的时间,最后将订单分配给花费时间最少的司机。

如果附近只有一个乘客,那么为其分配到达时间最快的司机就可以了。但如果附近有多个乘客,那么就需要考虑所有人的等待时间了。比如附近有乘客1和乘客2,以及司机X和司机Y。司机X接乘客1的时间是2分钟,接乘客2的时间是3分钟;司机Y接乘客1的时间是3分钟,接乘客2的时间是5分钟。

如果按照单个乘客最短时间选择,给乘客1分配司机X,那么乘客2只能分配司机Y了,乘客总的等待时间就是7分钟。如果给乘客1分配司机Y,乘客2分配司机X,乘客总等待时间就是6分钟。司机的时间就是平台的金钱,显然,后者这样的派单更节约所有司机的整体时间,也能为公司带来更多营收,同时也为整体用户带来更好的体验。

这样,我们就不能一个订单一个订单地分别分配司机,我们需要将一批订单聚合在一起,统一进行派单,如下图:

分单子系统收到用户的叫车订单后,不是直接发送给派单引擎进行派单,而是发给一个订单聚合池,订单聚合池里有一些订单聚合桶。订单写完一个聚合桶,就把这个聚合桶内的全部订单推送给派单引擎,由派单引擎根据整体时间最小化原则进行派单。

这里的“写完一个聚合桶”,有两种实现方式,一种是间隔一段时间算写完一个桶,一种是达到一定数量算写完一个桶。最后Udi选择间隔3秒写一个桶。

这里需要关注的是,派单的时候需要依赖地理系统进行路径规划。事实上,乘客到达时间和金额预估、行驶过程导航、订单结算与投诉处理,都需要依赖地理系统。Udi初期会使用第三方地理系统进行路径规划,但是将来必须要建设自己的地理系统。

16.3.4 订单状态模型

对于交易型系统而言,订单是其最核心的数据,主要业务逻辑也是围绕订单展开。在订单的生命周期里,订单状态会多次变化,每次变化都是由于核心的业务状态发生了改变,也因此在前面设计的多个地方都提到订单状态。但是这种散乱的订单状态变化无法统一描述订单的完整生命周期,因此我们设计了订单状态模型,如下图:

用户叫车后,系统即为其创建一个订单,订单进入“创单”状态。然后该订单通过消息队列进入分单子系统,分单子系统调用派单引擎为其派单,订单状态进入“派单中”。派单引擎分配到司机,一方面发送消息给司机,一方面修改订单状态为“已派单”。

如果司机去接到乘客,订单状态就改为“行程中”;如果司机拒绝接单,就需要为乘客重新派单,订单重新进入消息队列,同时订单状态也改回为“派单中”;如果司机到达上车点,但是联系不到乘客,没有接到乘客,那么订单就会标记为“已取消”。如果在派单中,乘客自己选择取消叫车,订单也进入“已取消”状态。“已取消”是订单的一种最终状态,订单无法再转变为其他状态。

司机到达目的地后,通过App确认送达,订单进入“待支付”状态,等待用户支付订单金额。用户支付后,完成订单生命周期,订单状态为“已完成”。

订单状态模型可以帮助我们总览核心业务流程,在设计阶段,可以通过状态图发现业务流程不完备的地方,在开发阶段,可以帮助开发者确认流程实现是否有遗漏。

二、粉丝福利

最近很多同学问我有没有java学习资料,我根据我从小白到架构师多年的学习经验整理出来了一份80W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴 可以关注我
公众号:“ 灰灰聊架构 ”, 回复暗号:“ 159 ”即可获取

这篇关于【Java】如何设计一个支持5 亿用户规模的网约车系统?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1027840

相关文章

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、