设计一个网约车派单系统
提出问题
网约车派单系统是典型的高并发、低延迟、实时地理位置的分布式系统。面试中这道题考查的不仅是系统设计能力,更是对地理空间索引、实时数据流、资源分配优化等综合工程能力的考察。生产环境中,滴滴高峰期每秒需要处理 3 万次以上派单请求(Uber 类似),每个请求必须在 200ms 内完成司机匹配,这对系统的可用性、一致性和扩展性提出了极高要求。面试官希望看到候选人对"附近司机怎么找""谁先接单""怎么防止一个司机被同时派给多单"等核心问题有清晰方案。
核心难点拆解
从工程实现角度,派单系统需要解决三个核心问题:
- 空间索引:给定乘客经纬度,怎么在几十毫秒内找到半径 3km 内的空闲司机?
- 状态一致性:怎么保证一个司机不会被同时派给两个乘客?
- 分配最优:当多个乘客和多个司机同时等待,怎么分配总等待时间最短?
这三个问题层层递进,面试时按这个顺序讲,比直接抛方案更有层次感。
地理位置索引:如何快速找到附近司机
网约车最核心的查询是"给定乘客位置,找出半径 3km 内的空闲司机"。如果所有司机都存放在一个二维平面坐标中,全表扫描显然不可行(10 万司机 × 每秒 3 万次查询,查不过来)。常用的解决方案有四种:
GeoHash
将经纬度编码为 12 位 base32 字符串,前缀匹配即可得到邻近区域。精度可控(6 位约 1.2km,7 位约 152m),支持 Redis GEO 的原生操作。优点是实现简单、查询快;缺点是边界区域需要额外处理(边缘区域可能遗漏相邻 GeoHash 格子内的司机)。
GeoHash 编码原理:地球纬度范围 [-90, 90],经度范围 [-180, 180]。GeoHash 把纬度区间二分,经度区间也二分,每次切分获得一个 bit,交替拼接后 base32 编码。例如,北京天安门 (39.9042, 116.3974) 的 6 位 GeoHash 是 wx4g0f。这个编码隐含了空间层级关系——前缀相同的前缀越长的格子,空间越近。
边界问题详解:
乘客在格子 A 的右边界,离他最近的司机在格子 B 的左边界(距离 50 米)
但格子 A 和 B 的 GeoHash 前缀完全不同,因为边界刚好落在编码的二进制切换点上
只查格子 A => 这 50 米内的司机全部漏掉解决方案:查中心格 + 周边 8 格(共 9 格),合并结果后再按球面距离精确过滤。
四叉树(Quadtree)
将平面递归四等分,每个叶子节点存储该区域内的司机信息。优点是精度高、分布不均匀时自适应分割;缺点是实现和维护成本高,需要自己构建树结构,并且更新频繁(司机每 3 秒上报一次位置)时树平衡成本高。滴滴内部早期用 GeoHash,后来切到了自研的四叉树方案,因为四叉树对热点区域(比如市中心)可以自动分裂到更细粒度,而非热点区域(郊区)保持粗粒度,内存利用率更高。
S2 几何库(Google S2)
将球面映射到立方体再投影为 64 位整数,表达能力比 GeoHash 更精细,支持任意距离的 Cell 覆盖。Uber 的 H3 也是一种变体,它用六边形覆盖球面,优点是相邻 Cell 距离完全相等,没有 GeoHash 的 8 格边界问题,Uber 内部就在用 H3。
四种方案对比:
| 方案 | 内存占用 | 查询延迟 | 边界问题 | 实现复杂度 | 生产代表 |
|---|---|---|---|---|---|
| GeoHash + Redis GEO | 低(ZSet 存储) | O(log N),约 1ms | 需要 8 格补偿 | 低 | 大多数中小型公司 |
| 四叉树 | 中(树结构) | O(log N) | 天然无 | 高 | 滴滴(自研) |
| S2 | 低(64位整数) | O(log N) | Cell 覆盖压缩 | 中 | Google Maps |
| H3 | 中(六边形) | O(log N) | 天然无,六边形等距 | 中 | Uber |
实际选型:大多数系统从 GeoHash 起步,配合周边 8 个格子消除边界问题。Redis GEO 底层就是 GeoHash + ZSet,查询 O(log N),非常适合"附近司机"这种高频低延迟场景。
// 使用 Jedis 实现 GeoHash 附近司机查询
import redis.clients.jedis.GeoRadiusParam;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
public List<DriverLocation> findNearbyDrivers(Jedis jedis, double lat, double lng, double radiusKm) {
// 1. 获取中心点 GeoHash 及周边 8 个格子
// 注意:只查中心格会漏掉边界上的司机
String centerHash = GeoHash.encode(lat, lng, 6); // 6 位精度约 1.2km
List<String> neighborHashes = GeoHash.neighbors(centerHash); // 8 邻域
// 2. 合并所有候选司机 ID(去重)
Set<String> candidateDriverIds = new HashSet<>();
for (String hash : neighborHashes) {
// Redis GEOADD 的 key 命名:geo:drivers:<hash>
// GEORADIUS 直接查坐标,不需要自己算
List<GeoCoordinate> drivers = jedis.georadiusByMember(
"geo:drivers:" + hash,
centerHash, // 以乘客位置为中心
radiusKm,
GeoUnit.KM,
GeoRadiusParam.geoRadiusParam().withCoord()
);
for (GeoCoordinate driver : drivers) {
candidateDriverIds.add(driver.getMember());
}
}
// 3. 球面距离精确过滤
// GeoHash 格子是矩形,对角线距离可能大于 radiusKm
// 比如 1.2km 精度的格子,对角线约 1.7km
// 如果 radiusKm = 1.0km,格子角上的司机距离乘客可能超过 1km
List<DriverLocation> result = new ArrayList<>();
for (String driverId : candidateDriverIds) {
double[] coord = getDriverLocation(driverId);
double dist = haversine(lat, lng, coord[0], coord[1]);
if (dist <= radiusKm) {
result.add(new DriverLocation(driverId, coord[0], coord[1], dist));
}
}
// 4. 按距离排序,返回 Top N
result.sort(Comparator.comparingDouble(DriverLocation::getDistance));
return result.subList(0, Math.min(10, result.size()));
}
// Haversine 公式计算球面距离
private double haversine(double lat1, double lng1, double lat2, double lng2) {
double R = 6371; // 地球半径,单位 km
double dLat = Math.toRadians(lat2 - lat1);
double dLng = Math.toRadians(lng2 - lng1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}生产经验:Haversine 公式计算速度大约 0.1ms/次,如果查 9 格 × 每格 50 个候选 = 450 次计算,大约 45ms。对于 200ms 延迟要求,这个时间可以接受。但如果候选司机数量大(比如高峰期市中心),可以先用粗精度(5 位,约 5km)过滤一遍,再精查。
实时位置上报与状态管理
数据写入链路
司机每秒或每几秒上传一次 GPS 坐标,系统需要处理海量位置更新。假设 10 万在线司机,每秒 3 次上报,即 30 万 QPS 的写入。如果每个请求直接写 Redis,Redis 单实例大约能扛 5-8 万 QPS,30 万 QPS 需要 Redis 集群,成本很高。
实际写入链路:
司机侧: [GPS 采集] --(3s/次)--> [WebSocket 长连接网关]
|
v
后端: [Kafka 位置主题 (30万 msg/s, 128 分区)]
|
v
[Flink 流处理 (3 节点, 每节点 8 并行度)]
├── 降噪:速度 > 200km/h 的异常点丢弃
│ (GPS 漂移:从北京突然跳到上海,直接丢弃)
├── 轨迹聚合:3s 原始点 → 30s 轨迹段
└── 地理围栏触发:进入/离开热点区域
|
v
[Redis 集群 (6 主 6 从)]
├── GEO 集合:附近司机查询
├── 司机状态 Hash:hash:driver:{id} → status, lat, lng, lastUpdate
└── 热力区域 ZSet:zset:heat:{geoHash} → 司机数、订单数Flink 降噪逻辑:GPS 漂移是实际生产中最常见的脏数据问题。典型场景:司机在隧道中 GPS 信号丢失,recover 后经纬度跳变几百米。如果不做降噪,司机会在 Redis GEO 中"瞬移",导致派单给实际不在该位置的司机。
// Flink ProcessFunction:GPS 降噪
public class GpsDenoiseFunction extends KeyedProcessFunction<String, GpsEvent, GpsEvent> {
private ValueState<GpsEvent> lastEventState;
@Override
public void processElement(GpsEvent event, Context ctx, Collector<GpsEvent> out) {
GpsEvent last = lastEventState.value();
if (last != null) {
double distance = haversine(last.getLat(), last.getLng(),
event.getLat(), event.getLng());
long timeDiff = event.getTimestamp() - last.getTimestamp(); // 秒
double speed = distance / (timeDiff / 3600.0); // km/h
// 如果速度超过 200km/h,丢弃(城市道路不可能)
if (speed > 200) {
// 记录异常日志,用于后期分析 GPS 模块质量
log.warn("GPS drift detected: driver={}, speed={}km/h, from=({},{}), to=({},{})",
event.getDriverId(), speed, last.getLat(), last.getLng(),
event.getLat(), event.getLng());
return; // 丢弃
}
}
lastEventState.update(event);
out.collect(event);
}
}状态机与防双派单
每个司机有 IDLE → ASSIGNED → ENROUTE → ARRIVED → IN_TRIP → COMPLETED → IDLE 状态流转。状态变换必须严格序列化,避免竞态——比如同一司机被两个乘客同时选中。
双派单是怎么发生的:
时间线:
T1: 乘客A发起订单,查找附近司机,发现司机D空闲
T2: 乘客B也发起订单,同时查到司机D空闲(因为司机D还没被标记为ASSIGNED)
T3: 两个派单请求同时执行 tryAssignDriver(D),都通过了 status == "IDLE" 检查
T4: 两个订单都派给了司机D,司机端收到两个订单推送解决方案对比:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Redis 分布式锁 | tryLock 司机级别 | 实现简单,理解成本低 | 锁超时难控制,节点宕机可能死锁 |
| Lua 脚本 | Redis 原子执行 | 不需要锁,天然原子 | 无法处理异步回调(如超时回滚) |
| 乐观锁版本号 | CAS 更新 | 无锁无阻塞 | 冲突时需要重试,重试逻辑复杂 |
| 数据库行锁 | SELECT ... FOR UPDATE | 强一致性 | 性能差,不适合高并发 |
推荐方案:Lua 脚本作为主路径,兜底方案用 Watch Dog 续期锁。
// 方案 A:Lua 脚本(推荐,主路径)
// Lua 脚本内容
String luaScript =
"local driverId = KEYS[1]\n" +
"local orderId = ARGV[1]\n" +
"local status = redis.call('GET', 'status:driver:' .. driverId)\n" +
"if status == 'IDLE' then\n" +
" redis.call('SET', 'status:driver:' .. driverId, 'ASSIGNED:' .. orderId)\n" +
" redis.call('EXPIRE', 'status:driver:' .. driverId, 30)\n" + // 30s 超时自动释放
" return 1\n" +
"end\n" +
"return 0";
// 业务代码
public boolean tryAssignDriverWithLua(String driverId, String orderId) {
// SHA 缓存,避免每次传输完整 Lua 脚本
String sha = jedis.scriptLoad(luaScript);
Object result = jedis.evalsha(sha,
Arrays.asList(driverId),
Arrays.asList(orderId));
return (Long) result == 1L;
}// 方案 B:Redisson 分布式锁(兜底,用于复杂场景)
public boolean tryAssignDriverWithLock(String driverId, String orderId) {
RLock lock = redisson.getLock("lock:driver:" + driverId);
try {
// tryLock 参数:waitTime=1s, leaseTime=3s
// 为什么 leaseTime 要设?怕死锁——如果拿到锁后节点挂了,3s 后自动释放
// 但 leaseTime 不能太小,否则业务没执行完锁就过期了
// 生产上建议用 Watch Dog 自动续期
if (!lock.tryLock(1, 3, TimeUnit.SECONDS)) {
log.warn("Acquire lock failed, driverId={}, orderId={}", driverId, orderId);
return false;
}
long version = redis.incr("ver:driver:" + driverId); // 乐观锁
String status = redis.get("status:driver:" + driverId);
if (!"IDLE".equals(status)) return false;
redis.set("status:driver:" + driverId, "ASSIGNED:" + orderId);
return true;
} finally {
lock.unlock();
}
}进阶问题:Lua 脚本的 EXPIRE 30s 超时后,司机状态自动回到 IDLE。但如果司机已经在前往接乘客的路上,状态回退会导致订单状态和司机状态不一致。解决方案:订单侧也有超时检查,如果超过 30s 司机没有点击"接单",订单自动取消,同时清除司机状态。这样两端状态不会产生永久不一致。
频率控制与带宽优化
司机 GPS 上报频率不能一刀切:
| 状态 | 频率 | 理由 |
|---|---|---|
| IDLE(空闲) | 3s/次 | 够用,降低带宽和 Redis 写入压力 |
| ASSIGNED(接单后去程) | 1s/次 | 需要精确追踪接驾路线 |
| IN_TRIP(行程中) | 5s/次 | 路线固定,不需要高频上报 |
| 陀螺仪检测到剧烈变化 | 临时 500ms/次 | 转弯、颠簸时位置变化快 |
生产数据:10 万司机,IDLE 占 70%(3s/次),ASSIGNED 占 10%(1s/次),IN_TRIP 占 20%(5s/次)。平均每秒上报量 = 70000/3 + 10000/1 + 20000/5 ≈ 23,333 + 10,000 + 4,000 = 37,333 QPS。相比一刀切 3s/次(33,333 QPS)只增加了 12%,但 ASSIGNED 状态的精度大幅提升。
派单算法:从简单到复杂
派单算法可以分层次演进,面试时按这个顺序讲,能体现你的工程思维:
第一层:就近派单(Nearest Driver)
选距离最近的空闲司机。实现简单,但全局最优性差——可能把 A 派给 3km 外的乘客,而 2km 外有另一个更优的乘客在等。但是,对于初期日单量 1000 单的系统,这个方案完全够用,而且实现成本最低。
时序流程:
乘客发起订单
↓
1. 获取乘客位置 (lat, lng)
↓
2. Redis GEO 查询附近 3km 内空闲司机
↓
3. 按直线距离排序,取 Top 5
↓
4. 并行查询 Top 5 的 ETA(路径规划 API)
↓
5. 选 ETA 最短的司机
↓
6. Lua 脚本原子派单
↓
7. 推送订单给司机端
↓
8. 司机 30s 内不接单 → 自动取消,回滚状态第二层:预估到达时间(ETA)
考虑路况、红绿灯、方向,用 ETA 替代直线距离。每次查询需要调用路径规划服务(高德 / 百度地图 API),单次查询延迟约 50-200ms。如果每个派单请求要查 10 个司机的 ETA,总延迟就变成 0.5-2s,远超 200ms 要求。
优化方案:预计算 + 缓存。只对"候选司机 Top 3 至 Top 5"实时查 ETA,其余的用缓存(缓存 TTL 设 30s,路况变化没那么快)。缓存 key 设计:eta:{fromLat}:{fromLng}:{toLat}:{toLng},但经纬度组合无限多,需要用 GeoHash 粗粒度聚合,比如 eta:{fromGeoHash5}:{toGeoHash5},这样每分钟同一对区域的 ETA 只需要查一次。
// ETA 缓存查询
public long getEta(double fromLat, double fromLng, double toLat, double toLng) {
// 用 5 位 GeoHash 做缓存 key(约 5km 精度)
String fromHash = GeoHash.encode(fromLat, fromLng, 5);
String toHash = GeoHash.encode(toLat, toLng, 5);
String cacheKey = "eta:" + fromHash + ":" + toHash;
String cached = redis.get(cacheKey);
if (cached != null) {
return Long.parseLong(cached); // 缓存命中,约 0.1ms
}
// 缓存未命中,调用路径规划 API
long eta = routePlanningService.getEta(fromLat, fromLng, toLat, toLng);
redis.setex(cacheKey, 30, String.valueOf(eta)); // TTL 30s
return eta;
}第三层:全局最优(Batch Matching)
日均百万单级别。Uber 论文中提到的方案——每隔几秒(如 3s)将当前待分配订单和空闲司机组成二分图,用 Hungarian 算法或 KM 算法求最小化总等待时间(或最大化总 GMV)的匹配。
二分图匹配数学模型:
设:
O = {o1, o2, ..., om} 为待分配订单集合
D = {d1, d2, ..., dn} 为空闲司机集合
C[i][j] = ETA(oi, dj) 为订单 oi 派给司机 dj 的预估到达时间
目标:最小化 Σ C[i][j] * x[i][j]
约束:
Σ x[i][j] <= 1, 每个司机最多接一单
Σ x[i][j] <= 1, 每个订单最多派给一个司机
x[i][j] ∈ {0, 1}生产实现注意:Hungarian 算法复杂度 O(n³),当 m=n=500 时,单次计算大约 10-50ms。但高峰期订单和司机数量可能达到 2000+,计算时间会飙升到秒级。优化方案:
- 空间聚类:按 GeoHash 将订单和司机分成多个区域,每个区域独立计算(区域间不跨区派单——除非附近区域没有空闲司机),这样每个区域的 m,n 通常不超过 100。
- 时间分片:高峰期 3s 窗口缩短到 1s,低谷期延长到 5s。窗口大小根据供需比动态调整。
- 贪心近似:当 m,n > 500 时,降级为贪心算法(最近司机优先),保证延迟不超 200ms。
面试追问:如果高峰期订单量暴涨,Batch Matching 的 3s 窗口该调大还是调小?
答:调小。高峰期订单多、司机多,即使 1s 窗口也能凑够足够的匹配对。低谷期才应该调大窗口,增加匹配可能性。Uber 的实现中,窗口大小是根据供需比动态调整的。一个简单的公式:windowSize = baseWindow / max(1, supplyDemandRatio),供需比越高,窗口越小。
供需热力与动态定价
当某个区域的订单密度远超司机密度时,触发动态定价(Surge Pricing),用价格杠杆调节供需。需要实时计算每个区域的热力值(订单数 / 司机数),由 Flink 滑动窗口聚合。
Flink 滑动窗口实现:
// Flink SQL:每 30 秒计算一次各区域供需比
// 使用 OVER 窗口避免数据倾斜
Table result = tableEnv.sqlQuery(
"SELECT " +
" geo_hash, " +
" COUNT(order_id) / COUNT(DISTINCT driver_id) AS supply_demand_ratio, " +
" CASE " +
" WHEN COUNT(order_id) / COUNT(DISTINCT driver_id) > 3.0 THEN 1.8 " +
" WHEN COUNT(order_id) / COUNT(DISTINCT driver_id) > 2.0 THEN 1.3 " +
" ELSE 1.0 " +
" END AS surge_multiplier " +
"FROM ride_events " +
"WHERE event_type IN ('ORDER', 'DRIVER_HEARTBEAT') " +
"GROUP BY TUMBLE(event_time, INTERVAL '30' SECOND), geo_hash"
);坑:动态定价被乘客薅羊毛
实际发生过:乘客发现某个区域溢价高,先走到旁边低溢价区域下单,再让司机绕路来接。解决方法:计算 ETA 时以乘客实际起点为准,而不是下单位置。或者引入"虚拟围栏",不同区域之间梯度定价,避免价格断崖。
另一个坑:溢价倍数不要设太高
滴滴内部实验数据:动态定价溢价比 2.0 倍以上时,乘客投诉率是 1.5 倍的 4 倍。大部分用户能接受 1.3-1.5 倍,超过 2.0 倍后用户流失显著增加。建议设置硬上限 2.0,超过上限时改用"排队等待"替代提价。
面试追问清单(面试官大概率会问这些)
Q1:司机 GPS 上报频率怎么定?
频率太高(1s/次)→ 30 万 QPS 写入,带宽成本高,Redis 扛不住。频率太低(10s/次)→ 位置不准,派单误差大。
标准答案:动态调整。司机在空闲状态时 3s/次,接单后去程中降到 1s/次(因为需要精确追踪),行程中 5s/次(路线固定,不需要高频上报)。另外,如果检测到手机陀螺仪在剧烈变化(颠簸、转弯),临时提高频率。
Q2:怎么保证派单的公平性?
没有绝对的公平,但可以做到"公平感"。典型做法:给等单时间长的司机加权,同等条件下优先派单;记录每个司机"被召唤但没接单"的次数,拒绝次数多的降低权重。
Q3:司机关闭 App 后怎么处理?
WebSocket 断连 → 标记为离线 → 从 Redis GEO 删除。但有个坑:GPS 的最后一次上报可能是在隧道里,信号丢失后司机其实已经开出隧道 2km 了。解决方案:断连后保留司机位置 30s(乐观保留),30s 后如果还没重连,标记离线并删除。这 30s 窗口内派到的订单,如果司机实际接不了,走超时自动取消流程。
Q4:Redis 集群 CPU 飙到 90% 怎么办?
真实案例:某二线城市网约车平台上线后第三个月,早高峰 8:00-9:00 Redis 集群 CPU 从 40% 飙升到 95%,GEORADIUS 查询延迟从 1ms 涨到 50ms,派单超时率达 15%。
排查过程:
redis-cli --hotkeys发现热 key 集中在市中心 3 个 GeoHash 格子redis-cli info commandstats显示 GEORADIUS 占了 70% 的 CPU 时间- 每个派单请求查 9 格 × 高并发,Redis 单线程处理不过来
解决方案:
- 缓存热区域查询结果:市中心热门区域的附近司机列表,每 1s 预计算一次,派单时直接读缓存,而不是实时 GEORADIUS
- 读写分离:GEO 写入走主库,查询走从库
- 提前扩容:提前 30 分钟扩容 Redis 集群(从 6 主 6 从扩到 12 主 12 从),不要等 CPU 告警再处理
Q5:司乘同显怎么实现?
乘客下单后,需要在 App 上实时看到司机的位置动画。这个功能叫"司乘同显",实现方案:
- 司机端每 1s 上报位置 → WebSocket 网关 → Kafka
- Flink 处理后,将位置推送到 Redis Pub/Sub 通道
pubsub:driver_track:{orderId} - 乘客端的 WebSocket 连接订阅对应的 Pub/Sub 通道,收到位置更新后渲染到地图上
- 坑:如果乘客端网络断开重连,丢失的中间位置怎么补?用 Redis Stream 替代 Pub/Sub,支持消费者组和消息回溯
Q6:怎么估算系统容量?
以二线城市为例:高峰同时在线司机 5 万,乘客下单峰值 3 万/分钟 = 500 单/秒。每个派单请求查 Redis GEO(O(log N) 约 1ms),加上状态检查和距离过滤,单次派单约 5ms。单机 8 核可以扛 500 QPS 的派单计算,但需要 2-3 台机器冗余。更重的负载在 GPS 写入(30 万 QPS),需要 Redis 集群分片。
总结
| 模块 | 核心方案 | 关键考量 |
|---|---|---|
| 地理位置索引 | GeoHash + Redis GEO,配合周边 8 格 | 边界问题、精度分级;量大可切 H3/S2 |
| 实时位置上报 | WebSocket → Kafka → Flink → Redis | 写入吞吐 30 万+ QPS、降噪过滤、动态频率 |
| 状态管理 | Lua 脚本原子操作 > Redis 锁 | 防双派单、锁过期时间要 Watch Dog 续期 |
| 派单算法 | 就近 ETA → 全局 Batch Matching | 初期别上 Batch Matching,复杂且不一定更好 |
| 动态定价 | Flink 滑动窗口聚合供需比 | 防薅羊毛、梯度定价避免价格断崖 |
| 司乘同显 | WebSocket + Redis Pub/Sub/Stream | 断连重连补消息、位置插值动画 |
生产避坑点:
- 双派单是最致命的线上事故——Uber 和滴滴都出过。锁和状态机要覆盖所有路径(包括订单取消、超时、司机切离线)。
- GeoHash 边界区域容易遗漏司机,必须引入周边 8 格查询补偿。
- 动态定价的溢价比不要设超过 2.0 倍,否则乘客投诉量爆发式增长(滴滴内部实验数据:2.0 倍以上投诉率是 1.5 倍的 4 倍)。
- 下单高峰期(早高峰 8:00-9:00)Redis 的 CPU 使用率可能飙升到 90%+,需要提前扩容,不要等告警再处理。
- 司机 GPS 漂移不做降噪,会导致司机在 Redis GEO 中"瞬移",派单准确率大幅下降。
参考
Uber 工程博客:The Real-time Ecosystem of the Uber (2016)
Google S2 Geometry Library:https://s2geometry.io
Uber H3:https://github.com/uber/h3
Redis GEO 文档:https://redis.io/commands/georadius
参考系统:滴滴出行、Uber 派单系统、Dispatching System Design(System Design Interview 系列)