网约车系统设计
网约车案例需要支持哪些角色与操作?
- 乘客: 查看附近车辆、预计到达时间 ETA 与价格,选择目的地叫车并查看司机实时位置;
- 司机: 接受或拒绝订单,查看接客地点,开始行程并在到达后结束;
- 质量目标: 高可靠、高可用、低延迟并可扩展,派单正确性比地图位置绝对同步更重要;
- 扩展需求: 行程评价、支付及指标分析;
- 核心约束: 一个司机同一时刻不能接下冲突行程,一个订单也不能被多个司机成功占有;
请求与定位上报的规模如何分别计算?
- 题设: 1 亿 DAU、100 万司机、每天 1000 万单,每个用户每天执行 10 次业务动作;每次记录按 400 B;
| 项目 | 计算 | 结果 |
|---|---|---|
| 业务请求 | 1 亿 × 10 | 10 亿/日 |
| 平均业务 RPS | 10 亿 / 86400 | 11574/s,约 12K/s |
| 每日逻辑记录 | 10 亿 × 400 B | 400 GB |
| 十年逻辑记录 | 400 GB × 3650 | 1.46 PB |
| 平均业务入口 | 400 GB / 86400 | 4.63 MB/s,约 5 MB/s |
| 平均行程创建 | 1000 万 / 86400 | 115.74/s |
- 假设边界: 请求不一定都永久保存,若只存行程而非完整动作事件,存储应按对应实体重算;
- 定位补充: 若 100 万在线司机每 5 秒上报一次,就是 20 万次/s,已远大于业务请求基线;
- 位置带宽: 位置载荷、协议开销、乘客推送及轨迹保留需要单列预算,原文的 5 MB/s 无法覆盖全部系统;
- 地区峰值: 请求集中在城市、上下班和活动区域,全球平均值不能代表单个派单分区的容量;
行程和支付数据如何关联?
| 实体 | 关键字段 | 主要约束 |
|---|---|---|
| customers | id、name、email、createdAt | 乘客身份 |
| drivers | id、name、email、dob、location、cabID | 司机资料与位置引用 |
| cabs | id、driverID、type、regNo | 车辆及司机关联 |
| trips | id、customerID、driverID、status、source、destination、createdAt | 行程状态与双方归属 |
| ratings | customerID、driverID、tripID、rating、feedback | 指定行程的评价 |
| payments | id、tripID、method、amount、status | 行程支付结果 |
- 关系修正: 原图同时保存 trip.paymentID 与 payment.tripID,实施时应明确唯一权威关联或事务约束,避免两侧指向不同记录;
- 金额补充: 原图使用 float,实际支付可采用最小货币单位整数或精确十进制,并保存币种;
- 状态分离: 当前位置高频变化,可保存在有过期规则的索引;行程与支付状态需要可靠持久化;
- 存储选择: 行程约束适合事务模型,空间查询可使用专用索引,不能要求一个库同时承担全部高频职责;
接口如何防止操作到错误订单?
requestRide(customerID, source, destination, cabType, paymentMethod, requestID) → ride
cancelRide(customerID, rideID, reason?) → result
acceptRide(driverID, rideID) → result
denyRide(driverID, rideID) → result
startTrip(driverID, tripID) → result
endTrip(driverID, tripID) → result
rateTrip(customerID, tripID, rating, feedback?) → result
- 原文修正: 取消接口只有 customerID 不足以唯一表达目标,这里补充 rideID;
- 坐标: source 与 destination 为明确顺序的经纬度对,需要约定坐标系统、单位和合法范围;
- 身份: customerID 与 driverID 必须对应认证主体,行程操作还需验证关联关系;
- 幂等: 叫车、取消、接单和结束行程都可能因网络重试而重复,按请求或状态版本保证只产生一次有效转换;
- 请求与行程: 原文混用 ride/trip,设计时区分“等待匹配的叫车请求”和“已建立的行程”,并明确 ID 映射;
服务怎样分配实时与持久状态责任?
- 乘客服务: 管理乘客身份与资料;
- 司机服务: 管理司机、车辆与上报入口;
- 叫车服务: 接收请求、聚合附近司机并执行匹配,管理匹配中的状态;
- 行程服务: 管理已接单后的开始、进行、完成和取消规则;
- 支付服务: 连接第三方支付并维护支付状态;
- 通知与分析: 通过事件独立工作,分别负责用户触达与运营指标;
- 服务边界: HTTP、gRPC、服务发现或网格按通信需要选择,先明确数据所有者再决定部署粒度;
从叫车到完成的主流程是什么?
乘客提交起终点与车型 → 创建待匹配请求
→ 查询候选司机并估算 ETA → 邀约
→ 某司机原子接单 → 通知双方并持续显示司机位置
→ 接客后开始行程 → 到达后结束行程
→ 支付处理 → 允许评价
- 状态约束: 每一步只能由允许的前置状态进入,拒绝订单不应意外结束其他司机的行程;
- 不确定结果: 请求超时后应查询状态而非盲目创建第二个行程;
- 失败边界: 匹配超时、乘客取消、司机失联与支付失败各有不同处理,不应统一回滚整个现实行程;
司机位置如何持续更新到乘客端?
- 上报: 司机端按时间或移动距离上报 GPS 坐标与采样时间,后台运行还要考虑系统权限和省电限制;
- 连接选择: WebSocket 可双向交换位置与状态,普通周期 HTTP 上报也是可行选项,不能把主动上报本身称为长轮询;
- 下行: 只有有权查看该司机的乘客接收相应更新,不向所有用户广播完整轨迹;
- 新鲜度: 每个位置带时间戳并设置过期规则,长期失联的司机不能继续作为可接单候选;
- 高频优化: 可合并更新并降低静止时采样频率,精度与吞吐按 ETA 和用户体验要求权衡;
经纬度条件查询如何作为附近司机的基线?
SELECT driver_id, latitude, longitude
FROM driver_locations
WHERE latitude BETWEEN :min_lat AND :max_lat
AND longitude BETWEEN :min_lon AND :max_lon;
- 初筛: 包围盒找出可能在附近的司机,再用实际地理距离过滤,不把矩形结果当精确半径结果;
- 单位修正: 原文直接用
X±R混合经纬度与半径,应先把距离转成合法边界或使用空间查询能力; - 性能判断: 无合适索引的全表扫描可能昂贵,但不能因此断言 SQL 本身无法支持大规模空间查询;
Geohash 怎样用于司机候选查询?
- 路径: 给司机当前位置计算 Geohash,查询覆盖乘客周围区域的多个编码范围,再按坐标距离过滤;
- 索引位置: 热点位置数据可放内存索引,以减少每次候选查询的访问成本;
- 边界条件: 跨网格司机不能因前缀不同而被遗漏,原理及精度表见 空间索引;
- 使用判断: 实现简单且能复用字符串索引,但需要处理覆盖范围、误差和高频更新;
四叉树怎样组织高密度地区的司机?
- 路径: 按区域保存司机,密度超过阈值时继续细分,查询只遍历与搜索范围相交的区域;
- 位置更新: 司机跨区域时移出旧节点并加入新节点,查询应避免读到不一致的重复位置;
- 缓冲: 可以先记录最新位置并批量更新索引,以吞吐换取明确可控的新鲜度延迟;
- Hilbert 映射: 可辅助布局或范围访问,但四叉树本身不依赖该算法,机制见 四叉树;
- 选择边界: 原文偏好四叉树,但应结合更新频率、区域密度和现有空间数据库能力验证,不能仅凭概念合适认定最优;
多个司机同时接单时怎样避免重复匹配?
- 原子状态转换: 只有订单仍待匹配且司机仍可用时,才能同时写入订单归属与司机占用状态;
- 竞争胜者: 使用条件写、版本号或事务决定唯一成功者,失败者收到明确的已被接单结果;
- 原文修正: 单进程 Mutex 只保护一个进程内线程,多个服务实例竞争时需要共享权威状态的原子保证;
- 跨边界: 若订单和司机状态跨存储,需有明确协调、预占租约与补偿策略,相关机制见 分布式事务;
- 租约过期: 旧接单请求不能在租约已失效后覆盖新归属,应校验版本或隔离令牌;
如何从附近候选中选择合适司机?
- 候选过滤: 先排除位置过期、车型不符、不可接单或无权限服务该区域的司机;
- 排序: 综合到达时间、路网、评分和相关反馈,而不只比较直线距离;
- 邀约: 可分批优先通知高排名司机,设置响应期限,避免同时向大量司机发出互相冲突的承诺;
- 高需求: 动态加价可反映供需紧张,但价格必须在订单和支付中具有可解释、稳定的确认版本;
支付完成如何可靠地反映到行程状态?
- 第三方支付: 原文采用 Stripe 或 PayPal 等处理器,把支付渠道细节交给专门服务;
- 完成信号: 不以浏览器跳回成功页作为唯一证据,应验证服务端事件并在必要时主动查询支付结果;
- Webhook: 校验事件签名,依据事件或支付 ID 幂等更新,允许重复或乱序到达;
- 状态边界: 行程已完成而支付待处理是合理中间状态,不能因网络超时再次扣费或把真实行程抹去;
- 参考: Stripe Webhook 文档说明签名验证、重复事件与顺序处理要求;
地理分区与缓存怎样控制热点?
- 区域分区: 按城市或业务服务区域归属位置数据,减少大多数附近查询的跨区请求;
- 边界查询: 用户靠近分区边缘时要查询相邻区域,不能把邮编边界当作物理距离边界;
- 热点地区: 人群和车辆分布不均时需要细分热门区域,单纯一致性哈希不能同时保证地理邻近与均匀业务负载;
- 缓存: 保存最近位置并附采样时间和 TTL,LRU 只能管理容量,不能说明位置仍有效;
- 未命中: 可尝试读取可靠位置源,但过旧位置只能作为历史信息,不能直接用于实时派单;
行程通知怎样避免干扰派单?
- 通知: 接单、到达和支付事件进入独立通知流程,平台分发见 即时通信,通知失败不改变已提交的司机归属;
行程统计应该记录到哪里?
- 统计: 收集等待时长、取消、匹配成功率和行程事件,原文误用视频案例 views 表,这里应落入行程与业务事件模型;
节点故障时如何保持派单约束?
- 冗余: 服务、入口、缓存和持久存储部署适当副本,关键接单判断仍回到权威状态,不能从延迟副本决定可用司机;
- 降级: 排名或分析故障可采用简化候选策略,但不可牺牲重复接单约束;
- 验证: 覆盖两人抢一车、两车接一单、取消与接单同时发生、位置过期、跨区查询、重复支付事件和连接中断;