在实际的后端系统设计中,网约车服务是一个经典且复杂的分布式系统课题。它不仅仅是简单的“用户下单、司机接单”,背后涉及实时地理位置匹配、动态定价、派单算法、多角色状态机、支付对账、风控反作弊等一系列高并发、低延迟的工程挑战。无论是作为面试中的系统设计环节,还是作为个人技术项目来构建核心能力,深入理解一个网约车服务的架构都极具价值。

本文将以一个资深工程师的视角,从零开始,逐步拆解如何设计一个类似 Uber 或 Lyft 的网约车服务。我们将聚焦于核心的后端架构、数据模型、关键流程和算法,并会提供可运行的代码片段、配置示例和排查思路。目标是让你不仅能画出架构图,更能理解每个组件为何存在、如何交互,以及在真实生产环境中可能遇到的坑。本文假设读者具备基本的分布式系统、数据库和微服务概念,我们将一起构建一个概念上完整、逻辑上自洽的简化版网约车系统。

1. 核心业务流程与系统边界定义

在动手画架构图之前,必须先把业务的核心流程和参与角色梳理清楚。这是所有后续技术决策的基石。

1.1 核心参与角色与状态

一个典型的网约车服务至少涉及三个核心角色:乘客(Rider)、司机(Driver)和系统后台(Dispatch System)。每个角色在业务流程中都有明确的状态变迁。

  • 乘客端状态流 空闲 -> 发起叫车(选择车型、起点、终点) -> 等待匹配 -> 匹配成功(等待司机到达) -> 行程中 -> 到达目的地,待支付 -> 支付完成,行程结束 -> 空闲
  • 司机端状态流 离线 -> 上线(可接单) -> 听单(空闲,等待系统派单) -> 收到订单(抢单或系统派单) -> 前往接驾 -> 到达上车点,等待乘客 -> 行程开始 -> 行程结束,待确认 -> 听单/离线
  • 订单状态流 创建(CREATED) -> 寻找司机(DISPATCHING) -> 司机接单(DRIVER_ASSIGNED) -> 司机到达上车点(DRIVER_ARRIVED) -> 行程开始(IN_TRIP) -> 行程结束,待支付(COMPLETED) -> 支付完成(CLOSED) / 取消(CANCELLED)

这些状态必须被严格定义和管理,任何不一致(例如司机端显示“行程中”而订单状态却是“等待接驾”)都会导致严重的用户体验问题或资损。

1.2 关键业务流程拆解

整个服务可以拆解为以下几个关键子流程,每个流程对应后端的一组服务或模块:

  1. 乘客叫车与订单创建 :乘客提交起点、终点、车型偏好。系统需要验证乘客账户状态、计算预估价格、进行基础风控(如反刷单),然后创建订单。
  2. 实时司机匹配与派单 :这是系统的“大脑”。需要基于乘客位置,从在线且空闲的司机池中,根据距离、司机评分、车型匹配、顺路度等多种因素,以毫秒级延迟完成最优匹配。模式可以是“抢单”或“系统派单”。
  3. 行程跟踪与计费 :匹配成功后,需要实时追踪司机和乘客的位置,计算行驶距离和时间,并基于动态定价规则(如高峰溢价、长途优惠)实时计算车费。
  4. 支付与清结算 :行程结束后,触发支付流程。可能涉及第三方支付渠道、优惠券抵扣、平台抽成、司机收入结算等复杂逻辑。
  5. 通知与通信 :在整个流程中,需要通过推送、短信或WebSocket,实时将状态变更通知给乘客和司机App。

明确了这些,我们就可以开始设计支撑这些流程的技术组件了。

2. 系统架构设计与技术选型

一个高可用的网约车后端通常采用微服务架构,服务之间通过RPC或消息队列进行异步解耦。下面是一个简化的核心架构图描述(文字表述):

[用户/司机 App] -> (负载均衡器) -> [API Gateway]
                                      |
                                      | (路由、认证、限流)
                                      v
    +-----------------------------+-----------------------------+
    |                             |                             |
[乘客服务]                    [司机服务]                    [派单服务]
(管理乘客信息、叫车)        (管理司机状态、位置)        (核心匹配算法)
    |                             |                             |
    +-----------------------------+-----------------------------+
                                      |
                                      | (服务间通信: gRPC/REST/MQ)
                                      v
    +-----------------------------+-----------------------------+
    |                             |                             |
[行程服务]                    [支付服务]                    [通知服务]
(管理订单状态、计费)        (处理支付、结算)        (发送推送/短信)
    |                             |                             |
    +-----------------------------+-----------------------------+
                                      |
                                      v
                               [数据存储层]
                               (MySQL, Redis, Kafka, etc.)

2.1 核心服务职责与交互

  • API网关 :所有客户端请求的单一入口。负责认证、鉴权、限流、路由转发到内部微服务。可以使用 Spring Cloud Gateway, Kong, Nginx+Lua 等实现。
  • 乘客服务 :处理乘客相关的所有操作,如注册、登录、查询历史订单、发起叫车请求。叫车请求会创建一个初始订单,并调用派单服务。
  • 司机服务 :管理司机生命周期(注册、审核、上线/下线)、实时上报和存储司机地理位置、管理司机的接单状态。
  • 派单服务 :这是最复杂的服务。它订阅司机位置更新,监听新创建的订单。内部包含匹配引擎,根据策略(如最近距离、服务质量分)为订单分配合适的司机。它需要极低的延迟和高吞吐量。
  • 行程服务 :订单创建后的总管。它维护订单状态机,协调乘客服务、司机服务、支付服务,驱动订单从一个状态流转到下一个状态。它也负责基于位置更新计算实时费用。
  • 支付服务 :处理支付创建、回调、退款、优惠券核销以及平台与司机之间的清分结算。
  • 通知服务 :一个异步消息中枢。其他服务将需要通知的事件(如“订单已匹配”、“司机已到达”)发送到消息队列(如Kafka),由通知服务消费并调用第三方推送服务(如极光、个推)或短信网关。

2.2 数据存储选型与设计

没有一种数据库能解决所有问题,网约车场景需要混合使用多种存储。

  • 关系型数据库 :用于存储核心的、需要强一致性和事务支持的数据。如用户信息、司机信息、订单主表、支付记录、优惠券。
    -- 订单表核心字段示例
    CREATE TABLE `ride_order` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
      `order_no` varchar(32) NOT NULL COMMENT '订单号',
      `passenger_id` bigint(20) NOT NULL COMMENT '乘客ID',
      `driver_id` bigint(20) DEFAULT NULL COMMENT '司机ID',
      `status` varchar(32) NOT NULL COMMENT '订单状态',
      `start_lat` decimal(10, 8) NOT NULL COMMENT '起点纬度',
      `start_lng` decimal(11, 8) NOT NULL COMMENT '起点经度',
      `end_lat` decimal(10, 8) DEFAULT NULL COMMENT '终点纬度',
      `end_lng` decimal(11, 8) DEFAULT NULL COMMENT '终点经度',
      `estimated_price` decimal(10, 2) NOT NULL COMMENT '预估价格',
      `final_price` decimal(10, 2) DEFAULT NULL COMMENT '最终价格',
      `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uk_order_no` (`order_no`),
      KEY `idx_passenger_status` (`passenger_id`, `status`),
      KEY `idx_driver_status` (`driver_id`, `status`),
      KEY `idx_created` (`created_at`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行程订单表';
    
  • Redis :用于缓存和存储实时、高频访问的临时数据。
    • 缓存 :用户信息、城市配置、定价规则。
    • 实时数据 :在线司机集合、司机最新位置(GeoHash)、订单匹配过程中的临时锁(防止一单多派)。
    • 计数器/限流 :用户每日叫车次数、短信验证码发送频率。
    // 示例:使用Redis Geo存储司机位置
    @Component
    public class DriverLocationService {
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
        private static final String DRIVER_LOCATION_KEY = "driver:location";
    
        public void updateDriverLocation(Long driverId, double lng, double lat) {
            // 将司机ID作为member,经纬度作为score(使用GeoHash)
            redisTemplate.opsForGeo().add(DRIVER_LOCATION_KEY, new Point(lng, lat), driverId.toString());
            // 同时可以设置一个过期时间,自动清理长时间未上报的司机
            redisTemplate.expire(DRIVER_LOCATION_KEY, 5, TimeUnit.MINUTES);
        }
    
        public List<Long> findNearbyDrivers(double lng, double lat, double radiusKm) {
            // 查询指定经纬度半径内的司机
            Circle within = new Circle(new Point(lng, lat), new Distance(radiusKm, Metrics.KILOMETERS));
            GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
                    .radius(DRIVER_LOCATION_KEY, within);
            // 转换为司机ID列表
            return results.getContent().stream()
                    .map(geo -> Long.valueOf(geo.getContent().getName()))
                    .collect(Collectors.toList());
        }
    }
    
  • 消息队列 :用于服务间异步解耦和流量削峰。Kafka非常适合日志、事件流(如位置更新、状态变更事件)。RabbitMQ或RocketMQ可用于事务性消息,如支付回调保证。
    # 示例:Spring Boot 集成 Kafka 配置 (application.yml)
    spring:
      kafka:
        bootstrap-servers: localhost:9092
        producer:
          key-serializer: org.apache.kafka.common.serialization.StringSerializer
          value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
        consumer:
          group-id: dispatch-service-group
          key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
          value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
          properties:
            spring.json.trusted.packages: "com.example.ridesharing.event"
    
  • Elasticsearch :用于订单历史、司机行程记录的复杂搜索和聚合分析,例如“搜索我上个月所有去机场的订单”。

3. 核心难点实现:实时派单与位置追踪

这是网约车系统最具挑战性的部分,我们深入其实现细节。

3.1 司机位置上报与存储

司机App需要以高频率(如每3-5秒)上报其GPS位置。直接写入数据库是不可行的。

方案

  1. 司机App 通过HTTP或WebSocket将 (driverId, lng, lat, timestamp) 上报到 API网关
  2. API网关 将位置数据打包发送到 Kafka 的一个特定Topic,如 driver-location-updates 。这样做可以削峰,避免后端服务被突发流量打垮。
  3. 司机服务 派单服务 都消费这个Topic。
    • 司机服务 消费后,将位置更新到Redis的GEO数据结构中,用于实时查询。
    • 派单服务 消费后,更新其内部维护的“可用司机内存池”,用于快速匹配。
// 示例:司机位置上报DTO和Kafka生产者
@Data
public class DriverLocationEvent {
    private Long driverId;
    private BigDecimal longitude;
    private BigDecimal latitude;
    private Long timestamp;
}

@Service
public class DriverLocationProducer {
    private static final String TOPIC = "driver-location-updates";
    @Autowired
    private KafkaTemplate<String, DriverLocationEvent> kafkaTemplate;

    public void sendLocationUpdate(DriverLocationEvent event) {
        kafkaTemplate.send(TOPIC, event.getDriverId().toString(), event);
    }
}

3.2 派单匹配引擎

匹配引擎的目标是在毫秒内为一个新订单找到最合适的司机。一个简单的“最近距离优先”策略实现如下:

  1. 触发 :乘客服务创建订单后,发布一个 OrderCreatedEvent 到Kafka。
  2. 消费 :派单服务消费该事件,开始匹配流程。
  3. 查询附近司机 :根据订单起点 (startLng, startLat) ,从Redis GEO中查询半径R(例如3公里)内的所有在线司机ID。
  4. 过滤与排序
    • 过滤 :排除状态不是“听单”的司机、排除车型不匹配的司机、排除评分过低的司机。
    • 排序 :对剩余的司机,按照到起点的距离进行排序。
  5. 派单 :选择排名第一的司机,尝试通过分布式锁(如Redis SETNX)锁定“订单-司机”关系,防止并发派给多人。锁定成功后,调用司机服务更新司机状态,并发布 OrderDispatchedEvent
// 示例:简化的派单匹配逻辑
@Service
public class DispatchService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private DriverServiceClient driverServiceClient; // 假设是Feign客户端
    @Autowired
    private OrderServiceClient orderServiceClient;

    @KafkaListener(topics = "order-created-events")
    public void handleOrderCreated(OrderCreatedEvent event) {
        Long orderId = event.getOrderId();
        Point startPoint = new Point(event.getStartLng(), event.getStartLat());

        // 1. 查询附近司机
        List<Long> nearbyDriverIds = findNearbyDrivers(startPoint, 3.0);
        if (nearbyDriverIds.isEmpty()) {
            // 无司机可用,可扩大范围或标记订单为等待
            return;
        }

        // 2. 过滤与排序 (这里简化,实际需查数据库或缓存获取司机详情)
        List<Long> eligibleDrivers = filterAndSortDrivers(nearbyDriverIds, event.getVehicleType());

        for (Long driverId : eligibleDrivers) {
            // 3. 尝试派单锁
            String lockKey = "dispatch:lock:order:" + orderId;
            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, driverId.toString(), 10, TimeUnit.SECONDS);
            if (Boolean.TRUE.equals(locked)) {
                try {
                    // 4. 调用司机服务,尝试分配订单
                    boolean assignSuccess = driverServiceClient.assignOrder(driverId, orderId);
                    if (assignSuccess) {
                        // 5. 更新订单状态
                        orderServiceClient.dispatchOrder(orderId, driverId);
                        // 发布派单成功事件
                        // ...
                        break; // 派单成功,结束循环
                    }
                } finally {
                    // 释放锁(如果派单失败,允许其他匹配尝试)
                    redisTemplate.delete(lockKey);
                }
            }
        }
    }
}

3.3 行程计费与动态定价

计费通常在行程服务中完成。司机开始行程和结束行程时,会发送事件。行程服务监听这些事件,并启动/停止一个计费计时器。

计费公式(简化) 总费用 = 起步价 + 里程费 * 行驶公里数 + 时长费 * 行驶分钟数 + 动态溢价

动态溢价可能基于:

  • 时间 :夜间服务费。
  • 区域 :机场、火车站等特殊区域。
  • 供需 :高峰时段、恶劣天气下的溢价倍数。
// 示例:计费服务核心逻辑
@Service
public class PricingService {
    public BigDecimal calculateFare(Trip trip, PricingRule rule) {
        BigDecimal fare = rule.getBaseFare(); // 起步价

        // 计算里程费
        BigDecimal distance = trip.getDistance(); // 单位:公里
        fare = fare.add(rule.getPerKmRate().multiply(distance));

        // 计算时长费
        long durationMinutes = trip.getDuration().toMinutes();
        fare = fare.add(rule.getPerMinuteRate().multiply(BigDecimal.valueOf(durationMinutes)));

        // 应用动态溢价(例如1.5倍)
        BigDecimal surgeMultiplier = getCurrentSurgeMultiplier(trip.getCityId(), trip.getStartTime());
        fare = fare.multiply(surgeMultiplier);

        // 确保不低于最低消费
        fare = fare.max(rule.getMinimumFare());

        return fare.setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal getCurrentSurgeMultiplier(Long cityId, LocalDateTime time) {
        // 从Redis或配置中心读取当前城市、当前时间片的溢价系数
        // 这是一个简化示例,实际逻辑更复杂
        return BigDecimal.valueOf(1.2);
    }
}

4. 生产环境关键考量与常见问题排查

一个能在学习环境跑通的Demo和能扛住生产流量的系统之间,隔着无数个“坑”。

4.1 稳定性与高可用设计

  • 服务无状态化 :所有服务实例不应保存本地会话状态,状态应存储在Redis或数据库中。这样便于水平扩容和故障转移。
  • 冗余与负载均衡 :每个服务至少部署两个实例,前面通过负载均衡器(如Nginx, Kubernetes Service)分发流量。
  • 熔断与降级 :使用Resilience4j或Sentinel实现熔断。例如,当支付服务不可用时,行程服务可以先将订单标记为“待支付”,并记录日志,稍后通过后台任务重试,而不是让用户一直等待或失败。
  • 监控与告警 :这是系统的“眼睛”。必须监控:
    • 应用层 :每个接口的QPS、延迟、错误率(使用Prometheus + Grafana)。
    • JVM层 :GC情况、堆内存使用、线程池状态。
    • 中间件 :Redis内存/连接数、Kafka堆积情况、数据库连接池。
    • 业务指标 :每日订单量、成交率、平均匹配时长、取消率。

4.2 数据一致性与分布式事务

网约车业务中有很多“要么一起成功,要么一起失败”的操作,例如“派单”需要同时更新订单状态和司机状态。

常用模式

  • 最终一致性 + 事件驱动 :这是主流选择。例如,派单服务在锁定订单和司机后,发布一个 OrderDispatchedEvent 。乘客服务和司机服务监听该事件,各自更新自己的视图。如果某个服务更新失败,需要有补偿机制(如监听死信队列重试)。
  • Saga模式 :对于跨多服务的长流程(如创建订单->派单->开始行程->结束行程->支付),可以使用Saga编排或协同模式,每个步骤都有对应的补偿操作。
  • 避免分布式事务 :在数据库设计时,尽量将关联紧密的数据放在同一个数据库实例中,利用本地事务。例如,订单的核心状态变迁和支付记录可以放在同一个库。

4.3 典型问题排查清单

当线上出现问题时,可以按照以下路径快速定位。

问题现象 可能原因 检查点 处理建议
乘客叫车后长时间无司机接单 1. 派单服务故障或压力大。
2. 附近无可用司机。
3. 司机位置上报异常。
4. 匹配算法过于严格或Bug。
1. 查看派单服务监控(CPU、错误日志)。
2. 查询Redis中订单起点附近的司机GEO数据是否为空。
3. 查看Kafka driver-location-updates Topic是否有堆积。
4. 查看派单服务对特定订单的匹配逻辑日志。
1. 重启或扩容派单服务实例。
2. 人工检查司机上线流程。
3. 重启司机位置上报链路或检查App端网络。
4. 紧急情况下,可临时放宽匹配条件(如扩大搜索半径)。
行程费用计算异常(过高或为0) 1. 计费服务获取的行驶距离/时长数据错误。
2. 动态定价规则配置错误或未加载。
3. 计费公式代码存在边界条件Bug。
1. 检查行程服务接收到的“行程结束”事件中的 distance duration 字段。
2. 检查配置中心或Redis中定价规则的数值。
3. 复查计费服务日志,查看计算过程中的中间值。
1. 修复数据源问题。
2. 回滚或修复定价配置。
3. 修复代码Bug,并对异常订单进行人工复核和补偿。
司机/乘客App收不到推送 1. 通知服务故障。
2. 消息队列堆积。
3. 第三方推送服务(如极光)配额用尽或配置错误。
4. 用户设备Token失效。
1. 检查通知服务健康状态和日志。
2. 检查Kafka中 notification-events Topic的消费延迟。
3. 查看第三方推送服务控制台的状态和报表。
4. 检查数据库中用户的设备Token是否过期。
1. 重启通知服务。
2. 增加通知服务消费者实例。
3. 联系第三方服务商或检查账户配置。
4. 引导用户重新登录更新Token。
数据库CPU持续飙高 1. 慢查询。
2. 缺少有效索引。
3. 被恶意爬虫或刷单脚本攻击。
1. 查看数据库慢查询日志,找到TOP N的慢SQL。
2. 使用 EXPLAIN 分析慢SQL的执行计划。
3. 分析访问日志,寻找异常的请求模式(如单一用户高频请求)。
1. 优化SQL语句,避免全表扫描和复杂JOIN。
2. 为高频查询条件添加索引(需评估对写性能的影响)。
3. 在API网关层加强限流和风控规则。

4.4 安全与风控

  • 防刷单 :识别同一设备、IP、支付账户的异常订单模式。对优惠券领取和使用设置频率限制。
  • 数据安全 :用户手机号、身份证等敏感信息脱敏存储。API接口需要对请求参数进行校验,防止SQL注入和XSS攻击。
  • 支付安全 :支付回调接口必须验证签名,防止伪造回调。金额相关操作必须使用 BigDecimal 并确定精度和舍入模式。

5. 从设计到实现:一个简化的可运行示例

为了将概念落地,我们创建一个最简化的Spring Boot项目,包含订单创建和派单的核心流程。

项目结构

ridesharing-demo/
├── src/main/java/com/example/ridesharing/
│   ├── RidesharingApplication.java
│   ├── controller/
│   │   ├── OrderController.java       # 乘客下单接口
│   │   └── DriverController.java      # 司机位置上报接口
│   ├── service/
│   │   ├── OrderService.java          # 订单服务
│   │   ├── DispatchService.java       # 派单服务(简化内存版)
│   │   └── DriverLocationService.java # 司机位置服务(使用Redis)
│   ├── repository/
│   │   └── OrderRepository.java       # 订单JPA仓库
│   ├── model/
│   │   ├── Order.java                 # 订单实体
│   │   └── DriverLocation.java        # 司机位置实体(Redis用)
│   └── event/
│       └── OrderCreatedEvent.java     # 订单创建事件
├── src/main/resources/
│   ├── application.yml                # 应用配置
│   └── application-dev.yml            # 开发环境配置(Redis, Kafka)
└── pom.xml                            # Maven依赖

核心依赖 ( pom.xml ):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 其他工具依赖 -->
</dependencies>

订单创建接口示例 ( OrderController.java ):

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        // 1. 参数校验
        if (request.getStartLat() == null || request.getStartLng() == null) {
            throw new IllegalArgumentException("起点坐标不能为空");
        }
        // 2. 调用服务层创建订单
        Order order = orderService.createOrder(
                request.getPassengerId(),
                request.getStartLng(),
                request.getStartLat(),
                request.getEndLng(),
                request.getEndLat(),
                request.getVehicleType()
        );
        // 3. 返回创建成功的订单
        return ResponseEntity.ok(order);
    }
}

内存派单服务简化版 ( DispatchService.java ): 这个版本没有使用Kafka,而是通过 @Async 模拟异步处理,并使用一个内存中的 ConcurrentHashMap 来模拟在线司机池。

@Service
public class SimpleDispatchService {
    // 模拟在线司机池:Map<司机ID, 位置>
    private final ConcurrentHashMap<Long, Point> onlineDrivers = new ConcurrentHashMap<>();
    private final Random random = new Random();

    // 司机上线
    public void driverOnline(Long driverId, Point location) {
        onlineDrivers.put(driverId, location);
        System.out.println("司机 " + driverId + " 上线,位置: " + location);
    }

    // 处理新订单(异步)
    @Async
    public void dispatchOrderAsync(Order order) {
        System.out.println("开始为订单 " + order.getId() + " 寻找司机...");
        // 模拟匹配延迟
        try {
            Thread.sleep(1000 + random.nextInt(2000));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 简单的匹配逻辑:随机选一个在线司机
        List<Long> driverIds = new ArrayList<>(onlineDrivers.keySet());
        if (!driverIds.isEmpty()) {
            Long assignedDriverId = driverIds.get(random.nextInt(driverIds.size()));
            order.setDriverId(assignedDriverId);
            order.setStatus(OrderStatus.DRIVER_ASSIGNED);
            // 这里应调用OrderRepository保存,并发送通知
            System.out.println("订单 " + order.getId() + " 已分配给司机 " + assignedDriverId);
        } else {
            System.out.println("当前无可用司机,订单 " + order.getId() + " 等待中");
        }
    }
}

运行与验证

  1. 启动本地MySQL、Redis(如果用到)。
  2. 运行Spring Boot应用。
  3. 使用 curl 或Postman调用 /api/drivers/{id}/location 模拟几个司机上线并上报位置。
  4. 调用 /api/orders 创建一个新订单。
  5. 观察控制台日志,查看派单过程。

这个示例极度简化,省略了错误处理、事务、消息队列和真实的地理计算,但它清晰地展示了从API接收到后台异步处理的完整代码链路。你可以在此基础上,逐步引入前面章节讨论的Redis GEO、Kafka、状态机等组件,将其演进为一个更接近生产环境的系统。

设计一个网约车服务是一次完整的分布式系统实战演练,它强迫你思考并发、一致性、延迟、容错和数据模型。从最简单的模型开始,逐步引入复杂性,并时刻用监控和日志来观察系统的行为,是掌握这类系统设计最有效的方法。下一步,你可以尝试实现基于Redis GEO的真实附近司机查询,或者引入状态机框架来管理订单状态流转,这将让你对生产级代码有更深的理解。

更多推荐