微信附近的人,用redis也能實現?(GEO)
相信微信附近的人的功能大家都應該用過
我可以很隨意的通過我自己的定位能看到我附近的人,並且能看到那個人距離我的距離,大家有沒有思考過這個是怎麼實現的?
作為一個程式猿任何問題應該都有一個思考的過程,而不是直接看結論,接下來大家一步一步的思考,直到問題解決。
獲取自己的位置
附近的人其實就是一種位置的比對關係,所以第一步是得獲取自己的位置,一般位置都是用經緯度來表示,具體經緯度的獲取得依賴客戶端,作為咱們後端程式設計師直接接收引數就可以了,所以這一步重點是用經緯度來表示各個節點的位置,對經緯度不是很瞭解的朋友可以複習一下中學的地理知識。
用關係型資料庫(mysql)的方式解決問題
我們先把問題簡化,假如我附近的人都是不動的,也就是說他們的位置是固定的,按照咱們傳統的思路,就是把每個人的經緯度存起來,然後遍歷這些經緯度,我們可以通過某種方法獲取我和各個經緯度之間的距離,然後把相對於我距離在 5km 以內的使用者展示出來就可以了
具體實現如下
把每個人的經緯度存起來,儲存如下
user_id | longitude(經度) | latitude(緯度) |
---|---|---|
1 | 116.39 | 39.91 |
2 | 121.48 | 31.4 |
3 | 117.30 | 39.71 |
... | ... | ... |
遍歷資料,和自己對比,獲得每個人和自己的距離
把資料庫的所有記錄都遍歷一遍,把每一條記錄的經緯度和自己的經緯度做個對比,就能獲取到各個記錄離自己的距離。
如何根據兩個經緯度,獲取到這兩個點之間的距離我在網上招了個方法,大家可以參考下
/** * 求兩個已知經緯度之間的距離,單位為米 * * @param lng1 $ ,lng2 經度 * @param lat1 $ ,lat2 緯度 * @return float 距離,單位米 * @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為當前檢視附近的使用者
這樣就可以完美解決我們的問題了。