附近商戶-GEO數據結構的基本用法
GEO就是Geolocation的簡寫形式,代表地理坐標
Redis在3.2版本中加入了對GEO的支持,允許存儲地理坐標信息,幫助我們根據經緯度來檢索數據。常見的命令有:
-
GEOADD:添加一個地理空間信息,包含:經度(longitude)、緯度(latitude)、值(member)
-
GEODIST:計算指定的兩個點之間的距離并返回
-
GEOHASH:將指定member的坐標轉為hash字符串形式并返回
-
GEOPOS:返回指定member的坐標
-
GEORADIUS:指定圓心、半徑,找到該圓內包含的所有member,并按照與圓心之間的距離排序后返回。6.以后已廢棄
-
GEOSEARCH:在指定范圍內搜索member,并按照與指定點之間的距離排序后返回。范圍可以是圓形或矩形。6.2.新功能
-
GEOSEARCHSTORE:與GEOSEARCH功能一致,不過可以把結果存儲到一個指定的key。 6.2.新功能
導入店鋪數據到GEO
當我們點擊美食之后,會出現一系列的商家,商家中可以按照多種排序方式,我們此時關注的是距離,這個地方就需要使用到我們的GEO,向后臺傳入當前app收集的地址(我們此處是寫死的) ,以當前坐標作為圓心,同時綁定相同的店家類型type,以及分頁信息,把這幾個條件傳入后臺,后臺查詢出對應的數據再返回。
我們把x軸坐標和y軸坐標當作score傳入到redis中,不要把所有數據都放到redis
同時把商戶類型做分類,相同類型的商戶作為一組,放入同一個GEO集合中
@Test
void loadShopData() {// 1.查詢店鋪信息List<Shop> list = shopService.list();// 2.把店鋪分組,按照typeId分組,typeId一致的放到一個集合Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));// 3.分批完成寫入Redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {// 3.1.獲取類型idLong typeId = entry.getKey();String key = SHOP_GEO_KEY + typeId;// 3.2.獲取同類型的店鋪的集合List<Shop> value = entry.getValue();List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());// 3.3.寫入redis GEOADD key 經度 緯度 memberfor (Shop shop : value) {// stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(), shop.getY())));}stringRedisTemplate.opsForGeo().add(key, locations);}
}
先查詢店鋪信息,從數據庫中獲取所有店鋪的信息,并將其存儲到List類型的變量list中
按店鋪類型分組,利用Stream API ,代碼將獲取的店鋪列表按typeId分組,相同typeId的店鋪會被歸于同一個列表,最終形成Map<Long,List>類型的映射map,其中鍵是typeId
,值是對應類型的店鋪列表。
對于每個類型的店鋪,先獲取當前類型的typeId,創建對應的Redis Key值
對于每個店鋪,創建一個RedisGeoCommands.GeoLocation對象,包括店鋪ID、經緯度等
將所有對象收集到一個列表,最后通過opsForGeo()批量寫入redis
附近商戶-實現附近商戶功能
// 1.判斷是否需要根據坐標查詢if (x == null || y == null) {// 不需要坐標查詢,按數據庫查詢Page<Shop> page = query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));// 返回數據return Result.ok(page.getRecords());}
先看有沒有傳坐標,沒有直接按數據庫查詢
// 3.查詢redis、按照距離排序、分頁。結果:shopId、distanceString key = SHOP_GEO_KEY + typeId;GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE.search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
若傳輸了坐標,就按距離排序后分頁
stringRedisTemplate.opsForGeo().search()
調用 Redis 的 GEOSEARCH 命令。key
:指定要搜索的地理位置數據集合。GeoReference.fromCoordinate(x, y)
:指定中心點坐標。new Distance(5000)
:設置搜索半徑為 5000 米。includeDistance().limit(end)
:要求返回距離信息,并限制最多返回end
條結果。
List<Long> ids = new ArrayList<>(list.size());Map<String, Distance> distanceMap = new HashMap<>(list.size());list.stream().skip(from).forEach(result -> {// 4.2.獲取店鋪idString shopIdStr = result.getContent().getName();ids.add(Long.valueOf(shopIdStr));// 4.3.獲取距離Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance);});
創建ids列表存放店鋪ID
常見distanceMap存儲店鋪ID和距離的映射關系
借助流中的skip(from)跳過前from個元素
然后對于每個元素獲取店鋪id和距離,添加到ids和distanceMap中
// 5.根據id查詢ShopString idStr = StrUtil.join(",", ids);List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();for (Shop shop : shops) {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}// 6.返回return Result.ok(shops);
將ids拼成字符串然后通過mybatis-plus提供的數據庫查詢操作得到店鋪,對于每一個店鋪,利用之前得到的distanceMap將位置信息賦值
總結
- Redis GEO 數據結構在 “附近商戶” 功能中核心作用是什么?
Redis GEO 用于存儲地理坐標信息(經度、緯度),并提供高效的地理位置查詢能力(如范圍搜索、距離計算),是實現 “按距離篩選附近商戶” 功能的核心存儲與查詢工具。 - 商戶數據導入 Redis GEO 的關鍵步驟是什么?
核心步驟包括:①從數據庫查詢所有商戶信息;②按商戶類型(typeId)分組,確保同類型商戶聚合;③將每組商戶的經緯度、ID 封裝為 GEO 坐標對象,批量寫入對應類型的 Redis GEO 集合(通過GEOADD命令)。 - 實現 “附近商戶” 查詢的核心流程是什么?
①接收用戶當前坐標、商戶類型、分頁參數;②通過 Redis GEO 的GEOSEARCH命令,以用戶坐標為中心,按指定半徑查詢目標類型商戶的 ID 及距離;③根據 ID 從數據庫查詢商戶詳細信息,并關聯 Redis 返回的距離數據;④返回帶距離的商戶列表。 - 為何要按商戶類型分組存儲 GEO 數據?
按類型分組可縮小查詢范圍,避免無關類型商戶參與計算,提升GEOSEARCH命令的執行效率;同時符合用戶 “按類型找商戶” 的實際業務場景,減少無效數據處理。 - 如何保證查詢結果中 “距離” 信息的準確性?
Redis GEO 在范圍查詢時通過includeDistance參數返回商戶與中心點的距離,查詢后將該距離與數據庫查詢的商戶信息一一映射(通過商戶 ID 關聯),確保距離與商戶信息精準匹配。