使用 Redis 和 Python 構建一個共享單車的應用程序
我經常出差。但不是一個汽車狂熱分子,所以當我有空閑時,我更喜歡在城市中散步或者騎單車。我參觀過的許多城市都有共享單車系統,你可以租個單車用幾個小時。大多數系統都有一個應用程序來幫助用戶定位和租用他們的單車,但對於像我這樣的用戶來說,在一個地方可以獲得可租賃的城市中所有單車的信息會更有幫助。
為了解決這個問題並且展示開源的強大還有為 Web 應用程序添加位置感知的功能,我組合了可用的公開的共享單車數據、Python 編程語言以及開源的 Redis 內存數據結構服務,用來索引和查詢地理空間數據。
由此誕生的共享單車應用程序包含來自很多不同的共享系統的數據,包括紐約市的 Citi Bike 共享單車系統(LCTT 譯註:Citi Bike 是紐約市的一個私營公共單車系統。在 2013 年 5 月 27 日正式營運,是美國最大的公共單車系統。Citi Bike 的名稱有兩層意思。Citi 是計劃贊助商花旗銀行(CitiBank)的名字。同時,Citi 和英文中「城市(city)」一詞的讀音相同)。它利用了花旗單車系統提供的 通用共享單車數據流 ,並利用其數據演示了一些使用 Redis 地理空間數據索引的功能。 花旗單車數據可按照 花旗單車數據許可協議 提供。
通用共享單車數據流規範
通用共享單車數據流規範 (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_information
和 station_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 數據流的 發現 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(或更高版本),則可以在代碼中使用 DELETE
和 HMSET
命令。 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 ft
1) "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 還提供了另外三個用於查詢索引數據的命令:GEOPOS
、GEODIST
和 GEORADIUSBYMEMBER
。
GEOPOS
命令可以為 地理哈希 中的給定元素提供坐標(LCTT 譯註:geohash 是一種將二維的經緯度編碼為一位的字元串的一種演算法,常用於基於距離的查找演算法和推薦演算法)。 例如,如果我知道西 38 街 8 號有一個共享單車站,ID 是 523,那麼該站的元素名稱是 NYC:station:523
。 使用 Redis,我可以找到該站的經度和緯度:
127.0.0.1:6379> geopos NYC:stations:location NYC:station:523
1) 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 WITHDIST
1) 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 會議上的演講。
via: https://opensource.com/article/18/2/building-bikesharing-application-open-source-tools
作者:Tague Griffith 譯者:Flowsnow 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive