秒配單!SpringBoot 與 GeoHash 聯手打造外賣騎手實時精準派單系統!
隨著即時配送行業的加速發展,外賣平臺的訂單與騎手規模呈現指數級增長。某頭部平臺每天處理超百萬訂單,在線騎手數量超過 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 yesSpringBoot 配置
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 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 查詢避免遺漏騎手。
這種方案不僅能保障外賣派單的實時性和準確性,還具備 良好的可擴展性,可支撐未來千萬級訂單。對網約車調度、同城快遞分配等場景同樣適用。





























