面试知识库

09 - GEO 附近商户#

知识图谱#

Redis GEO 原理#

Redis GEO 底层使用 ZSet 实现:

  • Member: shopId
  • Score: GeoHash 编码值(将经纬度映射为一维整数)
GeoHash 原理:
  经度(-180~180) → 二进制编码 → 交叉合并 → 52位整数
  纬度(-90~90)   → 二进制编码 ↗

  相邻位置的 GeoHash 值接近 → ZSet 中相邻 → 范围查询高效
plaintext

核心代码走读#

文件: src/main/java/com/hmdp/service/impl/ShopServiceImpl.java

数据预热#

商铺 GEO 数据需要提前导入 Redis:

// 按 typeId 分组,批量 GEOADD
// GEOADD shop:geo:1 116.397128 39.916527 "shopId1"
// GEOADD shop:geo:1 116.405285 39.904989 "shopId2"
stringRedisTemplate.opsForGeo().add(key, new Point(x, y), shopId);
java

面试 Q&A#

Q1: 为什么用 Redis GEO 而不是 MySQL 的空间函数?#

性能差异巨大:

  • MySQL ST_Distance:需要全表扫描或空间索引,QPS 有限(~千级)
  • Redis GEOSEARCH:基于 GeoHash + ZSet,O(N+logN) 复杂度,QPS 可达万级

对于高频的「附近商户」查询,Redis GEO 是更好的选择。

追问:Redis GEO 的底层数据结构是什么?#

底层是 ZSet(跳表 + 压缩列表)。经纬度通过 GeoHash 算法编码为 52 位整数作为 score。相邻地理位置的 GeoHash 值相近,因此 ZSet 的范围查询天然支持空间邻近查询。

再追问:GeoHash 有什么缺陷?#

边界问题:两个物理上相邻的位置,如果恰好跨越 GeoHash 网格边界,它们的 GeoHash 值可能差距很大。Redis 内部通过搜索目标网格及其 8 个相邻网格来解决这个问题,但使用者需要了解这个特性。

Q2: 分页是怎么实现的?#

Redis GEOSEARCH 的 COUNT 参数只支持 LIMIT(返回前 N 条),不支持 OFFSET

所以采用了 查多跳前 策略:

  1. 查询前 end 条结果(limit(end)
  2. 在 Java 中 skip(from) 跳过前页数据
  3. from ~ end 之间的数据作为当前页

追问:这种分页方式有什么问题?#

深分页性能差:第 100 页需要查出前 100 × pageSize 条数据,大部分被丢弃。

优化方案:

  1. 缩小搜索半径:随着页数增加,前端可以引导用户缩小范围
  2. 游标分页:记录上一页最远距离,下一页以此为起点
  3. 限制最大页数:大多数 LBS 应用只展示前几页

Q3: ORDER BY FIELD 是什么?为什么需要它?#

MySQL 的 IN 查询不保证返回顺序与 IN 列表一致。例如 WHERE id IN (3, 1, 2) 可能返回顺序为 1, 2, 3

ORDER BY FIELD(id, 3, 1, 2) 强制按指定顺序排列结果,保持 Redis GEO 返回的距离排序。

这个技巧在项目中多处使用:Feed 流查询、点赞 Top5 查询都用了相同模式。

追问:ORDER BY FIELD 有 SQL 注入风险吗?#

当前代码直接拼接 idStr 到 SQL 中(last("ORDER BY FIELD(id," + idStr + ")")),理论上有注入风险。但因为 ids 来源于 Redis GEO 结果(shopId),不是用户直接输入,风险较低。

更安全的做法是使用参数化查询或在 Java 层面做结果排序。


踩坑点#

踩坑点说明面试官考察意图
GEO 无 OFFSET只能查多跳前,深分页性能差LBS 场景的分页设计
distanceMap 手动维护GEO 返回的 ID 和 DB 查询需要手动关联距离跨数据源数据组装
数据预热GEO 数据需要提前导入,新增商铺需同步更新数据一致性
5km 硬编码搜索半径固定 5000m,不可配置可配置性设计
ORDER BY FIELD 拼接存在 SQL 注入风险(虽然输入来自内部)安全意识

加分回答#

  • 对比其他 LBS 方案:Elasticsearch Geo Query(支持更丰富的空间查询)、PostGIS、MongoDB 2d索引
  • 提到 GeoHash 精度与搜索效率的关系(Redis 默认使用 52 位精度,约 0.6m 误差)
  • 分析 GEOSEARCH 的时间复杂度:O(N+logM),N 是范围内结果数,M 是 ZSet 总元素数
  • 提到商铺数据更新时需要同步更新 GEO 索引(当前代码只更新了缓存,没更新 GEO)

关联文档#