背景
我們最近有個專案需求,實現面對面匹配好友功能。
預想的場景是兩個使用者面對面的時候可以很方便地通過搖一搖或者其它互動方式快速找到對方來達成好友。
關鍵點
後端服務基於GPS位置快速匹配附近的人。
方案
1、全量遍歷
第一時間想到的方式是直接計算使用者的距離,若使用者距離在要求的範圍之內,則這兩個使用者達成匹配。
問題
這個方案需要遍歷全量使用者,計算量很大,匹配的效率很低。
2、GeoHash
geohash的思想是將二維的經緯度轉換成一維的字串,geohash有以下三個特點:
- 字串越長,表示的範圍越精確。
- 字串相似的表示距離相近,利用字串的字首匹配,可以查詢附近的地理位置。這樣就實現了快速查詢某個座標附近的地理位置。
- geohash計算的字串,可以反向解碼出原來的經緯度。
geohash精度資訊如下,字串越長,表示的範圍越精確。
2.1、Redis GeoHash
Redis在3.2版本之後提供了geo地理位置的支援。我們可以很方便地通過Redis維護地理位置,快速查詢某個座標附近特定距離內的地理位置。
問題
在一般場景裡面商店的地理位置基本不變,資訊也是長期有效。所以通過Redis維護所有的地理位置也比較合適。可是在我們的需求場景,好友匹配是個瞬時場景。使用者的匹配請求本身是有時效性的,若直接使用Redis的GeoHash來維護地理位置還需要考慮資訊的刪除等情況,反而變得複雜。
2.2、Redis Hash+GeoHash
我們可以直接使用Redis的Hash結構來維護所有待匹配的使用者。field為使用者ID,value為使用者匹配相關的資訊,其中包含使用者座標對應的GeoHash。然後通過定時任務獲取所有待匹配的使用者資訊,根據geohash欄位排序,再計算相鄰使用者是否在要求的距離之內,若條件符合則可以達成匹配。
關鍵點
兩個使用者在彼此附近,那他們的geohash字串會很相似,在按geohash排序時,這兩個使用者資訊下標也會相鄰。
3、R樹
R樹是用來做空間資料儲存的樹狀資料結構。在外賣場景很多時候會使用R樹來維護商家的配送範圍,為使用者篩選出在其附近的商家。在本次需求R樹不適用。
問題
最終我們採用Redis Hash+GeoHash的方案,在實施過程中也有其它問題需要考慮。
1、使用者全量集合大
對使用者全量集合進行拆分,先按使用者城市來做一次分桶。大概率上只有同一個城市的使用者才可能出現在附近。另外使用者匹配請求有效時間通常比較短,所以經過分桶之後全量集合大小應該可控。
- 可以根據使用者GPS資訊或者IP地址來估算使用者所在城市;
- 可能會出現城市邊緣兩個附近的使用者一直沒辦法匹配上的情況,這種情況概率很低,可以忽略;
2、使用者集合的併發處理
在我們進行匹配任務的同時使用者可能會取消匹配。在進行匹配操作時,我們會獲取全量的使用者集,並刪除對應的全量集資料(通過pipeline方式實現原子性)。所以此時使用者取消匹配會失敗,簡化邏輯。若最後使用者沒有匹配上,再將相應的資訊批量回寫到使用者集合即可。
3、座標系不統一
在測試過程中發現Android端和iOS端會出現座標系不統一的情況,這種情況下因出現位置偏移導致無法匹配。可以通過座標系轉換的方式統一座標系。
4、GeoHash的問題
如下圖所示,圖片來自網路。邊緣附近的點,黃色的點要比黑色的點更加靠近紅點,但是由於黑點跟紅點的GeoHash字首匹配數目更多,因此得到黑點更加靠近紅點的結果 。一般的處理方式是通過篩選周圍8個區域內的所有點,然後計算距離得到滿足條件結果。
在我們的匹配定時任務裡面,GeoHash只是作為距離遠近的參考依據,理論上相鄰區域的使用者也會在相鄰下標位置,然後再通過計算距離判斷條件是否符合。
小結
我們通過GeoHash來實現面對面匹配功能。GeoHash將二維的經緯度轉換成一維的字串,我們再對一維的字串進行排序操作,這樣可以快速找到相鄰的使用者資訊。