【系統設計】鄰近服務

在本文中,我們將設計一個鄰近服務,用來發現用戶附近的地方,比如餐館,酒店,商場等。

設計要求

從一個小明去面試的故事開始。

面試官:你好,我想考察一下你的設計能力,如果讓你設計一個鄰近服務,用來搜索用戶附近的商家,你會怎麼做?

小明:好的,用戶可以指定搜索半徑嗎?如果搜索範圍內沒有足夠的商家,系統是否支持擴大搜索範圍?

面試官:對,用戶可以根據需要修改,大概有以下幾個選項,0.5km,1km,2km,5km,10km,20km。

小明:嗯,還有其他的系統要求嗎?

面試官:另外還需要考慮的是,系統的低延遲,高可用,和可擴展性,以及數據隱私。

小明:好的,瞭解了。

總結一下,需要做一個鄰近服務,可以根據用戶的位置(經度和緯度)以及搜索半徑返回附近的商家,半徑可以修改。因爲用戶的位置信息是敏感數據,我們可能需要遵守數據隱私保護法。

高層次設計

高層次設計圖如下所示,系統包括兩部分:基於位置的服務 (location-based service)LBS 和業務(bussiness)相關的服務。

讓我們來看看系統的每個組件。

負載均衡器

負載均衡器可以根據路由把流量分配給多個後端服務。

基於位置的服務 (LBS)

LBS 服務是系統的核心部分,通過位置和半徑尋找附近的商家。LBS 具有以下特點:

  • 沒有寫請求,但是有大量的查詢
  • QPS 很高,尤其是在密集地區的高峯時段。
  • 服務是無狀態的,支持水平擴展。

Business 服務

商戶創建,更新,刪除商家信息,以及用戶查看商家信息。

數據庫集羣

數據庫集羣可以使用主從配置,提升可用性和性能。數據首先保存到主數據庫,然後複製到從庫,主數據庫處理所有的寫入操作,多個從數據庫用於讀取操作。

接下來,我們具體討論位置服務 LBS 的實現。

1. 二維搜索

這種方法簡單,有效,根據用戶的位置和搜索半徑畫一個圓,然後找到圓圈內的所有商家,如下所示。

商家的緯度用 latitude 表示,經度用 longitude 表示。同樣的用戶的緯度和經度可以用 user_latitude 和 user_longitude 表示,半徑用 radius 表示。

上面的搜索過程可以翻譯成下面的僞 SQL 。

SELECT business_id, latitude, longitude,
FROM business
WHERE 
latitude >= (@user_latitude - radius) AND latitude < (@user_latitude + radius)
AND
longitude >= (@user_longitude - radius) AND longitude < (@user_longitude + radius)

這種方式可以實現我們的需求,但是實際上效率不高,因爲我們需要掃描整個表。雖然我們可以對經緯度創建索引,效率有提升,但是並不夠,我們還需要對索引的結果計算取並集。

2. Geohash

我們上面說了,二維的經度和緯度做索引的效果並不明顯。而 Geohash 可以把二維的經度和緯度轉換爲一維的字符串,通過算法,每增加一位就遞歸地把世界劃分爲越來越小的網格,讓我們來看看它是如何實現的。

首先,把地球通過本初子午線和赤道分成四個象限,如下

  • 緯度範圍 [-90, 0] 用 0 表示
  • 緯度範圍 [0, 90] 用 1 表示
  • 經度範圍 [-180, 0] 用 0 表示
  • 經度範圍 [0, 180] 用 1 表示

然後,再把每個網格分成四個小網格。

重複這個過程,直到網格的大小符合我們的需求,Geohash 通常使用 base32 表示。讓我們看兩個例子。

  • Google 總部的 Geohash(長度爲 6):

    1001 10110 01001 10000 11011 11010 (base32 convert) → 9q9hvu (base32)
    
  • Facebook 總部的 Geohash(長度 爲 6):

    1001 10110 01001 10001 10000 10111 (base32 convert) → 9q9jhr (base32)
    

Geohash 有 12 個精度(也稱爲級別), 它可以控制每個網格的大小,字符串越長,拆分的網格就越小,如下

實際中,按照具體的場景選擇合適的 Geohash 精度。

通過這種方式,最終把地圖分成了下面一個個小的網格,一個 Geohash 字符串就表示了一個網格,這樣查詢每個網格內的商家信息,搜索是非常高效的。

可能你已經發現了一些規律,上圖的每個網格中,它們都相同的前綴 wtw3。是的,Geohash 的特點是,兩個網格的相同前綴部分越長,就表示它們的位置是鄰近的。

反過來說,兩個相鄰的網格,它們的 Geohash 字符串一定是相似的嗎?

不一定,因爲存在 邊界問題。當兩個網格都在邊緣時,雖然它們是相鄰的,但是 Geohash 的值從第一位就不一樣,如下圖,兩個紫色的點相鄰。

下面是一個精度比較高的網格,有些相鄰網格的 Geohash 的值是完全不一樣的。

還有一個邊界問題是,對於用戶(橙色)來說,隔壁網格的商家(紫色)可能比自己網格的商家(紫色)的距離還要近,如下圖

所以,在查詢附近的商家時,不能只侷限於用戶所在的網格,要擴大到用戶相鄰的4個或者9個網格,然後再計算距離,進行篩選,最終找到距離合適的商家。

另外,當在用戶在偏遠的郊區時,我們可以按照下面的方式,擴大搜索範圍,返回足夠數量的商家。

Geohash 的使用非常廣泛的,另外 Redis 和 MongoDB 都提供了相應的功能,可以直接使用。

3 . 四叉樹

還有一種比較流行的解決方案是四叉樹,這種方法可以遞歸地把二維空間劃分爲四個象限,直到每個網格的商家數量都符合要求。

如下圖,比如確保每個網格的數量不超過10,如果超過,就拆分爲四個小的網格。

請注意,四叉樹是一種內存數據結構,它不是數據庫解決方案。它運行在每個LBS 服務上,數據結構是在服務啓動時構建的。

接下來,看一下節點都存儲了哪些信息?

內部節點

網格的左上角和右下角的座標,以及指向 4個 子節點的指針。

葉子節點

網格的左上角和右下角的座標,以及網格內的商家的 ID 數組。

現實世界的四叉樹示例

Yext 提供了一張圖片 ,顯示了其中一個城市構建的四叉樹。我們需要更小、更細粒度的網格用在密集區域,而更大的網格用在偏遠的郊區。

谷歌 S2 和 希爾伯特曲線

Google S2 庫是這個領域的另一個重要參與者,和四叉樹類似,它是一種內存解決方案。它基於希爾伯特曲線把球體映射到一維索引。

希爾伯特曲線 是一種能填充滿一個平面正方形的分形曲線(空間填充曲線),由大衛·希爾伯特在1891年提出,如下

希爾伯特曲線是怎麼生成的?

最簡單的一階希爾伯特曲線,先把正方形平均分成四個網格,然後從其中一個網格的正中心開始,按照方向,連接每一個網格。

二階的希爾伯特曲線, 每個網格都先生成一階希爾伯特曲線 , 然後把它們首尾相連。

三階的希爾伯特曲線

n階的希爾伯特曲線, 實現一條線連接整個平面。

同樣,希爾伯特曲線也可以填充整個三維空間。

希爾伯特曲線的一個重要特點是 降維,可以把多維空間轉換成一維數組,可以通過動畫看看它是如何實現的。

在一維空間上的搜索比在二維空間上的搜索效率高得多了。

多數據中心和高可用

我們可以把 LBS 服務部署到多個區域,不同地區的用戶連接到最近的數據中心,這樣做可以提升訪問速度以及系統的高可用,並根據實際的場景,進行擴展。

最終設計圖

  1. 用戶需要尋找附近 500 米的餐館。客戶端把用戶位置(經度和緯度),半徑(500m)發送給後端。
  2. 負載均衡器把請求轉發給 LBS。
  3. 基於用戶位置和半徑信息,LBS 找到與搜索匹配的 geohash 長度。
  4. LBS 計算相鄰的 Geohash 並將它們添加到列表中。
  5. 調用 Redis 服務獲取對應的商家 ID。
  6. LBS 根據返回的商家列表,計算用戶和商家之間的距離,並進行排名,然後返回給客戶端。

總結

在本文中,我們設計了一個鄰近服務,介紹了4種常見了實現方式,分別是二維搜索,Geohash, 四叉樹和 Google S2。它們有各自的優缺點,您可以根據實際的業務場景,選擇合適的實現。

Reference

https://halfrost.com/go_spatial_search/#toc-25

https://www.amazon.com/System-Design-Interview-Insiders-Guide/dp/1736049119

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章