在咱們平時使用的許多app中有附近的人
這一功能,像微信、qq附近的人,哈羅、街兔附近的車輛。這些功能就在咱們平常生活中出現。html
像相似於附近的人這一類業務,在Java中是如何實現的呢?java
本文就簡單介紹下目前的幾種解決方案,並提供簡單的示例代碼git
注: 本文僅涉及附近的人
這一業務場景的解決方案討論,並未涉及到相關的技術細節和方案優化,各位看官能夠放心閱讀。github
<!-- more --> redis
目前業內的解決方案大都依據geoHash展開,考慮到不一樣的數據量以及不一樣的業務場景,本文主要討論如下3種方案算法
外接矩形
的實現方式是相對較爲簡單的一種方式。spring
假設給定某用戶的位置座標, 求在該用戶指定範圍內的其餘用戶信息sql
此時能夠將位置信息和距離範圍簡化成平面幾何題來求解數據庫
以當前用戶爲圓心,以給定距離爲半徑畫圓,那麼在這個圓內的全部用戶信息就是符合結果的信息,直接檢索圓內的用戶座標難以實現,咱們能夠經過獲取這個圓的外接正方形
。json
經過外接正方形,獲取經度和緯度的最大最小值
,根據最大最小值能夠將座標在正方形內的用戶信息搜索出來。
此時在外接正方形中不屬於圓形區域的部分就屬於多餘的部分,這部分用戶信息距離當前用戶(圓心)的距離一定是大於給定半徑的,故能夠將其剔除,最終得到指定範圍內的附近的人
<img src="https://cdn.jsdelivr.net/gh/larscheng/myImg/blogImg/nearbysearch/20191210200831.png" style="zoom:67%;" />
這裏只貼出部分核心代碼,詳細的代碼可見源碼:NearBySearch
在實現附近的人搜索中,須要根據位置經緯度點,進行一些距離和範圍的計算,好比求球面外接正方形的座標點,球面兩座標點的距離等,能夠引入Spatial4j庫。
<dependency> <groupId>com.spatial4j</groupId> <artifactId>spatial4j</artifactId> <version>0.5</version> </dependency>
user
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名稱', `longitude` double DEFAULT NULL COMMENT '經度', `latitude` double DEFAULT NULL COMMENT '緯度', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '建立時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
private SpatialContext spatialContext = SpatialContext.GEO; /** * 獲取附近x米的人 * * @param distance 距離範圍 單位km * @param userLng 當前經度 * @param userLat 當前緯度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.獲取外接正方形 Rectangle rectangle = getRectangle(distance, userLng, userLat); //2.獲取位置在正方形內的全部用戶 List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY()); //3.剔除半徑超過指定距離的多餘用戶 users = users.stream() .filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); } private Rectangle getRectangle(double distance, double userLng, double userLat) { return spatialContext.getDistCalc() .calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat), distance * DistanceUtils.KM_TO_DEG, spatialContext, null); }
<select id="selectUser" resultMap="BaseResultMap"> SELECT * FROM user WHERE 1=1 and (longitude BETWEEN ${minlng} AND ${maxlng}) and (latitude BETWEEN ${minlat} AND ${maxlat}) </select>
前面介紹了經過Mysql存儲用戶的信息和gps座標,經過計算外接正方形的座標點來粗略篩選結果集,最終剔除超過範圍的用戶。
而如今要提到的
Mysql+geohash
方案,一樣是以Mysql爲基礎,只不過引入了geohash算法,同時在查詢上藉助索引。
geohash被普遍應用於位置搜索類的業務中,本文不對它進行展開說明,有興趣的同窗能夠看一下這篇博客:[GeoHash核心原理解析],這裏簡單對它作一個描述:
GeoHash算法將經緯度座標點編碼成一個字符串,距離越近的座標,轉換後的geohash字符串越類似
,例以下表數據:
用戶 | 經緯度 | Geohash字符串 |
---|---|---|
小明 | 116.402843,39.999375 | wx4g8c9v |
小華 | 116.3967,39.99932 | wx4g89tk |
小張 | 116.40382,39.918118 | wx4g0ffe |
其中根據經緯度計算獲得的geohash字符串,不一樣精度(字符串長度)表明了不一樣的距離偏差。具體的不一樣精度的距離偏差可參考下表:
geohash碼長度 | 寬度 | 高度 |
---|---|---|
1 | 5,009.4km | 4,992.6km |
2 | 1,252.3km | 624.1km |
3 | 156.5km | 156km |
4 | 39.1km | 19.5km |
5 | 4.9km | 4.9km |
6 | 1.2km | 609.4m |
7 | 152.9m | 152.4m |
8 | 38.2m | 19m |
9 | 4.8m | 4.8m |
10 | 1.2m | 59.5cm |
11 | 14.9cm | 14.9cm |
12 | 3.7cm | 1.9cm |
使用Mysql存儲用戶信息,其中包括用戶的經緯度信息和geohash字符串。
WHERE geohash Like 'geohashcode%'
來查詢數據集這裏只貼出部分核心代碼,詳細的代碼可見源碼:NearBySearch
一樣的要涉及到座標點的計算和geohash的計算,開始以前先導入spatial4j
user_geohash
,給geohash碼添加索引CREATE TABLE `user_geohash` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名稱', `longitude` double DEFAULT NULL COMMENT '經度', `latitude` double DEFAULT NULL COMMENT '緯度', `geo_code` varchar(64) DEFAULT NULL COMMENT '經緯度所計算的geohash碼', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '建立時間', PRIMARY KEY (`id`), KEY `index_geo_hash` (`geo_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
private SpatialContext spatialContext = SpatialContext.GEO; /*** * 添加用戶 * @return */ @PostMapping("/addUser") public boolean add(@RequestBody UserGeohash user) { //默認精度12位 String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude()); return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now())); } /** * 獲取附近指定範圍的人 * * @param distance 距離範圍 單位km * @param len geoHash的精度 * @param userLng 當前經度 * @param userLat 當前緯度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("len") int len, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.根據要求的範圍,肯定geoHash碼的精度,獲取到當前用戶座標的geoHash碼 String geoHashCode = GeohashUtils.encodeLatLon(userLat, userLng, len); QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>() .likeRight("geo_code",geoHashCode); //2.匹配指定精度的geoHash碼 List<UserGeohash> users = userGeohashService.list(queryWrapper); //3.過濾超出距離的 users = users.stream() .filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); } /*** * 球面中,兩點間的距離 * @param longitude 經度1 * @param latitude 緯度1 * @param userLng 經度2 * @param userLat 緯度2 * @return 返回距離,單位km */ private double getDistance(Double longitude, Double latitude, double userLng, double userLat) { return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat), spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM; }
經過上面幾步,就能夠實現這一業務場景,不只提升了查詢效率,而且保護了用戶的隱私,不對外暴露座標位置。而且對於同一位置的頻繁請求,若是是同一個geohash字符串,能夠加上緩存,減緩數據庫的壓力。
geohash算法將地圖分爲一個個矩形,對每一個矩形進行編碼,獲得geohash碼,可是當前點與待搜索點距離很近可是剛好在兩個區域
,用上面的方法則就不適用了。
解決這一問題的辦法:獲取當前點所在區域附近的8個區域的geohash碼,一併進行篩選。
如何求解附近的8個區域的geohash碼
可參考Geohash求當前區域周圍8個區域編碼的一種思路
瞭解了思路,這裏咱們可使用第三方開源庫ch.hsr.geohash
來計算,經過maven引入
<dependency> <groupId>ch.hsr</groupId> <artifactId>geohash</artifactId> <version>1.0.10</version> </dependency>
對上一章節的nearBySearch
方法進行修改以下:
/** * 獲取附近指定範圍的人 * * @param distance 距離範圍 單位km * @param len geoHash的精度 * @param userLng 當前經度 * @param userLat 當前緯度 * @return json */ @GetMapping("/nearby") public String nearBySearch(@RequestParam("distance") double distance, @RequestParam("len") int len, @RequestParam("userLng") double userLng, @RequestParam("userLat") double userLat) { //1.根據要求的範圍,肯定geoHash碼的精度,獲取到當前用戶座標的geoHash碼 GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len); //2.獲取到用戶周邊8個方位的geoHash碼 GeoHash[] adjacent = geoHash.getAdjacent(); QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>() .likeRight("geo_code",geoHash.toBase32()); Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32())); //3.匹配指定精度的geoHash碼 List<UserGeohash> users = userGeohashService.list(queryWrapper); //4.過濾超出距離的 users = users.stream() .filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance) .collect(Collectors.toList()); return JSON.toJSONString(users); }
基於前兩種方案,咱們能夠發現gps這類數據屬於讀多寫少
的狀況,若是使用redis來實現附近的人,想必效率會大大提升。
Redis提供6條命令,來幫助咱們我完成大部分業務的需求,關於Redis提供的geohash操做命令介紹可閱讀博客:Redis 究竟是怎麼實現「附近的人」這個功能的呢?
本文主要介紹下,咱們示例代碼中用到的兩個命令:
GEOADD key longitude latitude member
:將給定的空間元素(緯度、經度、名字)添加到指定的鍵裏面
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
: 根據給定地理位置座標獲取指定範圍內的地理位置集合(附近的人)
GEOADD
),redis會將經緯度參數值轉換爲52位的geohash碼,GEORADIUS
命令,獲取指定座標點某一範圍內的數據這裏只貼出部分核心代碼,詳細的代碼可見源碼:NearBySearch
@Autowired private RedisTemplate<String, Object> redisTemplate; //GEO相關命令用到的KEY private final static String KEY = "user_info"; public boolean save(User user) { Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>( user.getName(), new Point(user.getLongitude(), user.getLatitude())) ); return flag != null && flag > 0; } /** * 根據當前位置獲取附近指定範圍內的用戶 * @param distance 指定範圍 單位km ,可根據{@link org.springframework.data.geo.Metrics} 進行設置 * @param userLng 用戶經度 * @param userLat 用戶緯度 * @return */ public String nearBySearch(double distance, double userLng, double userLat) { List<User> users = new ArrayList<>(); // 1.GEORADIUS獲取附近範圍內的信息 GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut = redisTemplate.opsForGeo().radius(KEY, new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)), RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs() .includeDistance() .includeCoordinates().sortAscending()); //2.收集信息,存入list List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent(); //3.過濾掉超過距離的數據 content.forEach(a-> users.add( new User().setDistance(a.getDistance().getValue()) .setLatitude(a.getContent().getPoint().getX()) .setLongitude(a.getContent().getPoint().getY()))); return JSON.toJSONString(users); }
方案 | 優點 | 缺點 |
---|---|---|
Mysql外接正方形 | 邏輯清晰,實現簡單,支持多條件篩選 | 效率較低,不適合大數據量,不支持按距離排序 |
Mysql+Geohash | 藉助索引有效提升效率,支持多條件篩選 | 不支持按距離排序,存在數據庫瓶頸 |
Redis+Geohash | 效率高,集成便捷,支持距離排序 | 不適合複雜對象存儲,不支持多條件查詢 |
總結以上三種方案,各有優劣,在不一樣的業務場景下,可選擇不一樣的方案來實現。
固然目前附近的人的解決方案並不只僅這三種,以上權當是這一功能的入門引子,但願對你們有所幫助。
本文的三種方案均有源碼提供,源碼地址
原文出處:https://www.cnblogs.com/larscheng/p/12063409.html