1.背景
有一個營銷砍價活動,當用戶報名完成後,需要選擇去領取活動商品的門店,可選的門店列表需要按照距離當前用戶經緯度最近的3家門店進行距離排序,假設候選門店有10000家門店。具體demo如下圖:
2. 方案
2.1 思路
-
思路一:暴力解法。維持一個只有3個結點的小根堆,遍歷10000家門店即可,比如使用java的PriorityQueue隊列可實現。缺點是比較耗時,需要遍歷所有門店進行距離計算,然後進行排序。
-
思路二:使用GeoHash算法(這裏不做重點講解)來優化,首先在活動綁定門店表(包含經緯度信息)中新加一列表示當前的geohash值。然後從精度依次範圍查詢,比如精度爲GeoHash精度5位,即5KM(實際4.9KM)以內的門店,若匹配到3家門店立即返回,否則匹配精度4位的,直到匹配到精度爲1位的(這裏同一精度的門店任選3家即可,然後進行3家門店排序,舉個例子:比如匹配到5KM內的門店有20家,只取任意3家,且只對這3家進行排序即可)。
- 匹配geohash精度5位,即5KM(實際4.9KM)以內的門店
- 匹配geohash精度4位,即20KM(實際19.5KM)以內的門店
- 匹配geohash精度3位,即156M(實際156KM)以內的門店
- 匹配geohash精度2位,即624KM(實際624.1KM)以內的門店
- 匹配geohash精度1位,即5000KM(實際19.5KM)以內的門店
2.2 GeoHash實踐
2.2.1 活動綁定門店表增加geohash字段
id(自增,bigint) | shop_id(門店ID,bigint) | lon(經度,String) | lat(緯度,String) | geohash(String) |
---|---|---|---|---|
23 | 234232 | 119.466602 | 29.214044 | wthynw2b |
2.2.2 構造查詢SQL
SELECT * FROM t_cmc_act_child_shop
WHERE is_deleted = 0
AND act_id =990
AND geohash LIKE CONCAT(#{geoCode,jdbcType=VARCHAR}, '%');
limit 3
2.3 代碼
-
在每次創建活動時需要構造活動綁定門店表的geohash字段,構造過程代碼如下:
public static String getGeohashStringWithBitLen(String longitude, String latitude, int bitLen) { if(Strings.isNullOrEmpty(longitude) || Strings.isNullOrEmpty(latitude)) { return ""; } double longitudeValue, latitudeValue; try { longitudeValue = Double.valueOf(longitude); latitudeValue = Double.valueOf(latitude); return GeohashUtils.encodeLatLon(latitudeValue, longitudeValue, bitLen); } catch (Exception e) { log.error("[GeoHashUtils-getGeohashStringWithBitLen] 轉換 geohash 失敗", e); } return ""; }
-
依次根據精度進行查詢,直接選擇出最近3家門店即可返回,代碼如下:
private List<ActJoinChildShopDTO> doGetNearbyShopList(Long actId, String userLongitude, String userLatitude) { // 校驗省略 List<ActJoinChildShopDTO> allActJoinChildShopDs = Lists.newArrayList(); int precision = 5; for(; precision >= 1; precision--) { List<ActJoinChildShopDTO> tempActJoinChildShopDTOS = getNearbyShopList(precision, userLongitude, userLatitude, actIds); allActJoinChildShopDs.addAll(tempActJoinChildShopDTOS); if(allActJoinChildShopDs.size() >= 3) { // 按距離進行排序 allActJoinChildShopDs.sort(Comparator.comparing(ActJoinChildShopDTO::getShopDistance)); return allActJoinChildShopDs.subList(0, 3); } } return Lists.newArrayList(); } private List<ActJoinChildShopDTO> getNearbyShopList(int bitLength, String userLongitude, String userLatitude, Long actId) { String geohash = GeoHashUtils.getGeohashStringWithBitLen(userLongitude, userLatitude, bitLength); if(Strings.isNullOrEmpty(geohash)) { return Collections.emptyList(); } // 數據庫sql查詢 List<ActChildShopDTO> actChildShopDTOS = actChildShopRepository.getNearbyActChildShops(geohash, actIds); // 轉換結果並返回(其中需要根據經緯度計算距離) ...... }