Skip to content

设计一个网约车派单系统

提出问题

网约车派单系统是典型的高并发、低延迟、实时地理位置的分布式系统。面试中这道题考查的不仅是系统设计能力,更是对地理空间索引、实时数据流、资源分配优化等综合工程能力的考察。生产环境中,滴滴高峰期每秒需要处理 3 万次以上派单请求(Uber 类似),每个请求必须在 200ms 内完成司机匹配,这对系统的可用性、一致性和扩展性提出了极高要求。面试官希望看到候选人对"附近司机怎么找""谁先接单""怎么防止一个司机被同时派给多单"等核心问题有清晰方案。

核心难点拆解

从工程实现角度,派单系统需要解决三个核心问题:

  1. 空间索引:给定乘客经纬度,怎么在几十毫秒内找到半径 3km 内的空闲司机?
  2. 状态一致性:怎么保证一个司机不会被同时派给两个乘客?
  3. 分配最优:当多个乘客和多个司机同时等待,怎么分配总等待时间最短?

这三个问题层层递进,面试时按这个顺序讲,比直接抛方案更有层次感。

地理位置索引:如何快速找到附近司机

网约车最核心的查询是"给定乘客位置,找出半径 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),非常适合"附近司机"这种高频低延迟场景。

java
// 使用 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 中"瞬移",导致派单给实际不在该位置的司机。

java
// 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 续期锁。

java
// 方案 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;
}
java
// 方案 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 只需要查一次。

java
// 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+,计算时间会飙升到秒级。优化方案:

  1. 空间聚类:按 GeoHash 将订单和司机分成多个区域,每个区域独立计算(区域间不跨区派单——除非附近区域没有空闲司机),这样每个区域的 m,n 通常不超过 100。
  2. 时间分片:高峰期 3s 窗口缩短到 1s,低谷期延长到 5s。窗口大小根据供需比动态调整。
  3. 贪心近似:当 m,n > 500 时,降级为贪心算法(最近司机优先),保证延迟不超 200ms。

面试追问:如果高峰期订单量暴涨,Batch Matching 的 3s 窗口该调大还是调小?

:调小。高峰期订单多、司机多,即使 1s 窗口也能凑够足够的匹配对。低谷期才应该调大窗口,增加匹配可能性。Uber 的实现中,窗口大小是根据供需比动态调整的。一个简单的公式:windowSize = baseWindow / max(1, supplyDemandRatio),供需比越高,窗口越小。

供需热力与动态定价

当某个区域的订单密度远超司机密度时,触发动态定价(Surge Pricing),用价格杠杆调节供需。需要实时计算每个区域的热力值(订单数 / 司机数),由 Flink 滑动窗口聚合。

Flink 滑动窗口实现

java
// 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%。

排查过程

  1. redis-cli --hotkeys 发现热 key 集中在市中心 3 个 GeoHash 格子
  2. redis-cli info commandstats 显示 GEORADIUS 占了 70% 的 CPU 时间
  3. 每个派单请求查 9 格 × 高并发,Redis 单线程处理不过来

解决方案

  1. 缓存热区域查询结果:市中心热门区域的附近司机列表,每 1s 预计算一次,派单时直接读缓存,而不是实时 GEORADIUS
  2. 读写分离:GEO 写入走主库,查询走从库
  3. 提前扩容:提前 30 分钟扩容 Redis 集群(从 6 主 6 从扩到 12 主 12 从),不要等 CPU 告警再处理

Q5:司乘同显怎么实现?

乘客下单后,需要在 App 上实时看到司机的位置动画。这个功能叫"司乘同显",实现方案:

  1. 司机端每 1s 上报位置 → WebSocket 网关 → Kafka
  2. Flink 处理后,将位置推送到 Redis Pub/Sub 通道 pubsub:driver_track:{orderId}
  3. 乘客端的 WebSocket 连接订阅对应的 Pub/Sub 通道,收到位置更新后渲染到地图上
  4. :如果乘客端网络断开重连,丢失的中间位置怎么补?用 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断连重连补消息、位置插值动画

生产避坑点

  1. 双派单是最致命的线上事故——Uber 和滴滴都出过。锁和状态机要覆盖所有路径(包括订单取消、超时、司机切离线)。
  2. GeoHash 边界区域容易遗漏司机,必须引入周边 8 格查询补偿。
  3. 动态定价的溢价比不要设超过 2.0 倍,否则乘客投诉量爆发式增长(滴滴内部实验数据:2.0 倍以上投诉率是 1.5 倍的 4 倍)。
  4. 下单高峰期(早高峰 8:00-9:00)Redis 的 CPU 使用率可能飙升到 90%+,需要提前扩容,不要等告警再处理。
  5. 司机 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 系列)

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。