SpringBoot與GeoHash整合,實(shí)現(xiàn)騎手的實(shí)時(shí)就近派單功能
作者:Java知識日歷
通過使用GeoHash結(jié)合Redis的地理空間功能,能夠?qū)崟r(shí)管理和查詢騎手的位置信息,并根據(jù)訂單量和評分等因素動(dòng)態(tài)分配最近的騎手來完成配送任務(wù)。
通過使用GeoHash結(jié)合Redis的地理空間功能,能夠?qū)崟r(shí)管理和查詢騎手的位置信息,并根據(jù)訂單量和評分等因素動(dòng)態(tài)分配最近的騎手來完成配送任務(wù)。
我們?yōu)槭裁词褂肎eoHash?
- 空間索引: GeoHash是一種將地理坐標(biāo)(經(jīng)緯度)編碼為字符串的算法,可以用于空間索引。 這使得我們可以方便地在Redis這樣的內(nèi)存數(shù)據(jù)庫中存儲和檢索地理位置數(shù)據(jù)。
- 快速篩選附近騎手: 通過GeoHash前綴匹配策略,可以在較短的時(shí)間內(nèi)篩選出一定范圍內(nèi)的騎手。 Redis提供了豐富的地理空間命令(如 GEOADD, GEORADIUS),可以直接利用這些命令來實(shí)現(xiàn)高效的地理空間查詢。
- 減少計(jì)算開銷: 相比于傳統(tǒng)的距離計(jì)算方法(如Haversine公式),GeoHash可以通過字符串比較來快速確定位置關(guān)系。 這減少了復(fù)雜的數(shù)學(xué)運(yùn)算,提高了查詢性能。
- 支持多級精度: GeoHash支持多種精度級別,可以根據(jù)需求調(diào)整搜索范圍。 例如,較長的GeoHash字符串表示更精確的位置,而較短的字符串則覆蓋更大的區(qū)域。這為我們提供了靈活性,可以根據(jù)不同的應(yīng)用場景調(diào)整查詢精度。
實(shí)現(xiàn)這個(gè)功能的關(guān)鍵點(diǎn)有哪些?
- 高效的位置存儲和查詢
- 動(dòng)態(tài)權(quán)重調(diào)整: 根據(jù)騎手當(dāng)前訂單量、歷史好評率計(jì)算派單優(yōu)先級
- 實(shí)時(shí)更新騎手位置,實(shí)時(shí)位置數(shù)據(jù)對于準(zhǔn)確派單至關(guān)重要。
代碼實(shí)操
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/><!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>delivery-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>delivery-system</name>
<description>Demo project for Spring Boot and GeoHash integration</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port=8080
spring.redis.host=localhost
spring.redis.port=6379
Redis Config
package com.example.deliverysystem.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
publicclass RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
騎手
package com.example.deliverysystem.model;
import lombok.Data;
@Data
publicclass Rider {
private String id; // 騎手ID
privatedouble latitude; // 緯度
privatedouble longitude; // 經(jīng)度
privateint orderCount; // 當(dāng)前訂單數(shù)量
privatedouble rating; // 歷史平均評分(0-5)
}
Repository
package com.example.deliverysystem.repository;
import com.example.deliverysystem.model.Rider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.GeoOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
publicclass RiderRepository {
privatefinal GeoOperations<String, String> geoOps;
@Autowired
public RiderRepository(RedisTemplate<String, String> redisTemplate) {
this.geoOps = redisTemplate.opsForGeo();
}
/**
* 添加騎手到Redis中的地理位置集合
*
* @param key Redis鍵名
* @param rider 騎手對象
*/
public void addRider(String key, Rider rider) {
Point point = new Point(rider.getLongitude(), rider.getLatitude());
geoOps.add(key, point, rider.getId());
}
/**
* 查找指定半徑范圍內(nèi)的騎手
*
* @param key Redis鍵名
* @param referencePoint 參考點(diǎn)(經(jīng)緯度)
* @param distance 半徑距離
* @return 附近的騎手列表
*/
public List<RedisGeoCommands.GeoResult<RedisGeoCommands.GeoLocation<String>>> findNearbyRiders(String key, Point referencePoint, Distance distance) {
Circle circle = new Circle(referencePoint, distance);
return geoOps.radius(key, circle);
}
/**
* 獲取騎手詳情
*
* @param results 附近騎手的結(jié)果列表
* @return 騎手詳情映射表
*/
public Map<Object, Object> getRiderDetails(List<RedisGeoCommands.GeoResult<RedisGeoCommands.GeoLocation<String>>> results) {
// 這里可以進(jìn)一步獲取每個(gè)騎手的詳細(xì)信息,例如從數(shù)據(jù)庫中讀取。
// 為了簡化示例,我們僅返回騎手ID列表。
return results.stream()
.collect(java.util.stream.Collectors.toMap(
result -> result.getContent().getName(),
result -> null));
}
}
Service
package com.example.deliverysystem.service;
import com.example.deliverysystem.model.Rider;
import com.example.deliverysystem.repository.RiderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@Service
publicclass DeliveryService {
privatefinal RiderRepository riderRepository;
@Autowired
public DeliveryService(RiderRepository riderRepository) {
this.riderRepository = riderRepository;
}
/**
* 添加騎手到系統(tǒng)
*
* @param key Redis鍵名
* @param rider 騎手對象
*/
public void addRider(String key, Rider rider) {
riderRepository.addRider(key, rider);
}
/**
* 查找最近的騎手
*
* @param key Redis鍵名
* @param latitude 目標(biāo)緯度
* @param longitude 目標(biāo)經(jīng)度
* @param radiusInKilometers 查詢半徑(公里)
* @return 最近的騎手列表,按優(yōu)先級排序
*/
public List<Rider> findNearestRiders(String key, double latitude, double longitude, double radiusInKilometers) {
Point referencePoint = new Point(longitude, latitude);
Distance radius = new Distance(radiusInKilometers, Metrics.KILOMETERS);
List<RedisGeoCommands.GeoResult<RedisGeoCommands.GeoLocation<String>>> nearbyRiders =
riderRepository.findNearbyRiders(key, referencePoint, radius);
Map<Object, Object> riderDetails = riderRepository.getRiderDetails(nearbyRiders);
// 假設(shè)這里可以根據(jù)騎手ID獲取其他屬性。
// 實(shí)際場景中,這些信息可能來自數(shù)據(jù)庫或其他數(shù)據(jù)源。
return nearbyRiders.stream()
.map(result -> {
Rider rider = new Rider();
rider.setId(result.getContent().getName());
// 根據(jù)實(shí)際需求獲取其他屬性。
return rider;
})
.sorted(Comparator.comparingDouble(this::calculatePriority))
.toList();
}
/**
* 計(jì)算騎手的派單優(yōu)先級
*
* @param rider 騎手對象
* @return 優(yōu)先級值(數(shù)值越大優(yōu)先級越高)
*/
private double calculatePriority(Rider rider) {
// 簡單的優(yōu)先級計(jì)算方法:較低的訂單量和較高的評分意味著更高的優(yōu)先級。
// 具體公式可以根據(jù)實(shí)際業(yè)務(wù)需求調(diào)整。
return -(rider.getOrderCount() * 1.0 / 5 + rider.getRating());
}
}
Controller
package com.example.deliverysystem.controller;
import com.example.deliverysystem.model.Rider;
import com.example.deliverysystem.service.DeliveryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/delivery")
publicclass DeliveryController {
privatefinal DeliveryService deliveryService;
@Autowired
public DeliveryController(DeliveryService deliveryService) {
this.deliveryService = deliveryService;
}
/**
* 添加新的騎手
*
* @param rider 新的騎手對象
*/
@PostMapping("/riders")
public void addRider(@RequestBody Rider rider) {
deliveryService.addRider("riders", rider);
}
/**
* 查找最近的騎手
*
* @param latitude 目標(biāo)緯度
* @param longitude 目標(biāo)經(jīng)度
* @param radiusInKilometers 查詢半徑(公里,默認(rèn)5公里)
* @return 最近的騎手列表
*/
@GetMapping("/nearest-rider/{latitude}/{longitude}")
public List<Rider> findNearestRiders(@PathVariable double latitude,
@PathVariable double longitude,
@RequestParam(defaultValue = "5") double radiusInKilometers) {
return deliveryService.findNearestRiders("riders", latitude, longitude, radiusInKilometers);
}
}
Application
package com.example.deliverysystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DeliverySystemApplication {
public static void main(String[] args) {
SpringApplication.run(DeliverySystemApplication.class, args);
}
}
測試
添加第一個(gè)騎手
curl -X POST http://localhost:8080/api/delivery/riders \
-H "Content-Type: application/json" \
-d '{"id": "rider1", "latitude": 39.9042, "longitude": 116.4074, "orderCount": 2, "rating": 4.5}'
Respons
空響應(yīng)(HTTP狀態(tài)碼200)
添加第二個(gè)騎手
curl -X POST http://localhost:8080/api/delivery/riders \
-H "Content-Type: application/json" \
-d '{"id": "rider2", "latitude": 39.9087, "longitude": 116.3975, "orderCount": 1, "rating": 4.8}'
Respons
空響應(yīng)(HTTP狀態(tài)碼200)
查詢最近的騎手
- 查詢位置為 (39.9063, 116.4039) 附近5公里范圍內(nèi)的騎手
curl -X GET "http://localhost:8080/api/delivery/nearest-rider/39.9063/116.4039"
Respons
[
{
"id": "rider1",
"latitude": 39.9042,
"longitude": 116.4074,
"orderCount": 2,
"rating": 4.5
},
{
"id": "rider2",
"latitude": 39.9087,
"longitude": 116.3975,
"orderCount": 1,
"rating": 4.8
}
]
責(zé)任編輯:武曉燕
來源:
Java知識日歷