01乘客最先看到什么——在决定之前先看到价格吗?
乘客输入上车点与目的地,在叫车前先得到一个车费估算(价格 + ETA)。
免费完整指南
设计Uber (Ride Hailing)。请覆盖Geospatial indexing trade-off: geohash vs quadtree vs S2/H3 cells for storing and querying driver locations, High-frequency driver location ingestion: update interval vs GPS...
面试节奏保持精简,让页面能把注意力花在真正的设计决策上。
不要只是陈述需求,要主动问出来。每张卡把设计约束和一句你可以在画架构前说出口的澄清问句配成一组。
01乘客最先看到什么——在决定之前先看到价格吗?
乘客输入上车点与目的地,在叫车前先得到一个车费估算(价格 + ETA)。
02他们按下叫车之后会怎样——匹配必须多快回来?
乘客以估算的车费叫车,并在大约一分钟内被匹配到附近一位可载客的司机——或得到一个明确的失败。
03司机端要做什么?
司机上线、发送位置 ping、一次收到一笔叫车邀约,接受或拒绝;接受后就导航前往上车点与目的地。
04乘客可以在匹配后取消吗,司机会经历什么?
可以:取消后司机立即恢复可载客、能接新行程,系统会记录是谁在何时取消。
05乘客会实时看着司机接近吗?
会:司机位置只在行程进行中流式传给被匹配的乘客——其他所有人只看到粗略的可用状态,永远拿不到可追踪的轨迹。
06在匹配时间窗内没有司机接受——那该怎么办?
请求会明确失败并附上重试指引——在这一分钟内给一个确定的「不行」,胜过一个永远转不完的圈圈。
范围外评分(乘客与司机) · 预约行程与车型分级(X/XL/Comfort) · 动态加价机制与付款结算
01一位司机有可能同时拿到两趟行程吗?
匹配是强一致的:一位司机同一时间最多持有一笔有效邀约或行程——永远不会重复派遣。
02司机位置需要多新?
司机在线时每隔几秒 ping 一次;邻近搜索看到的数据可能是几秒前的,但绝不会是几分钟前的。
03我们该按什么样的司机位置更新峰值速率来设计?
在一个刻意设定的压力上限:全球 1,000 万名司机在线、每约 5 秒 ping 一次,系统要吸收每秒约 200 万次位置写入。
04当一座体育场散场时会发生什么?
来自单一区域约 10 万笔叫车请求的突发会排队并优雅地消化——邻近区域与其他城市不受影响。
05每一步要感觉多快?
车费估算在数秒内;匹配(或一个明确的失败)端到端在大约一分钟内。
真实面试探得比一份整齐清单深得多。这些范围问句,区分出真正拷问问题的人和只是背诵的人。
把每个估算都当成一种压力,用来合理化一个组件:缓存、队列、分片、副本、worker pool 或退路。
位置写入洪流
压力上限:全球 1,000 万名司机在线 × 每 5 秒 1 次 ping10,000,000 ÷ 5 ≈ 2M writes/s
没有关系型数据库吸收得了这个——位置进入一个内存内的地理索引(geohash/H3 桶)并以 TTL 逐出。
邻近查询成本
上车点落进一个 geohash cell;覆盖搜索半径意味着那个 cell 加上它的 8 个相邻 cell——一个 3 × 3 网格,9 个 cell9 次 cell 读取 × 每次内存查询远低于 1 ms ≈ 个位数毫秒
geohash 把「谁在我附近」变成少数几次 key 查询,而不是一次全表扫描。
演唱会突发
约 10 分钟内来自单一街区约 10 万笔请求100,000 ÷ 600 秒 ≈ 单一区域每秒 170 次匹配
按地理区域对匹配队列分片——尖峰只让一个区域的消费者饱和,而不是整座城市。
邀约预算
60 秒内匹配;每位被邀约的司机约有 10 秒回应60 秒 ÷ 10 秒 ≈ 在期限前试 5-6 位司机
10 秒的邀约锁限制了预算内能塞进多少候选人——把候选人排好序,别乱撒邀约。
幽灵司机清理
位置条目在最后一次 ping 之后 15-30 秒过期——按 5 秒的节奏算,就是漏掉 3-6 次 ping15-30 s ÷ 每次 ping 5 s = 3-6 个沉默间隔 → 条目过期;宕机的司机最坏约 30 秒内就不再收到邀约
宕机或离线的司机会自己从索引中消失——不需清理作业,也不会把邀约发给幽灵。
决策示例
两种非常不同的负载共用这套系统:每秒约 200 万次位置写入从司机端流式涌入,而每一次叫车匹配只需要在一分钟内找到少数几位附近的候选人。
我会把司机位置放在一个带短 TTL 的内存内地理索引(geohash/H3 桶)里,这样停止 ping 的司机就直接消失。匹配捞出附近的候选人,并一次把行程邀约给一位司机,把那位司机锁住约 10 秒——接受就赢得行程,超时或拒绝就释放锁,下一位候选人拿到邀约。尖峰期间的叫车请求在一个按区域分片的队列里等候,所以一座体育场散场只会拖慢那个街区,而不是整座城市。
我不会做的事:把每一次位置 ping 都写进主数据库,再扫描它来找附近的司机——每秒 200 万次写入再加上扫描会把它压垮。我也不会在没有锁的情况下把一趟行程同时邀约给多位司机:两个接受同时抵达,两位司机都以为自己接到了活。而且我不会在第一天就对匹配系统分片——先测量每个区域的速率;过早分片只会增加故障模式,却没有增加你真正需要的容量。
如果匹配质量比速度更重要——拼车、批处理——我会把请求收集几秒钟,再以小批次一起匹配,而不是一次一笔。
先看一张完整的图,再把每条路径各自画成一张图 —— 写入路径与读取路径承载不同流量,合理化不同的组件。
完整全貌
司机 ping 直接流进内存内的地理索引;匹配从中读取附近的候选人,并一次一位司机地邀约行程。行程的真相活在数据库里——索引是可丢弃的。
路径 1
先车费,再叫车。匹配捞出少数几位附近的候选人,一次邀约一位司机;这个 10 秒的锁防止重复派遣,并在司机忽略邀约时自动往下走。
路径 2
每秒约 200 万次 ping 落在内存里,分桶进 geohash/H3 cell;条目在 15-30 秒后过期,所以停止 ping 的司机会自己消失。
在优化之前,先让契约可被检视:端点、实体、所有权、重试与状态。
POST/fare
请求{ pickup, destination }
响应200 { fare_id, estimated_fare, eta }
调用地图供应商取得路线 + ETA;估算会被存起来,让叫车请求可以引用它。
POST/rides
请求{ fare_id }
响应201 { ride_id, state: requested } · 404 fare 已过期
启动匹配;乘客会被推送匹配结果(或自行轮询)。
POST/drivers/location
请求{ lat, lng }
响应200
司机身份来自 session token,绝不来自请求主体。高频写入直接进入地理索引。
PATCH/rides/{ride_id}
请求{ accept | decline }
响应200 ride · 409 邀约已过期
接受会原子性地翻转邀约;拒绝或 10 秒超时会释放司机,下一位候选人拿到邀约。
核心实体
Riderrider_id (PK) · payment_profile
Driverdriver_id (PK) · vehicle · status: offline/available/offered/on_trip
status 字段就是防重复派遣的守门员:只有 available 的司机能收到邀约,而这个翻转是原子的。
Farefare_id (PK) · pickup · destination · estimated_fare · eta
在估算时创建;叫车请求引用 fare_id,让报出的价格不会被悄悄更动。
Rideride_id (PK) · rider_id · driver_id · fare_id · state: requested/matched/in_progress/completed
DriverLocationdriver_id · lat/lng · updated_at
活在内存内的地理索引(带 TTL 的 geohash/H3 cell)里,不在关系型存储中——它是可丢弃的数据。
在面试最后三分之一挑一条路线。每条路线给你主题、它该回答的面试官问题,以及要避免的失败模式。
1,000 万名司机每 5 秒 ping 一次。每秒 200 万次写入去哪里,你又如何回答「谁在这个上车点附近」?
位置 ping 进的是一个内存里的地理索引,绝不是关系型数据库。它扛不住每秒 200 万次写入。把每个位置归到一个 geohash 或 H3 格子里,配一个短 TTL。要找谁在上车点附近,就读那个格子加它的 8 个邻居——九次内存查找,个位数毫秒,而不是全表扫描。
把 ping 写进主数据库再扫描邻近——在这个速率下它会挂掉。
两笔叫车请求在同一瞬间都要同一位附近的司机。如何让刚好一个赢?
用一个短的独占锁把司机抢下来。这是一个原子的 compare-and-set,所以恰好一个请求赢,输的那个转向它的下一个候选。真正的行程状态由数据库保管:accept 把司机翻成 on-ride,取消则把他放回 available,不留下过期的占用。
没有锁就同时邀约多位司机——两个接受都以为自己赢了。
被邀约的司机忽略了请求。第 10 秒时发生什么,行程又如何仍在一分钟内匹配成功?
派单是一个 10 秒的锁,不是阻塞式的等待。到第 10 秒它自己过期,匹配转向下一个排名的司机。每个约 10 秒,60 秒的预算里塞得下 5 到 6 个司机。这就是为什么你要仔细给候选排序,而不是把派单到处乱撒。
让一位没回应的司机卡住整趟行程,而不是用一个会往下走的锁 TTL。
几分钟内 10 万人从单一街区叫车。你如何让城市的其他部分不受影响?
把匹配队列按地理区域分区。10 分钟 10 万个请求,也就每秒约 170 次匹配。它只会打满那一个区域的 consumer,其他每个区域照常消化。热点区域看到的是老老实实的排队——等久一点,或者一个明确的失败。城市其余部分照跑不误。
单一全局匹配队列——一个局部尖峰变成全市性的故障。
一个司机 App 在标记为 available 时宕机。他们还能收到邀约多久,又是什么把他们清掉?
每条位置记录在最后一次 ping 之后 15 到 30 秒过期。按 5 秒一次的节奏,也就是漏掉 3 到 6 次 ping,所以一个崩溃的司机会自己从索引里掉出去。不需要清理任务。一个“幽灵”最多也就还能收派单约 30 秒。
位置条目没有 TTL——邀约一直发给一小时前就离开的司机。
把 Uber (Ride Hailing) 大声讲一遍,让 AI 为你的说明评分。