跑腿业务听起来不像电商大促那么“硬核”,但真到了高峰时段,用户下单、骑手抢单、位置上报、状态变更,所有压力几乎在同一时间涌进来,系统能不能扛住,拼的就是订单派发和配送管理这两条主干道的设计水平。我做过的几个同城跑腿项目里,被问得最多的就是“为什么一到中午就卡单”“为什么两个人抢同一个单会同时成功”“为什么配送状态迟迟不更新”。这些问题背后,基本都指向Java高并发优化。这篇内容围绕订单派发与配送管理方案,把我在实际项目里用到的架构思路、代码片段、核心参数以及踩过的坑完整梳理一遍,适合正在做同城配送、外卖、跑腿类系统的后端同学参考,也适合准备Java面试时把高并发场景讲得更落地的人。
1. 先拆清楚跑腿业务的高并发热点与整体架构
1.1 跑腿系统的三个高并发核心场景
跑腿业务的并发模型和普通秒杀不一样。秒杀是短时峰值、商品固定,跑腿业务则是持续的高频请求流,而且每个请求都带有地理位置维度。我把它拆成三个核心场景来设计。
第一个是用户端的下单和支付。高峰时段比如午高峰、下雨天、节假日,用户下单请求会在几分钟内快速爬升。订单接口需要完成计价、风控、商家确认、优惠校验等逻辑,任何一个环节出现慢SQL或者外部调用超时,都会拖垮下单主链路。这个场景的瓶颈通常不在单纯的下单写库,而在下单前的一系列校验和关联查询。
第二个是骑手端的抢单与接单。这是整个系统并发压力最高的点,一个订单发布后,可能同时被几百上千个骑手看到,每个骑手点“抢单”都是一次写入操作。这里最怕的是“超抢”,也就是多个骑手同时抢同一个订单,最终数据不一致。另一个极端是“抢不到”,订单被某个骑手锁定但又不推进,导致好事者一直等待。
第三个是配送过程中的状态流转和位置上报。骑手位置每隔几秒甚至几百毫秒上报一次,一个城市几千个骑手在线,每秒就是上万次位置写入。订单状态变更,包括取货、送达、异常上报,也要在秒级内反馈给用户。这个场景的瓶颈在大流量写入和状态的一致性与时效性。
这三个场景各有各的底层矛盾:下单集中在数据库写入和关联查询,抢单集中在并发写和锁竞争,位置上报集中在大流量顺序写。架构设计时如果只做“通用高并发优化”,很容易顾此失彼,所以必须先按场景拆开,再分别选择方案。
1.2 整体架构与时延预算,先把问题量化
很多团队一上来就微服务、上K8s,结果连基本的时延目标都没定。我自己做这套系统时,第一件事不是画架构图,而是定可量化的时延预算:用户点“下单”到看到订单生成结果,P95响应时间低于500ms;骑手点“抢单”到看到结果,P95低于200ms;订单状态变更从发生到用户可见,也就是App推送或页面刷新,低于1s。
根据这个目标,整体架构按链路拆成了接入层、订单服务、派单中心、配送服务、数据存储五块。接入层用Nginx和网关做限流、鉴权和灰度;订单服务负责计价、风控、下单和订单查询;派单中心负责抢单和智能派单;配送服务负责位置上报、状态流转和超时处理;数据层用Redis扛热数据,MySQL作为最终存储,针对地理位置查询引入空间检索能力。
这里有一个容易被忽略的点:跑腿业务的所有核心逻辑都和“距离”强相关,从候选骑手筛选到配送范围校验到计价规则,全都依赖位置数据。所以存储和计算层面都需要考虑空间维度,而不是靠MySQL普通索引硬扛。我在早期版本就吃过亏,用MySQL的经纬度字段加普通索引做范围查询,一旦订单量上来,SQL直接全表扫描,响应时间飙升到秒级。
下面用一个表格把各层职责和选型整理出来,方便对照。
| 层级 | 核心职责 | 常用组件 | 关键指标 |
|---|---|---|---|
| 接入层 | 限流、鉴权、路由 | Nginx、Spring Cloud Gateway | 单机QPS、限流准确率 |
| 订单服务 | 计价、下单、订单查询 | Spring Boot、MySQL、Redis | 下单P95耗时 |
| 派单中心 | 抢单、智能派单、重试 | Redis、Redisson、MQ | 抢单P95耗时、派单成功率 |
| 配送服务 | 位置上报、状态流转 | Redis GEO、MQ、线程池 | 位置写入吞吐、状态延迟 |
| 数据层 | 最终存储、查询分析 | MySQL、ES、分库分表 | 慢SQL数量、主从延迟 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 订单派发模块:从“抢”到“派”,锁与算法的取舍
2.1 抢单模式:Redis预减、分布式锁与幂等控制
抢单模式是跑腿系统最直接的派发方式,实现起来比智能派单简单,但并发控制必须做扎实。我把核心链路拆成四步。
订单创建后,状态置为“待抢单”,同时把订单ID和状态写入Redis,key是订单状态,value用字符串或Hash保存,主要是为了让后续判断走Redis,不把压力打到MySQL。骑手端调用抢单接口,第一步并不是直接更新数据库,而是先通过Redis检查订单当前状态,如果已经不是“待抢单”,直接返回失败。
第二步是加分布式锁。锁的粒度一定要细,我踩过用全局锁的坑,后来改成按订单ID加锁,key为dispatch:rob:{orderId},避免所有抢单请求竞争同一把锁。锁的实现我用Redisson的可重入锁,设置等待时间和自动释放时间,代码如下:
java复制String lockKey = "dispatch:rob:" + orderId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 等待200ms,超过则放弃,避免大量线程阻塞
if (lock.tryLock(200, 5000, TimeUnit.MILLISECONDS)) {
// 持锁后二次校验订单状态,防止等待期间订单已被其他骑手抢走
String status = redisTemplate.opsForValue().get("order:" + orderId + ":status");
i
