秒配单!SpringBoot 与 GeoHash 联手打造外卖骑手实时精准派单系统!

外卖平台的实时派单,本质是一个 高频写入 + 快速查询 + 高并发 的技术难题。传统数据库方案往往在查询效率和并发控制上遇到瓶颈,而 SpringBoot + GeoHash + Redis 的组合恰好能在三方面实现突破。

随着即时配送行业的加速发展,外卖平台的订单与骑手规模呈现指数级增长。某头部平台每天处理超百万订单,在线骑手数量超过 20 万。这样庞大的规模带来了三大核心挑战:

  1. 位置更新高频:骑手每 3 秒上报一次坐标,单日产生 5.76 亿条位置数据,传统数据库难以承载高频写入。
  2. 派单需快速就近匹配:系统需在 200ms 内返回 3 公里范围内候选骑手,而传统 SQL 基于 ST_Distance 的全表计算常常超过 500ms。
  3. 高并发下避免数据竞争:高峰期同时触发 1000+ 订单派单,若处理不当会出现锁冲突与数据不一致,直接影响用户体验。

传统方案在 查询效率、数据可靠性、并发处理与边界匹配 上存在明显短板。为破解瓶颈,本文将介绍如何借助 SpringBoot + GeoHash + Redis,搭建一个高效、可靠且可扩展的实时派单系统。

为何选择 GeoHash?

空间降维:二维转一维

GeoHash 使用 Base32 编码将经纬度转为字符串(如 39.908823,116.397470 → wx4g89)。这样,本来需要在二维平面计算的“附近骑手”问题,可以简化为字符串前缀匹配,查询性能提升一个数量级。

精度灵活

GeoHash 的长度决定了定位精度:

  • 6 位(如 wx4g89):约 1 公里范围,适合全城范围的粗粒度筛选。
  • 7 位(如 wx4g89e):约 100 米范围,适合最后一公里的精匹配。

这种灵活性避免了过度精确带来的数据分散,同时兼顾效率与准确性。

Redis 提供原生地理支持

Redis 内置了 GEOADDGEORADIUS 等命令,可以直接存储骑手坐标与执行范围查询。结合 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 的组合恰好能在三方面实现突破:

  1. GeoHash 降维:空间查询转字符串匹配,效率提升十倍。
  2. Redis 高并发:原生 GEO 命令确保百万级骑手位置实时更新。
  3. 边界问题解决:相邻 GeoHash 查询避免遗漏骑手。

这种方案不仅能保障外卖派单的实时性和准确性,还具备 良好的可扩展性,可支撑未来千万级订单。对网约车调度、同城快递分配等场景同样适用。

AI大模型学习福利

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

引用中未提及外卖骑手端系统的实现方式相关内容。不过一般来说,外卖骑手端系统通常借助智能调度系统实现。该系统会综合考虑多种因素,如订送地址、预计送达时间、骑手当前位置、骑手送能力和订完成情况等,运用人工智能和大数据技术进行分析和计算。 在算法层面,可能会采用优化算法,以毫级的速度进行运算,从众多可能的方案中找出最优解,也就是能让整体送效率最高、成本最低的方案。例如,系统会优先将距离骑手较近、送时间要求较紧迫且骑手能够按时送达的订发给骑手。 以下是一个简的伪代码示例,模拟智能调度系统的部分逻辑: ```python # 定义订类 class Order: def __init__(self, order_id, delivery_address, expected_time): self.order_id = order_id self.delivery_address = delivery_address self.expected_time = expected_time # 定义骑手类 class Rider: def __init__(self, rider_id, current_location): self.rider_id = rider_id self.current_location = current_location self.orders = [] # 模拟智能调度系统 def intelligent_dispatch(orders, riders): for order in orders: best_rider = None min_distance = float('inf') for rider in riders: # 计算骑手当前位置到订送地址的距离 distance = calculate_distance(rider.current_location, order.delivery_address) if distance < min_distance: min_distance = distance best_rider = rider if best_rider: best_rider.orders.append(order) return riders # 模拟计算距离的函数 def calculate_distance(location1, location2): # 这里可以实现具体的距离计算逻辑,如使用地图 API return 10 # 示例返回值 # 示例数据 orders = [Order(1, "地址1", "12:00"), Order(2, "地址2", "12:30")] riders = [Rider(1, "位置1"), Rider(2, "位置2")] # 进行 assigned_riders = intelligent_dispatch(orders, riders) # 输出结果 for rider in assigned_riders: print(f"骑手 {rider.rider_id} 分的订: {[order.order_id for order in rider.orders]}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值