使用 Redis 和 Python 構建一個共用單車的應用程式

2018-11-14 11:21:00

學習如何使用 Redis 和 Python 構建一個位置感知的應用程式。

我經常出差。但不是一個汽車狂熱分子,所以當我有空閒時,我更喜歡在城市中散步或者騎單車。我參觀過的許多城市都有共用單車系統,你可以租個單車用幾個小時。大多數系統都有一個應用程式來幫助使用者定位和租用他們的單車,但對於像我這樣的使用者來說,在一個地方可以獲得可租賃的城市中所有單車的資訊會更有幫助。

為了解決這個問題並且展示開源的強大還有為 Web 應用程式新增位置感知的功能,我組合了可用的公開的共用單車資料、Python 程式語言以及開源的 Redis 記憶體資料結構服務,用來索引和查詢地理空間資料。

由此誕生的共用單車應用程式包含來自很多不同的共用系統的資料,包括紐約市的 Citi Bike 共用單車系統(LCTT 譯註:Citi Bike 是紐約市的一個私營公共單車系統。在 2013 年 5 月 27 日正式營運,是美國最大的公共單車系統。Citi Bike 的名稱有兩層意思。Citi 是計劃贊助商花旗銀行(CitiBank)的名字。同時,Citi 和英文中“城市(city)”一詞的讀音相同)。它利用了花旗單車系統提供的 通用共用單車資料流General Bikeshare Feed,並利用其資料演示了一些使用 Redis 地理空間資料索引的功能。 花旗單車資料可按照 花旗單車資料許可協定 提供。

通用共用單車資料流規範

通用共用單車資料流規範General Bikeshare Feed Specification(GBFS)是由 北美共用單車協會 開發的 開放資料規範,旨在使地圖程式和運輸程式更容易的將共用單車系統新增到對應平台中。 目前世界上有 60 多個不同的共用系統使用該規範。

Feed 流由幾個簡單的 JSON 資料檔案組成,其中包含系統狀態的資訊。 Feed 流以一個頂級 JSON 檔案開頭,其參照了子資料流的 URL:

{    "data": {        "en": {            "feeds": [                {                    "name": "system_information",                    "url": "https://gbfs.citibikenyc.com/gbfs/en/system_information.json"                },                {                    "name": "station_information",                    "url": "https://gbfs.citibikenyc.com/gbfs/en/station_information.json"                },                . . .            ]        }    },    "last_updated": 1506370010,    "ttl": 10}

第一步是使用 system_informationstation_information 的資料將共用單車站的資訊載入到 Redis 中。

system_information 提供系統 ID,系統 ID 是一個簡短編碼,可用於為 Redis 鍵名建立名稱空間。 GBFS 規範沒有指定系統 ID 的格式,但確保它是全域性唯一的。許多共用單車資料流使用諸如“coastbikeshare”,“boisegreenbike” 或者 “topekametro_bikes” 這樣的短名稱作為系統 ID。其他的使用常見的有地理縮寫,例如 NYC 或者 BA,並且使用通用唯一識別符號(UUID)。 這個共用單車應用程式使用該識別符號作為字首來為指定系統構造唯一鍵。

station_information 資料流提供組成整個系統的共用單車站的靜態資訊。車站由具有多個欄位的 JSON 物件表示。車站物件中有幾個必填欄位,用於提供物理單車站的 ID、名稱和位置。還有幾個可選欄位提供有用的資訊,例如最近的十字路口、可接受的付款方式。這是共用單車應用程式這一部分的主要資訊來源。

建立資料庫

我編寫了一個範例應用程式 loadstationdata.py,它模仿後端進程中從外部源載入資料時會發生什麼。

查詢共用單車站

GitHub 上 GBFS 倉庫中的 systems.csv 檔案開始載入共用單車資料。

倉庫中的 systems.csv 檔案提供已註冊的共用單車系統及可用的 GBFS 資料流的發現 URLdiscovery URL。 這個發現 URL 是處理共用單車資訊的起點。

load_station_data 程式獲取系統檔案中找到的每個發現 URL,並使用它來查詢兩個子資料流的 URL:系統資訊和車站資訊。 系統資訊提供提供了一條關鍵資訊:系統的唯一 ID。 (注意:系統 ID 也在 systems.csv 檔案中提供,但檔案中的某些識別符號與資料流中的識別符號不匹配,因此我總是從資料流中獲取識別符號。)系統上的詳細資訊,比如共用單車 URL、電話號碼和電子郵件, 可以在程式的後續版本中新增,因此使用 ${system_id}:system_info 這個鍵名將資料儲存在 Redis 中。

載入車站資料

車站資訊提供系統中每個車站的資料,包括該系統的位置。load_station_data 程式遍歷車站資料流中的每個車站,並使用 ${system_id}:station:${station_id} 形式的鍵名將每個車站的資料儲存到 Redis 中。 使用 GEOADD 命令將每個車站的位置新增到共用單車的地理空間索引中。

更新資料

在後續執行中,我不希望程式碼從 Redis 中刪除所有 Feed 資料並將其重新載入到空的 Redis 資料庫中,因此我仔細考慮了如何處理資料的原地更新。

程式碼首先載入所有需要系統在記憶體中處理的共用單車站的資訊資料集。 當載入了一個車站的資訊時,該站就會按照 Redis 鍵名從記憶體中的車站集合中刪除。 載入完所有車站資料後,我們就剩下一個包含該系統所有必須刪除的車站資料的集合。

程式疊代處理該資料集,並建立一個事務刪除車站的資訊,從地理空間索引中刪除該車站的鍵名,並從系統的車站列表中刪除該車站。

程式碼重點

範例程式碼中有一些值得注意的地方。 首先,使用 GEOADD 命令將所有資料項新增到地理空間索引中,而使用 ZREM 命令將其刪除。 由於地理空間型別的底層實現使用了有序集合,因此需要使用 ZREM 刪除資料項。 需要注意的是:為簡單起見,範例程式碼演示了如何在單個 Redis 節點工作; 為了在叢集環境中執行,需要重新構建事務塊。

如果你使用的是 Redis 4.0(或更高版本),則可以在程式碼中使用 DELETEHMSET 命令。 Redis 4.0 提供 UNLINK 命令作為 DELETE 命令的非同步版本的替代。 UNLINK 命令將從鍵空間中刪除鍵,但它會在另外的執行緒中回收記憶體。 在 Redis 4.0 中 HMSET 命令已經被棄用了而且 HSET 命令現在接收可變引數(即,它接受的引數個數不定)。

通知用戶端

處理結束時,會向依賴我們資料的用戶端傳送通知。 使用 Redis 發布/訂閱機制,通知將通過 geobike:station_changed 通道和系統 ID 一起發出。

資料模型

在 Redis 中構建資料時,最重要的考慮因素是如何查詢資訊。 共用單車程式需要支援的兩個主要查詢是:

  • 找到我們附近的車站
  • 顯示車站相關的資訊

Redis 提供了兩種主要資料型別用於儲存資料:雜湊和有序集。 雜湊型別很好地對映到表示車站的 JSON 物件;由於 Redis 雜湊不使用固定的資料結構,因此它們可用於儲存可變的車站資訊。

當然,在地理位置上尋找站點需要地理空間索引來搜尋相對於某些坐標的站點。 Redis 提供了幾個使用有序集資料結構構建地理空間索引的命令。

我們使用 ${system_id}:station:${station_id} 這種格式的鍵名儲存車站相關的資訊,使用 ${system_id}:stations:location 這種格式的鍵名查詢車站的地理空間索引。

獲取使用者位置

構建應用程式的下一步是確定使用者的當前位置。 大多數應用程式通過作業系統提供的內建服務來實現此目的。 作業系統可以基於裝置內建的 GPS 硬體為應用程式提供定位,或者從裝置的可用 WiFi 網路提供近似的定位。

查詢車站

找到使用者的位置後,下一步是找到附近的共用單車站。 Redis 的地理空間功能可以返回使用者當前坐標在給定距離內的所有車站資訊。 以下是使用 Redis 命令列介面的範例。

想象一下,我正在紐約市第五大道的蘋果零售店,我想要向市中心方向前往位於西 37 街的 MOOD 布料店,與我的好友 Swatch 相遇。 我可以坐出租車或地鐵,但我更喜歡騎單車。 附近有沒有我可以使用的單車共用站呢?

蘋果零售店位於 40.76384,-73.97297。 根據地圖顯示,在零售店 500 英尺半徑範圍內(地圖上方的藍色)有兩個單車站,分別是陸軍廣場中央公園南單車站和東 58 街麥迪遜單車站。

我可以使用 Redis 的 GEORADIUS 命令查詢 500 英尺半徑範圍內的車站的 NYC 系統索引:

127.0.0.1:6379> GEORADIUS NYC:stations:location -73.97297 40.76384 500 ft1) "NYC:station:3457"2) "NYC:station:281"

Redis 使用地理空間索引中的元素作為特定車站的後設資料的鍵名,返回在該半徑內找到的兩個共用單車站。 下一步是查詢兩個站的名稱:

127.0.0.1:6379> hget NYC:station:281 name"Grand Army Plaza & Central Park S"127.0.0.1:6379> hget NYC:station:3457 name"E 58 St & Madison Ave"

這些鍵名對應於上面地圖上標識的車站。 如果需要,可以在 GEORADIUS 命令中新增更多標誌來獲取元素列表,每個元素的坐標以及它們與當前點的距離:

127.0.0.1:6379> GEORADIUS NYC:stations:location -73.97297 40.76384 500 ft WITHDIST WITHCOORD ASC 1) 1) "NYC:station:281"   2) "289.1995"   3) 1) "-73.97371262311935425"      2) "40.76439830559216659"2) 1) "NYC:station:3457"   2) "383.1782"   3) 1) "-73.97209256887435913"      2) "40.76302702144496237"

查詢與這些鍵名關聯的名稱會生成一個我可以從中選擇的車站的有序列表。 Redis 不提供方向和路線的功能,因此我使用裝置作業系統的路線功能繪製從當前位置到所選單車站的路線。

GEORADIUS 函數可以很輕鬆的在你喜歡的開發框架的 API 裡實現,這樣就可以向應用程式新增位置功能了。

其他的查詢命令

除了 GEORADIUS 命令外,Redis 還提供了另外三個用於查詢索引資料的命令:GEOPOSGEODISTGEORADIUSBYMEMBER

GEOPOS 命令可以為 地理雜湊geohash 中的給定元素提供坐標(LCTT 譯註:geohash 是一種將二維的經緯度編碼為一位的字串的一種演算法,常用於基於距離的查詢演算法和推薦演算法)。 例如,如果我知道西 38 街 8 號有一個共用單車站,ID 是 523,那麼該站的元素名稱是 NYC:station:523。 使用 Redis,我可以找到該站的經度和緯度:

127.0.0.1:6379> geopos NYC:stations:location NYC:station:5231) 1) "-73.99138301610946655"   2) "40.75466497634030105"

GEODIST 命令提供兩個索引元素之間的距離。 如果我想找到陸軍廣場中央公園南單車站與東 58 街麥迪遜單車站之間的距離,我會使用以下命令:

127.0.0.1:6379> GEODIST NYC:stations:location NYC:station:281 NYC:station:3457 ft "671.4900"

最後,GEORADIUSBYMEMBER 命令與 GEORADIUS 命令類似,但該命令不是採用一組坐標,而是採用索引的另一個成員的名稱,並返回以該成員為中心的給定半徑內的所有成員。 要查詢陸軍廣場中央公園南單車站 1000 英尺範圍內的所有車站,請輸入以下內容:

127.0.0.1:6379> GEORADIUSBYMEMBER NYC:stations:location NYC:station:281 1000 ft WITHDIST1) 1) "NYC:station:281"   2) "0.0000"2) 1) "NYC:station:3132"   2) "793.4223"3) 1) "NYC:station:2006"   2) "911.9752"4) 1) "NYC:station:3136"   2) "940.3399"5) 1) "NYC:station:3457"   2) "671.4900"

雖然此範例側重於使用 Python 和 Redis 來解析資料並構建共用單車系統位置的索引,但可以很容易地衍生為定位餐館、公共交通或者是開發人員希望幫助使用者找到的任何其他型別的場所。

本文基於今年我在北卡羅來納州羅利市的開源 101 會議上的演講