相信微信附近的人的功能大家都應該用過
我可以很隨意的通過我自己的定位能看到我附近的人,並且能看到那個人距離我的距離,大家有沒有思考過這個是怎麼實現的?
作為一個程序猿任何問題應該都有一個思考的過程,而不是直接看結論,接下來大家一步一步的思考,直到問題解決。
獲取自己的位置
附近的人其實就是一種位置的比對關係,所以第一步是得獲取自己的位置,一般位置都是用經緯度來表示,具體經緯度的獲取得依賴客戶端,作為咱們後端程式設計師直接接收參數就可以了,所以這一步重點是用經緯度來表示各個節點的位置,對經緯度不是很了解的朋友可以複習一下中學的地理知識。
用關係型資料庫(mysql)的方式解決問題
我們先把問題簡化,假如我附近的人都是不動的,也就是說他們的位置是固定的,按照咱們傳統的思路,就是把每個人的經緯度存起來,然後遍歷這些經緯度,我們可以通過某種方法獲取我和各個經緯度之間的距離,然後把相對於我距離在 5km 以內的用戶展示出來就可以了
具體實現如下
把每個人的經緯度存起來,存儲如下
遍歷數據,和自己對比,獲得每個人和自己的距離
把資料庫的所有記錄都遍歷一遍,把每一條記錄的經緯度和自己的經緯度做個對比,就能獲取到各個記錄離自己的距離。
如何根據兩個經緯度,獲取到這兩個點之間的距離我在網上招了個方法,大家可以參考下
/**
* 求兩個已知經緯度之間的距離,單位為米
*
* @param lng1 $ ,lng2 經度
* @param lat1 $ ,lat2 緯度
* @returnfloat 距離,單位米
* @author www.Alixixi.com
*/
function getdistance($lng1, $lat1, $lng2, $lat2) {
// 將角度轉為狐度
$radLat1 = deg2rad($lat1); //deg2rad()函數將角度轉換為弧度
$radLat2 = deg2rad($lat2);
$radLng1 = deg2rad($lng1);
$radLng2 = deg2rad($lng2);
$a = $radLat1 - $radLat2;
$b = $radLng1 - $radLng2;
$s = 2 * asin(sqrt(pow(sin($a / 2), 2) + cos($radLat1) * cos($radLat2) * pow(sin($b / 2), 2))) * 6378.137 * 1000;
return$s;
}
篩選出距離和自己在 5km 以內的數據就是我們想得到的結果
把上次算出來的距離一一對比,在 5km 以內的數據就是我們需要的附近的人的數據。
用關係型資料庫(mysql)存在的問題
其實用 mysql 的方式表面上看著是可以解決問題的,其實不然
首先遍歷數據就是遍歷所有的數據,而且是在一個需要及時返回結果的接口中,這樣做是非常不科學的,用戶量非常多的話根本不現實遍歷完了之後還得繼續計算距離,這個數量級也是非常大的距離那些都弄完了還得再篩選一遍在附近的,又是一遍所有數據的遍歷如果符合附近的人的要求是需要按照距離從近到遠來排序,又得遍歷計算上述方式如果用戶量比較小其實是可以實現的,但是現在移動網際網路公司一般用戶體量都很大,全表遍歷的方式基本都可以 pass 掉,所以接下來我們來看一種新的方案,用 redis geo 的方式來實現
redis geo 介紹
首先我們需要注意的是,redis geo 是 3.2 版本才有的,所以需要用這個功能的朋友記得更新 redis 的版本
其實 redis geo 只有 6 個操作命令,知道這些命令基本思路就出來了
GEOADD:增加某個地理位置的坐標GEOPOS:獲取某個地理位置的坐標GEODIST:獲取兩個地理位置的距離GEORADIUS:根據給定地理位置坐標獲取指定範圍內的地理位置集合GEORADIUSBYMEMBER:根據給定地理位置獲取指定範圍內的地理位置集合GEOHASH:獲取某個地理位置的 geohash 值對於上面的命令,我們直接看例子吧,方便大家更深入的理解
redis> GEOADD nearbyPeople 13.36 38.11 "user_1" 15.08 37.50 "user_2"
(integer) 2
對於上面例子來說 相當於 nearbyPeople 是一個總的 key,user_1 和 user_2 是相當於 nearbyPeople 裡面的兩個元素以及他們對應的經緯度其實上述例子就是說把 user_1 和 user_2 的經緯度存在了 nearbyPeople 這個 key 中
redis> GEOPOS nearbyPeople user_1 user_2
1) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "15.08726745843887329"
2) "37.50266842333162032"
這個就比較簡單了,就是獲取 nearbyPeople 中的元素 user_1 和 user_2 這兩個元素的經緯度,當然如果之前沒有 geoadd 相對應元素的經緯度的話,會返回 nil
redis> GEODIST nearbyPeople user_1 user_2
"166274.1516"
redis> GEODIST nearbyPeople user_1 user_2 km
"166.2742"
redis> GEODIST nearbyPeople user_1 user_2 mi
"103.3182"
獲取 nearbyPeople 中 user_1 和 user_2 這兩個節點之間的距離,距離單位可以指定,如下所示
m :米,默認單位。km :千米。mi :英裡。ft :英尺。GEORADIUS 這個比較重要,也是比較核心的一個方法,參數也比較多,咱們來具體參照文檔說一說
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
參數說明:
m :米,默認單位。km :千米。mi :英裡。ft :英尺。WITHDIST: 在返回位置元素的同時, 將位置元素與中心之間的距離也一併返回。WITHCOORD: 將位置元素的經度和維度也一併返回。WITHHASH: 以 52 位有符號整數的形式, 返回位置元素經過原始 geohash 編碼的有序集合分值。 這個選項主要用於底層應用或者調試, 實際中的作用並不大。COUNT 限定返回的記錄數。ASC: 查找結果根據距離從近到遠排序。DESC: 查找結果根據從遠到近排序。
redis>GEORADIUS nearbyPeople 15 37 200 km WITHDIST
1) 1) "user_1"
2) "190.4424"
2) 1) "user_2"
2) "56.4413"
上述命令也就是說把 nearbyPeople 中的 距離經緯度(15,37)200km 以內的元素都找出來,而且帶上距離
GEORADIUSBYMEMBER 其實和 GEORADIUS 作用都一樣,唯一的區別在於
GEORADIUS 是以某個經緯度為基準點
GEORADIUSBYMEMBER 是以某個元素為基準點
用 redis geo 的方式解決問題
其實上述命令熟悉了的同學這個問題就很好解決了
首先我們可以在後臺把每個人的位置定時刷新到以 nearbyPeople 為 key 的 geo 對象中。
reids> GEOADD nearbyPeople 13.36 38.11 "user_1" 15.08 37.50 "user_2" .......
因為查看附近的人的位置信息也在 nearBy 中,所以顯然用 GEORADIUSBYMEMBER 比較合適
GEORADIUSBYMEMBER nearbyPeople user_n 5 km WITHDIST //user_n為當前查看附近的用戶
這樣就可以完美解決我們的問題了。