偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

秒配單!SpringBoot 與 GeoHash 聯(lián)手打造外賣(mài)騎手實(shí)時(shí)精準(zhǔn)派單系統(tǒng)!

開(kāi)發(fā) 前端
外賣(mài)平臺(tái)的實(shí)時(shí)派單,本質(zhì)是一個(gè) 高頻寫(xiě)入 + 快速查詢(xún) + 高并發(fā) 的技術(shù)難題。傳統(tǒng)數(shù)據(jù)庫(kù)方案往往在查詢(xún)效率和并發(fā)控制上遇到瓶頸,而 SpringBoot + GeoHash + Redis 的組合恰好能在三方面實(shí)現(xiàn)突破。

隨著即時(shí)配送行業(yè)的加速發(fā)展,外賣(mài)平臺(tái)的訂單與騎手規(guī)模呈現(xiàn)指數(shù)級(jí)增長(zhǎng)。某頭部平臺(tái)每天處理超百萬(wàn)訂單,在線騎手?jǐn)?shù)量超過(guò) 20 萬(wàn)。這樣龐大的規(guī)模帶來(lái)了三大核心挑戰(zhàn):

  1. 位置更新高頻:騎手每 3 秒上報(bào)一次坐標(biāo),單日產(chǎn)生 5.76 億條位置數(shù)據(jù),傳統(tǒng)數(shù)據(jù)庫(kù)難以承載高頻寫(xiě)入。
  2. 派單需快速就近匹配:系統(tǒng)需在 200ms 內(nèi)返回 3 公里范圍內(nèi)候選騎手,而傳統(tǒng) SQL 基于 ST_Distance 的全表計(jì)算常常超過(guò) 500ms。
  3. 高并發(fā)下避免數(shù)據(jù)競(jìng)爭(zhēng):高峰期同時(shí)觸發(fā) 1000+ 訂單派單,若處理不當(dāng)會(huì)出現(xiàn)鎖沖突與數(shù)據(jù)不一致,直接影響用戶(hù)體驗(yàn)。

傳統(tǒng)方案在 查詢(xún)效率、數(shù)據(jù)可靠性、并發(fā)處理與邊界匹配 上存在明顯短板。為破解瓶頸,本文將介紹如何借助 SpringBoot + GeoHash + Redis,搭建一個(gè)高效、可靠且可擴(kuò)展的實(shí)時(shí)派單系統(tǒng)。

為何選擇 GeoHash?

空間降維:二維轉(zhuǎn)一維

GeoHash 使用 Base32 編碼將經(jīng)緯度轉(zhuǎn)為字符串(如 39.908823,116.397470 → wx4g89)。這樣,本來(lái)需要在二維平面計(jì)算的“附近騎手”問(wèn)題,可以簡(jiǎn)化為字符串前綴匹配,查詢(xún)性能提升一個(gè)數(shù)量級(jí)。

精度靈活

GeoHash 的長(zhǎng)度決定了定位精度:

  • 6 位(如 wx4g89):約 1 公里范圍,適合全城范圍的粗粒度篩選。
  • 7 位(如 wx4g89e):約 100 米范圍,適合最后一公里的精匹配。

這種靈活性避免了過(guò)度精確帶來(lái)的數(shù)據(jù)分散,同時(shí)兼顧效率與準(zhǔn)確性。

Redis 提供原生地理支持

Redis 內(nèi)置了 GEOADD、GEORADIUS 等命令,可以直接存儲(chǔ)騎手坐標(biāo)與執(zhí)行范圍查詢(xún)。結(jié)合 Hash 結(jié)構(gòu)存儲(chǔ) GeoHash → 騎手ID 的映射,可以輕松支撐 每秒十萬(wàn)次位置更新與查詢(xún)

解決邊界問(wèn)題

僅查詢(xún)單個(gè) GeoHash 區(qū)域會(huì)漏掉邊界騎手。通過(guò) 目標(biāo) GeoHash + 相鄰 8 個(gè) GeoHash 的策略,可以覆蓋訂單周邊區(qū)域,確保不會(huì)遺漏臨近騎手。

系統(tǒng)設(shè)計(jì)

整體架構(gòu)

系統(tǒng)分為四層:

  • 感知層:騎手端 APP 每 3 秒上傳位置;用戶(hù)端下單上傳收貨地址。
  • 接入層:SpringBoot 接收請(qǐng)求,校驗(yàn)參數(shù)。
  • 業(yè)務(wù)層:GeoHash 轉(zhuǎn)碼、派單計(jì)算邏輯。
  • 存儲(chǔ)層:Redis 保存騎手位置、GeoHash 映射、訂單狀態(tài)。

數(shù)據(jù)流程

騎手位置上報(bào)

  • APP → POST /rider/report
  • 轉(zhuǎn)換為 GeoHash,更新 Redis(GEO + Hash)。

訂單派單

  • 用戶(hù)下單 → POST /order/dispatch
  • 流程:

收貨地址 → GeoHash

獲取目標(biāo) + 相鄰 8 個(gè) GeoHash 下的騎手

計(jì)算距離,篩選 在線 + 未超載 + 3 公里內(nèi) 騎手

排序取 Top3,推送派單通知

數(shù)據(jù)模型

騎手位置模型

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;          // 經(jīng)度
    private double lat;          // 緯度
    private String geoHash;      // GeoHash
    private boolean online;      // 是否在線
    private int orderCount;      // 當(dāng)前接單量
}

訂單模型

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;     // 收貨經(jīng)度
    private double recvLat;     // 收貨緯度
    private String geoHash;     // 收貨地址的GeoHash
    private String assignedRider; // 分配的騎手ID
    private String status;      // 狀態(tài):待派單/已分配/完成
}

核心代碼實(shí)現(xiàn)

Service 層

騎手位置服務(wù)
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:";


    /**
     * 騎手位置上報(bào)
     */
    public void reportLocation(RiderLocation rider) {
        // GEO 存儲(chǔ)坐標(biāo)
        redisTemplate.opsForGeo().add(GEO_KEY,
                new RedisGeoCommands.GeoLocation<>(rider.getRiderId(),
                        new Point(rider.getLng(), rider.getLat())));


        // Hash 存儲(chǔ)附加信息
        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()));
    }


    /**
     * 根據(jù) geoHash 獲取騎手列表(簡(jiǎn)化)
     */
    public String[] getRidersByGeoHash(String geoHash) {
        // 實(shí)際場(chǎng)景可用 redis scan + hash 過(guò)濾,這里演示簡(jiǎn)化返回
        return new String[]{"rider1", "rider2"};
    }
}
派單服務(wù)
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;


    /**
     * 創(chuàng)建訂單并派單
     */
    public Order createAndDispatch(Order order) {
        // 1. 計(jì)算訂單GeoHash
        String orderGeoHash = GeoHashUtils.encode(order.getRecvLat(), order.getRecvLng(), geoHashPrecision);
        order.setGeoHash(orderGeoHash);
        order.setStatus("待派單");


        // 2. 查詢(xún)目標(biāo) GeoHash + 相鄰 8 個(gè)區(qū)域
        Set<String> candidates = new HashSet<>();
        for (String gh : GeoHashUtils.adjacent(orderGeoHash)) {
            candidates.addAll(Arrays.asList(riderLocationService.getRidersByGeoHash(gh)));
        }


        // 3. 簡(jiǎn)化:隨便取一個(gè)候選騎手
        String assignedRider = candidates.stream().findFirst().orElse(null);


        // 4. 更新訂單對(duì)象
        if (assignedRider != null) {
            order.setAssignedRider(assignedRider);
            order.setStatus("已分配");
        }
        return order;
    }
}

Controller 層

騎手位置上報(bào)接口

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 "騎手位置上報(bào)成功";
    }
}

派單接口

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);
    }
}

環(huán)境與配置

Redis 啟動(dòng)

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  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">外賣(mài)派單可視化</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="經(jīng)度"></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>

結(jié)論

外賣(mài)平臺(tái)的實(shí)時(shí)派單,本質(zhì)是一個(gè) 高頻寫(xiě)入 + 快速查詢(xún) + 高并發(fā) 的技術(shù)難題。傳統(tǒng)數(shù)據(jù)庫(kù)方案往往在查詢(xún)效率和并發(fā)控制上遇到瓶頸,而 SpringBoot + GeoHash + Redis 的組合恰好能在三方面實(shí)現(xiàn)突破:

  1. GeoHash 降維:空間查詢(xún)轉(zhuǎn)字符串匹配,效率提升十倍。
  2. Redis 高并發(fā):原生 GEO 命令確保百萬(wàn)級(jí)騎手位置實(shí)時(shí)更新。
  3. 邊界問(wèn)題解決:相鄰 GeoHash 查詢(xún)避免遺漏騎手。

這種方案不僅能保障外賣(mài)派單的實(shí)時(shí)性和準(zhǔn)確性,還具備 良好的可擴(kuò)展性,可支撐未來(lái)千萬(wàn)級(jí)訂單。對(duì)網(wǎng)約車(chē)調(diào)度、同城快遞分配等場(chǎng)景同樣適用。

責(zé)任編輯:武曉燕 來(lái)源: 路條編程
相關(guān)推薦

2025-05-20 09:00:04

SpringGeoHash派單

2020-09-11 06:57:31

系統(tǒng)外賣(mài)騎手

2016-08-05 18:25:31

2022-03-17 17:34:21

AI算法騎手

2022-06-30 14:07:10

分庫(kù)分表系統(tǒng)

2014-11-13 10:30:00

2023-08-31 22:17:15

JavaMySQLB+樹(shù)

2014-10-30 09:37:30

西部數(shù)據(jù)

2025-08-15 07:36:48

彈幕系統(tǒng)Spring

2025-06-26 04:10:00

2025-09-26 08:46:30

2025-06-19 02:11:00

2022-06-30 07:34:46

分庫(kù)分表外賣(mài)訂單系統(tǒng)

2020-12-03 09:00:02

Java外賣(mài)系統(tǒng)

2025-07-14 05:00:00

監(jiān)控系統(tǒng)工具

2025-09-08 07:48:01

SpringWebSocket監(jiān)控

2013-03-29 09:28:30

文件共享思杰NetApp

2012-09-21 14:00:30

紅帽Linux

2025-03-11 09:28:34

2014-10-27 18:13:02

ITSM
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)