拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Cloud微服务实战:网约车项目核心链路与高并发方案

简介OnlineTaxi 是基于 Spring Cloud 的网约车全流程实战项目面向具备一定 Java 基础、希望学习微服务架构的开发者或相关专业学生。项目按乘客端、司机端与能力层拆分为订单、派单、乘客用户、短信、计价、验证码、钱包、支付、地图等多个服务并整合了 Eureka 注册中心、Config 配置中心、Zuul 网关、Hystrix 熔断监控、Zipkin 链路追踪等常用组件能帮助读者理解真实业务下的服务拆分与组件选型。资源压缩包共 1081 个文件大小 137.48MB包含大量 Java 源码、XML 配置、YML 配置文件以及编译后的 Class 文件同时配有 PNG 图片与 JS 等辅助资源目录结构清晰便于对照学习。演示流程覆盖登录注册、验证码、司机接单、到达约定地点、接待旅客、开始行程、发起收款等关键环节能直观掌握业务闭环。已有 2287 人学习适合作为微服务项目实践、毕业设计或课程设计的参考资料。 手头这个OnlineTaxi网约车项目是我最近复盘次数最多的一个Spring Cloud微服务实践。当初做它的动机很简单——市面上大多数demo项目都是订单用户库存那种三板斧真正能把网约车这种业务跑通的却不多。网约车看着不复杂可一旦把乘客端、司机端、派单、计价、支付、消息推送全部串起来分布式事务、流量削峰、服务治理这些Spring Cloud的核心痛点会全暴露出来做一遍比单纯看十遍文档都靠谱。这篇文章我尽量按实际开发顺序来写适合几类人看准备做微服务选型、想找真实业务练手的后端开发已经在用Spring Cloud但想看看网约车场景里怎么落地的同行还有那些把黑马Spring Cloud课程刷完、急着找一个完整项目来验证所学内容的朋友。文章里不会只讲概念我会把服务拆分思路、派单流程、缓存策略、网关集群、Feign超时陷阱这些问题全部串起来说代码和配置都会给出可以直接用的版本。1. 项目为什么这么设计OnlineTaxi的整体思路与模块划分1.1 一个网约车项目到底要拆多少个服务很多人做微服务项目最容易犯的错是上来就照着网上的架构图一顿拆结果服务拆了十几个每个服务里就两三个接口部署和运维成本倒比单体架构高出一大截。我在一开始就定了一个原则按业务域拆分能合则合能独立则独立。OnlineTaxi最终拆成了下面这些服务api-gateway统一入口负责路由、鉴权、限流。auth-server认证中心负责登录、Token签发与校验。passenger-service乘客端服务管理乘客信息、常用地址。driver-service司机端服务管理司机信息、车辆信息、出车状态。order-service订单中心负责订单生命周期管理。dispatch-service派单中心负责匹配司机、推送订单、超时重派。payment-service支付中心负责订单计价、支付流水、对账。message-service消息中心负责短信、App推送、站内信。map-service地图与路径服务封装第三方地图API做距离计算和路径规划。重点说一下派单中心。派单这个动作在业务上要同时读取订单状态、司机实时位置、计价规则和当前拥堵情况如果揉在订单服务里后续每次调整派单策略都要改动核心订单链路风险非常大。独立成服务后订单服务只负责下单—改状态—关单派单服务只负责找车—推送—派单两边通过MQ解耦各自演进互不干扰。1.2 选型复盘注册中心、网关、配置中心为什么这样选这个项目的技术栈是Spring Cloud Alibaba全家桶Nacos做注册中心和配置中心Gateway做网关OpenFeign做服务间调用Sentinel做熔断限流RocketMQ做消息中间件。选Nacos而不是Eureka原因很直接Eureka 2.x已经停止维护而且它只是一个注册中心配置中心还得额外搭Spring Cloud Config。Nacos同时搞定注册发现和配置管理控制台界面也友好还能直接支持命名空间隔离测试环境和生产环境用一套Nacos就能分开管理。网关选了Spring Cloud Gateway而不是Zuul 1.x是因为Zuul 1.x基于Servlet本质上是同步阻塞模型高并发场景下线程容易被IO阻塞占满。Gateway基于WebFlux底层是Netty的异步非阻塞模型同样配置下性能表现明显更好。而且Spring Cloud Gateway从Spring Cloud Finchley版本开始就进入了官方主推序列后续版本迭代和生态支持都更稳。还有朋友问为什么不直接用Dubbo。Dubbo和Spring Cloud最大的区别在于Dubbo是一个高性能RPC框架核心优势是二进制传输和内置服务治理但网关、配置中心、分布式追踪这些组件并没有官方全家桶需要自己拼装Spring Cloud则是一整套微服务解决方案从注册发现到网关到配置管理都有标准化的实现。网约车这种业务场景对单次RPC性能并不敏感但对团队协作、运维监控、系统集成的标准化要求很高所以Spring Cloud更合适。2. 核心业务链路订单服务如何与其他服务协作2.1 订单主链路拆解从乘客下单到司机接单订单主链路是整个项目最核心的流程我把它分成几个阶段来设计。第一阶段乘客创建订单。passenger-service调用order-service的/api/order/create接口order-service校验乘客状态、用车起点终点、预估价格后写入订单表订单状态为CREATED。第二阶段进入派单流程。order-service在订单落库后不直接同步调用dispatch-service而是发送一条OrderCreatedEvent到RocketMQ的ORDER_DISPATCH_TOPIC。这样做的原因是派单流程比较重需要查司机位置、计算距离、推送抢单如果全部做成同步调用乘客在下单接口上可能等上好几秒体验极差。第三阶段司机抢单。dispatch-service消费消息后通过地理位置匹配出一批候选司机把订单推送到司机端App。司机点击接单后dispatch-service调用order-service的/api/order/grab接口订单状态变为ACCEPTED同时通知乘客端司机已接单。第四阶段行程与支付。司机到达上车点后开始行程行程结束触发计费order-service调用payment-service创建支付单乘客完成支付后payment-service通过消息通知order-service更新订单状态为PAID。这里同步和异步调用的划分原则是主链路上需要立即返回结果、且失败后必须让用户感知的操作用Feign同步调用不需要用户等待、允许后台慢慢处理的操作用MQ异步解耦。比如创建订单和确认接单必须同步否则用户不知道结果派单通知和支付完成后的订单状态更新则完全可以异步。下面是一个典型的Feign调用示例order-service需要调用payment-service创建支付单FeignClient(name payment-service, fallback PaymentClientFallback.class) public interface PaymentClient { PostMapping(/api/payment/create) ResultPaymentOrderVO createPayment(RequestBody CreatePaymentRequest request); } Component Slf4j public class PaymentClientFallback implements PaymentClient { Override public ResultPaymentOrderVO createPayment(CreatePaymentRequest request) { log.error(调用支付服务创建支付单失败orderId{}, request.getOrderId()); return Result.error(支付服务暂不可用请稍重试); } }2.2 消息驱动在网约车场景里的三个关键用处RocketMQ在这个项目里承担了三个职责业务解耦、流量削峰、最终一致性。先说业务解耦。订单服务、派单服务、支付服务、消息服务之间的调用凡是异步的都走消息。以支付成功为例payment-service发送PaymentSuccessEventorder-service和message-service各自消费这个消息订单服务改状态消息服务发推送两边互不依赖。再说流量削峰。早高峰和晚高峰的订单量是平峰的十几倍如果所有请求都直接打到数据库连接池很快就会被耗尽。我的做法是订单创建请求先写Redis缓存立即返回呼叫中状态然后通过消息队列慢慢把订单写入数据库并由派单服务消费。消息队列本身就有削峰填谷的能力哪怕瞬间涌入大量订单dispatch-service也能按照自己的消费能力稳定处理。最后是最终一致性。乘客下单后订单状态和派单状态要保证最终一致但不能用分布式事务硬做。这里用的是RocketMQ事务消息order-service先发半消息然后执行本地事务订单落库本地事务成功后提交半消息dispatch-service才能看到这条订单。如果本地事务失败半消息被回滚dispatch-service永远收不到这就保证了有订单才有派单的一致性约束。消费端做幂等是必须的因为RocketMQ本身不保证消息只被消费一次。我的做法是在消费逻辑里用订单号作为业务键查询一次如果发现订单已经在派单中直接返回消费成功避免重复派单。3. 高并发场景的实战处理派单、缓存与一致性3.1 派单服务里的延迟队列实现网约车业务里超时无司机接单是常态。乘客下单后如果5秒内没有司机抢单系统不能干等要把订单重新推给一批新的司机。这个等待后重试的需求用RocketMQ的延迟消息最合适。我定义了三级延迟5秒、15秒、30秒。5秒后如果还没有司机接单系统把订单重新放入派单队列这次扩大范围推送如果15秒后仍无人接单再扩大一次范围30秒后还没接单则取消订单并通知乘客暂时没有司机接单请稍后再试。RocketMQ的延迟消息实现非常简单发送时设置延迟等级即可Message message new Message( ORDER_DISPATCH_TOPIC, orderId.getBytes(StandardCharsets.UTF_8) ); // 延迟级别3表示延迟10秒具体对应关系参考RocketMQ配置 message.setDelayTimeLevel(3); SendResult sendResult rocketMQTemplate.getProducer().send(message);这里有一个坑RocketMQ的延迟消息用的是固定的延迟级别最多支持18个等级并不支持任意秒数的延迟。所以在设计重试间隔时一定要对照实际支持的延迟级别来选不要想当然地写个延迟7秒。3.2 缓存设计热点订单和司机位置的Redis策略网约车场景里热门商圈的订单数据、司机实时位置是被查询最多的数据。如果每次都打到数据库数据库压力会非常大。但缓存也不能乱用我踩过几个典型问题。第一个是缓存穿透。乘客端如果反复查询一个不存在的订单号每次都会穿透到数据库。解决办法有两个一是对不存在的订单也缓存一个空值并设置较短的过期时间二是在代码里加一个布隆过滤器订单创建时写入过滤器查询前先判断订单号是否存在。第二个是缓存击穿。某个热门司机的位置key过期瞬间大量请求同时涌到数据库。解决办法是加互斥锁只允许一个线程去数据库查询并重建缓存其他线程等待后直接读取新缓存。下面是加锁重建缓存的伪代码public DriverLocation getDriverLocation(String driverId) { String cacheKey driver:location: driverId; Object cacheValue redisTemplate.opsForValue().get(cacheKey); if (cacheValue ! null) { return (DriverLocation) cacheValue; } // 加互斥锁只放行一个请求去查数据库 String lockKey driver:location:lock: driverId; boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (!locked) { Thread.sleep(50); return getDriverLocation(driverId); } try { DriverLocation location driverLocationMapper.selectById(driverId); redisTemplate.opsForValue().set(cacheKey, location, Duration.ofMinutes(10)); return location; } finally { redisTemplate.delete(lockKey); } }第三个是缓存雪崩。大量key同时过期会导致请求集中打到数据库。处理方式很简单在设置过期时间时加入一个随机值比如基础过期时间10分钟加随机0到120秒避免key在同一时刻集体失效。3.3 接口幂等与分布式事务网约车场景里幂等是必须处理的。乘客在弱网环境下点呼叫按钮可能连点三次司机抢单时也可能因为网络抖动重复提交。我在项目里做了一个自定义注解Idempotent配合Redis实现接口幂等。实现思路是请求进入接口时生成一个幂等键业务唯一标识比如乘客下单用乘客ID时间戳随机数司机接单用司机ID订单ID。把幂等键存入Redis设置一个合理的过期时间。如果Redis里已经有这个键说明是重复请求直接返回之前的结果如果没有则执行业务逻辑处理完成后删除幂等键。数据库层面也可以加上唯一索引做兜底比如t_order_grab表对driver_id和order_id建立联合唯一索引即使并发请求都通过了Redis校验数据库的唯一索引也能拦住重复数据。分布式事务是另一个重点。订单服务和支付服务分属两个不同数据库支付成功后需要更新订单状态这里不可能用强一致事务。我的方案是payment-service先写本地支付流水表再发事务消息order-service消费消息更新订单状态。如果消费者一直消费失败消息进入死信队列通过定时任务扫描死信队列人工处理。这套方案不复杂但能保证最终一致性也方便排查。4. Spring Cloud基础设施搭建要点4.1 Spring Cloud Gateway集群部署与路由配置热搜词里有人问Spring Cloud Gateway能做集群吗答案是不仅能而且必须能。Gateway本身是无状态的多个Gateway实例挂到同一个负载均衡器后面就组成了集群。我在项目里用Nginx对Gateway做负载均衡配置类似下面这样upstream gateway_cluster { server 192.168.1.10:8080 weight5; server 192.168.1.11:8080 weight5; server 192.168.1.12:8080 weight5; } server { listen 80; server_name api.onlinetaxi.com; location / { proxy_pass http://gateway_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }Gateway本身就是无状态的所以多个Gateway实例之间不需要做session同步。但要注意的是网关层的限流如果用了本地限流比如Guava限流那每台网关实例是独立的限流器集群10台机器意味着限流阈值被放大了10倍。解决方案是用Redis配合Sentinel实现分布式限流或者直接用Sentinel Dashboard配置集群流控规则。下面是Gateway的路由配置示例重点看StripPrefix的用法spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1 - id: dispatch-service uri: lb://dispatch-service predicates: - Path/api/dispatch/** filters: - StripPrefix1全局过滤器是网关层必须写的我在GlobalFilter里做Token校验校验通过后把乘客ID或司机ID放入请求头转发给下游服务。4.2 Feign调用的超时、熔断与降级配置服务间调用最容易出问题的就是超时。网约车项目里一个下单请求要经历网关、订单服务、派单服务、支付服务多个节点任何一环超时都可能导致整个链路失败。Feign的超时配置要分连接超时和读取超时。连接超时是指建立TCP连接的最长等待时间读取超时是指等待对端返回数据的最长等待时间。我的配置是连接超时3秒、读取超时5秒这样既能容忍正常的业务处理时间又不会让线程长时间挂起。feign: client: config: default: connectTimeout: 3000 readTimeout: 5000熔断用的是Sentinel相比Hystrix来说Sentinel支持动态规则配置和实时监控控制台不需要重启应用就能调整规则。在Feign接口上加上SentinelResource并指定fallback方法SentinelResource( value order-service#createOrder, fallback createOrderFallback, blockHandler createOrderBlock ) public ResultOrderVO createOrder(CreateOrderRequest request) { return orderFeignClient.createOrder(request); }Sentinel规则建议通过控制台动态推送不要写死在代码里。早期版本我试过直接在代码里加载规则后面加了新接口想调阈值每次都要重新发布太痛苦了。换成nacos数据源之后规则改完秒级生效运维效率提升很明显。4.3 配置中心的动态刷新实践Nacos做配置中心的一个好处是支持配置动态刷新。我在bootstrap.yml里配置了Nacos地址把数据库连接池参数、业务开关、推送文案这些高频变更的配置都放到Nacos配置中心。需要动态刷新的Bean加上RefreshScope注解RefreshScope Component ConfigurationProperties(prefix order.business) public class OrderBusinessConfig { private Integer dispatchTimeoutSeconds; private Integer cancelOrderMinutes; // getter and setter }这个功能用好了非常舒服比如线上突然要调整派单超时时间直接在Nacos控制台改配置发布不需要重启服务就能生效。但这里要注意两个坑第一RefreshScope会和Scheduled冲突如果同一个类里既有定时任务又要动态刷新配置定时任务的cron表达式不会随配置刷新而更新第二配置刷新会触发Bean重新创建如果有初始化资源比如连接池、线程池在PostConstruct里创建刷新后可能重复创建导致资源泄漏。我的经验是动态刷新只用于简单属性复杂的初始化逻辑还是老老实实重启。5. 常见问题与排查技巧实录5.1 服务注册了但调不通Nacos实例IP的坑这个坑我印象太深了。当时部署了两台order-serviceNacos控制台显示两个实例都健康但passenger-service调用时偶尔报连接拒绝。查了半天发现问题出在实例IP注册。服务器有多块网卡时Nacos客户端默认拿到的IP可能是内网Docker网桥IP这个IP只能本机访问其他机器当然连不上。解决办法是在application.yml里显式指定注册IPspring: cloud: nacos: discovery: # 指定为服务器对外提供服务的网卡IP ip: 192.168.1.20 # 指定使用哪块网卡 network-interface: eth0同样的问题在Docker部署时也很常见容器内的IP是动态的注册到Nacos的IP对宿主机外的服务不可达。建议在容器启动时通过环境变量注入宿主机IP然后手动指定spring.cloud.nacos.discovery.ip。5.2 Feign超时引发重复下单事故这是我线上遇到的最严重的一次问题。乘客端因为网络波动请求在网关层超时后重试结果订单服务里出现了两条一模一样的订单。排查下来有两个原因一是Feign的读取超时设置太短订单服务的创建订单逻辑包含了距离计算和计价正常需要2秒我把读取超时设成了1秒导致大量正常请求被判超时二是网关层开启了重试机制请求超时后自动重新转发。这个问题的处理分两步第一步把Feign读取超时调整到合理值并给订单创建接口加幂等键第二步在网关的路由过滤器里关闭对幂等敏感接口的重试只允许对GET请求重试。spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - name: Retry args: retries: 0隐患排查的通用思路是服务间调用必须默认加超时时间重试只加到幂等的接口上否则宁可失败也不要重试。5.3 分布式事务消息丢失的排查记录另一个典型问题是订单创建了但一直停在等待派单状态。初步判断是OrderCreatedEvent消息丢了但RocketMQ一般不会无故丢消息仔细排查下来发现问题出在事务消息的回查接口上。RocketMQ事务消息的流程是先发半消息然后执行本地事务再提交或回滚半消息。如果第一步发送成功了但第二步提交半消息时网络超时RocketMQ会反向回查本地事务状态。我当时的回查接口直接返回了UNKNOWN导致RocketMQ无法确认这个半消息是提交还是回滚多次回查无果后消息就被丢弃了。正确的回查逻辑应该是根据本地事务表反查真实状态如果订单表中存在这条订单返回COMMIT如果不存在返回ROLLBACK。排查到最后是代码里回查时查错了表查了缓存没查数据库。最终的兜底方案是加一个定时巡检任务每隔5分钟扫描一次订单表里状态为CREATED且创建时间超过5分钟的订单如果发现异常就重新发送派单消息。定时任务兜底虽然不优雅但在分布式场景里非常实用能解决绝大部分消息丢失的边界问题。5.4 常见问题排查速查表整理一份排查清单方便大家直接参考症状根因解决措施服务A调服务B报连接拒绝Nacos实例IP错误显式指定spring.cloud.nacos.discovery.ip网关重试导致重复下单非幂等接口开启了Retry关闭写接口的重试接口加幂等键订单创建但一直等待派单事务消息回查逻辑错误回查本地事务表增加定时巡检兜底服务重启后配置丢失配置中心未配置命名空间按环境配置spring.cloud.nacos.config.namespaceSentinel规则不生效使用了本地加载而非动态推送改成nacos数据源推送规则消息重复消费消费者未做幂等使用业务主键查询去重再做数据处理最后说一点我个人的体会。做OnlineTaxi这个项目最花时间的其实不是写代码而是反复调各种看似正常但实际有问题的配置。微服务架构的难点不在某个单一服务而在于服务之间的协作和边界。每次踩坑本质上都是在加深对分布式系统底层逻辑的理解——超时、重试、幂等、一致性这些东西光看书很难有切身体会真的做一遍项目才能感受到它们的分量。如果你也在做类似的网约车项目建议先从订单主链路跑通再逐步加入派单、支付、消息这些复杂度迭代着来比一次性搭完整个系统要靠谱得多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门