基于Geohash与优先队列的商户就近搜索方案
核心思路
通过Geohash将二维地理位置编码为一维字符串实现快速区域筛选,结合优先队列(堆排序)动态维护最近商户列表,平衡计算效率与准确性。
数据预处理阶段
商户数据需提前建立Geohash索引并存储。以MySQL为例,商户表结构设计如下:
CREATE TABLE merchants (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
longitude DOUBLE, -- 经度
latitude DOUBLE, -- 纬度
geohash CHAR(12) -- 9级Geohash约±19米精度
);
CREATE INDEX idx_geohash ON merchants(geohash);
使用Java生成Geohash(依赖commons-codec库):
public static String encodeGeohash(double lat, double lon, int precision) {
GeoHash geoHash = GeoHash.withCharacterPrecision(lat, lon, precision);
return geoHash.toBase32();
}
实时查询阶段
输入参数
用户坐标(userLat, userLon),搜索半径radius(单位:米),返回数量limit
步骤说明
- 计算候选Geohash块
根据半径确定所需Geohash精度级别(如半径500米对应7级编码),获取用户所在块及相邻8个块(Moore邻居):
List<String> getAdjacentGeohashes(String baseGeohash) {
GeoHash center = GeoHash.fromGeohashString(baseGeohash);
return Arrays.stream(center.getAdjacent())
.map(GeoHash::toBase32)
.collect(Collectors.toList());
}
- 数据库批量查询
使用IN语句查询所有候选块的商户:
SELECT id, name, latitude, longitude
FROM merchants
WHERE geohash IN (:geohashList)
- 精确距离计算与筛选
使用优先队列维护Top5最近商户,避免全量排序:
PriorityQueue<Merchant> queue = new PriorityQueue<>(
(a,b) -> Double.compare(b.distance, a.distance) // 最大堆
);
for (Merchant merchant : candidateMerchants) {
double dist = haversine(userLat, userLon, merchant.lat, merchant.lon);
if (dist <= radius) {
merchant.distance = dist;
queue.offer(merchant);
if (queue.size() > limit) queue.poll(); // 淘汰最远元素
}
}
距离计算公式(Haversine)
static double haversine(double lat1, double lon1, double lat2, double lon2) {
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); // 地球半径6371km
}
性能优化措施
-
多级Geohash缓存
建立不同精度的Geohash索引(如6-9级),根据查询半径自动选择最优级别 -
分布式计算
对于超大规模数据,采用Elasticsearch的geo_point类型或RedisGEO模块:
// Elasticsearch查询示例
SearchRequest request = new SearchRequest("merchants");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.geoDistanceQuery("location")
.point(userLat, userLon)
.distance(radius, DistanceUnit.METERS))
.size(limit);
request.source(sourceBuilder);
- 结果缓存
对高频查询坐标进行TTL缓存,减少重复计算
方案优势
-
时间复杂度可控
Geohash预过滤使距离计算量从O(N)降至O(1)~O(√N),优先队列筛选保持O(N logK) -
扩展性强
支持水平分片,可通过Geohash前缀实现数据分区 -
精度可调
Geohash级别与搜索半径动态匹配,避免过度计算
该方案在美团、饿了么等LBS系统中有成熟应用,经实际验证可支持万级QPS的附近商户查询需求。
278

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



