1. 程式人生 > 實用技巧 >微信附近的人,用redis也能實現?(GEO)

微信附近的人,用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 個操作命令,知道這些命令基本思路就出來了

  1. GEOADD:增加某個地理位置的座標
  2. GEOPOS:獲取某個地理位置的座標
  3. GEODIST:獲取兩個地理位置的距離
  4. GEORADIUS:根據給定地理位置座標獲取指定範圍內的地理位置集合
  5. GEORADIUSBYMEMBER:根據給定地理位置獲取指定範圍內的地理位置集合
  6. 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為當前檢視附近的使用者

這樣就可以完美解決我們的問題了。