外卖平台的实时派单,本质是一个 高频写入 + 快速查询 + 高并发 的技术难题。传统数据库方案往往在查询效率和并发控制上遇到瓶颈,而 SpringBoot + GeoHash + Redis 的组合恰好能在三方面实现突破。
随着即时配送行业的加速发展,外卖平台的订单与骑手规模呈现指数级增长。某头部平台每天处理超百万订单,在线骑手数量超过 20 万。这样庞大的规模带来了三大核心挑战:
- 位置更新高频:骑手每 3 秒上报一次坐标,单日产生 5.76 亿条位置数据,传统数据库难以承载高频写入。
- 派单需快速就近匹配:系统需在 200ms 内返回 3 公里范围内候选骑手,而传统 SQL 基于
ST_Distance的全表计算常常超过 500ms。 - 高并发下避免数据竞争:高峰期同时触发 1000+ 订单派单,若处理不当会出现锁冲突与数据不一致,直接影响用户体验。
传统方案在 查询效率、数据可靠性、并发处理与边界匹配 上存在明显短板。为破解瓶颈,本文将介绍如何借助 SpringBoot + GeoHash + Redis,搭建一个高效、可靠且可扩展的实时派单系统。
为何选择 GeoHash?
空间降维:二维转一维
GeoHash 使用 Base32 编码将经纬度转为字符串(如 39.908823,116.397470 → wx4g89)。这样,本来需要在二维平面计算的“附近骑手”问题,可以简化为字符串前缀匹配,查询性能提升一个数量级。
精度灵活
GeoHash 的长度决定了定位精度:
- 6 位(如
wx4g89):约 1 公里范围,适合全城范围的粗粒度筛选。 - 7 位(如
wx4g89e):约 100 米范围,适合最后一公里的精匹配。
这种灵活性避免了过度精确带来的数据分散,同时兼顾效率与准确性。
Redis 提供原生地理支持
Redis 内置了 GEOADD、GEORADIUS 等命令,可以直接存储骑手坐标与执行范围查询。结合 Hash 结构存储 GeoHash → 骑手ID 的映射,可以轻松支撑 每秒十万次位置更新与查询。
解决边界问题
仅查询单个 GeoHash 区域会漏掉边界骑手。通过 目标 GeoHash + 相邻 8 个 GeoHash 的策略,可以覆盖订单周边区域,确保不会遗漏临近骑手。
系统设计
整体架构
系统分为四层:
- 感知层:骑手端 APP 每 3 秒上传位置;用户端下单上传收货地址。
- 接入层:SpringBoot 接收请求,校验参数。
- 业务层:GeoHash 转码、派单计算逻辑。
- 存储层:Redis 保存骑手位置、GeoHash 映射、订单状态。
数据流程
骑手位置上报
- APP →
POST /rider/report - 转换为 GeoHash,更新 Redis(GEO + Hash)。
订单派单
- 用户下单 →
POST /order/dispatch - 流程:
收货地址 → GeoHash
获取目标 + 相邻 8 个 GeoHash 下的骑手
计算距离,筛选 在线 + 未超载 + 3 公里内 骑手
排序取 Top3,推送派单通知
数据模型
骑手位置模型
package com.icoderoad.dispatch.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 骑手位置模型
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RiderLocation {
private String riderId; // 骑手ID
private double lng; // 经度
private double lat; // 纬度
private String geoHash; // GeoHash
private boolean online; // 是否在线
private int orderCount; // 当前接单量
}
订单模型
package com.icoderoad.dispatch.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 订单模型
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private String orderId; // 订单ID
private double recvLng; // 收货经度
private double recvLat; // 收货纬度
private String geoHash; // 收货地址的GeoHash
private String assignedRider; // 分配的骑手ID
private String status; // 状态:待派单/已分配/完成
}
核心代码实现
Service 层
骑手位置服务
package com.icoderoad.dispatch.service;
import com.icoderoad.dispatch.model.RiderLocation;
import com.icoderoad.dispatch.util.GeoHashUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RiderLocationService {
private final StringRedisTemplate redisTemplate;
private static final String GEO_KEY = "delivery:riders";
private static final String HASH_KEY = "delivery:rider:info:";
/**
* 骑手位置上报
*/
public void reportLocation(RiderLocation rider) {
// GEO 存储坐标
redisTemplate.opsForGeo().add(GEO_KEY,
new RedisGeoCommands.GeoLocation<>(rider.getRiderId(),
new Point(rider.getLng(), rider.getLat())));
// Hash 存储附加信息
redisTemplate.opsForHash().put(HASH_KEY + rider.getRiderId(),
"geoHash", GeoHashUtils.encode(rider.getLat(), rider.getLng(), 6));
redisTemplate.opsForHash().put(HASH_KEY + rider.getRiderId(),
"online", String.valueOf(rider.isOnline()));
redisTemplate.opsForHash().put(HASH_KEY + rider.getRiderId(),
"orderCount", String.valueOf(rider.getOrderCount()));
}
/**
* 根据 geoHash 获取骑手列表(简化)
*/
public String[] getRidersByGeoHash(String geoHash) {
// 实际场景可用 redis scan + hash 过滤,这里演示简化返回
return new String[]{"rider1", "rider2"};
}
}
派单服务
package com.icoderoad.dispatch.service;
import com.icoderoad.dispatch.model.Order;
import com.icoderoad.dispatch.util.GeoHashUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@RequiredArgsConstructor
public class DispatchService {
private final RiderLocationService riderLocationService;
@Value("${dispatch.max-distance}")
private double maxDistance;
@Value("${dispatch.geohash-precision}")
private int geoHashPrecision;
/**
* 创建订单并派单
*/
public Order createAndDispatch(Order order) {
// 1. 计算订单GeoHash
String orderGeoHash = GeoHashUtils.encode(order.getRecvLat(), order.getRecvLng(), geoHashPrecision);
order.setGeoHash(orderGeoHash);
order.setStatus("待派单");
// 2. 查询目标 GeoHash + 相邻 8 个区域
Set<String> candidates = new HashSet<>();
for (String gh : GeoHashUtils.adjacent(orderGeoHash)) {
candidates.addAll(Arrays.asList(riderLocationService.getRidersByGeoHash(gh)));
}
// 3. 简化:随便取一个候选骑手
String assignedRider = candidates.stream().findFirst().orElse(null);
// 4. 更新订单对象
if (assignedRider != null) {
order.setAssignedRider(assignedRider);
order.setStatus("已分配");
}
return order;
}
}
Controller 层
骑手位置上报接口
package com.icoderoad.dispatch.controller;
import com.icoderoad.dispatch.model.RiderLocation;
import com.icoderoad.dispatch.service.RiderLocationService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/rider")
@RequiredArgsConstructor
public class RiderController {
private final RiderLocationService riderLocationService;
@PostMapping("/report")
public String reportLocation(@RequestParam String riderId,
@RequestParam double lng,
@RequestParam double lat) {
RiderLocation rider = new RiderLocation(riderId, lng, lat, null, true, 0);
riderLocationService.reportLocation(rider);
return "骑手位置上报成功";
}
}
派单接口
package com.icoderoad.dispatch.controller;
import com.icoderoad.dispatch.model.Order;
import com.icoderoad.dispatch.service.DispatchService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
private final DispatchService dispatchService;
@PostMapping("/dispatch")
public Order dispatch(@RequestParam String orderId,
@RequestParam double lng,
@RequestParam double lat) {
Order order = new Order(orderId, lng, lat, null, null, null);
return dispatchService.createAndDispatch(order);
}
}
环境与配置
Redis 启动
docker run -d --name redis-geohash -p 6379:6379 \
-v redis-data:/data \
-e REDIS_PASSWORD=redis123 \
redis:6.2.6 --appendonly yes
SpringBoot 配置
spring:
redis:
host: localhost
port: 6379
password: redis123
lettuce:
pool:
max-active: 200
max-idle: 50
dispatch:
max-distance: 3000 # 派单最大距离(米)
geohash-precision: 6 # GeoHash 精度
前端派单可视化界面
dispatch.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>派单可视化</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://webapi.amap.com/maps?v=2.0&key=你的高德Key"></script>
</head>
<body class="container mt-4">
<h3 class="mb-3">外卖派单可视化</h3>
<div id="map" style="width: 100%; height: 500px;" class="mb-3"></div>
<div class="card p-3">
<h5>模拟下单</h5>
<div class="row mb-2">
<div class="col"><input type="text" id="orderId" class="form-control" placeholder="订单ID"></div>
<div class="col"><input type="text" id="lng" class="form-control" placeholder="经度"></div>
<div class="col"><input type="text" id="lat" class="form-control" placeholder="纬度"></div>
<div class="col"><button id="btnDispatch" class="btn btn-primary w-100">派单</button></div>
</div>
<div id="result" class="alert alert-info d-none"></div>
</div>
<script>
var map = new AMap.Map("map", { zoom: 12, center: [116.397428, 39.90923] });
var riders = [
{id: "rider1", lng: 116.40, lat: 39.91},
{id: "rider2", lng: 116.38, lat: 39.92},
{id: "rider3", lng: 116.42, lat: 39.90}
];
riders.forEach(r => {
new AMap.Marker({
position: [r.lng, r.lat],
map: map,
title: r.id,
icon: "https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png"
});
});
$("#btnDispatch").click(function () {
var orderId = $("#orderId").val();
var lng = $("#lng").val();
var lat = $("#lat").val();
$.post("/order/dispatch", {orderId: orderId, lng: lng, lat: lat}, function (res) {
$("#result").removeClass("d-none").text(res);
if(res.includes("骑手")) {
new AMap.Marker({
position: [lng, lat],
map: map,
title: "订单 " + orderId,
icon: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png"
});
}
});
});
</script>
</body>
</html>
结论
外卖平台的实时派单,本质是一个 高频写入 + 快速查询 + 高并发 的技术难题。传统数据库方案往往在查询效率和并发控制上遇到瓶颈,而 SpringBoot + GeoHash + Redis 的组合恰好能在三方面实现突破:
- GeoHash 降维:空间查询转字符串匹配,效率提升十倍。
- Redis 高并发:原生 GEO 命令确保百万级骑手位置实时更新。
- 边界问题解决:相邻 GeoHash 查询避免遗漏骑手。
这种方案不仅能保障外卖派单的实时性和准确性,还具备 良好的可扩展性,可支撑未来千万级订单。对网约车调度、同城快递分配等场景同样适用。
AI大模型学习福利
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量
194

被折叠的 条评论
为什么被折叠?



