使用Redis和Python構建一個共享單車的應用程序
學習如何使用 Redis 和 Python 構建一個位置感知的應用程序。
我經(jīng)常出差。但不是一個汽車狂熱分子,所以當我有空閑時,我更喜歡在城市中散步或者騎單車。我參觀過的許多城市都有共享單車系統(tǒng),你可以租個單車用幾個小時。大多數(shù)系統(tǒng)都有一個應用程序來幫助用戶定位和租用他們的單車,但對于像我這樣的用戶來說,在一個地方可以獲得可租賃的城市中所有單車的信息會更有幫助。
為了解決這個問題并且展示開源的強大還有為 Web 應用程序添加位置感知的功能,我組合了可用的公開的共享單車數(shù)據(jù)、Python 編程語言以及開源的 Redis 內(nèi)存數(shù)據(jù)結構服務,用來索引和查詢地理空間數(shù)據(jù)。
由此誕生的共享單車應用程序包含來自很多不同的共享系統(tǒng)的數(shù)據(jù),包括紐約市的 Citi Bike 共享單車系統(tǒng)(LCTT 譯注:Citi Bike 是紐約市的一個私營公共單車系統(tǒng)。在 2013 年 5 月 27 日正式營運,是美國***的公共單車系統(tǒng)。Citi Bike 的名稱有兩層意思。Citi 是計劃贊助商花旗銀行(CitiBank)的名字。同時,Citi 和英文中“城市(city)”一詞的讀音相同)。它利用了花旗單車系統(tǒng)提供的 通用共享單車數(shù)據(jù)流,并利用其數(shù)據(jù)演示了一些使用 Redis 地理空間數(shù)據(jù)索引的功能。 花旗單車數(shù)據(jù)可按照 花旗單車數(shù)據(jù)許可協(xié)議 提供。
通用共享單車數(shù)據(jù)流規(guī)范
通用共享單車數(shù)據(jù)流規(guī)范(GBFS)是由 北美共享單車協(xié)會 開發(fā)的 開放數(shù)據(jù)規(guī)范,旨在使地圖程序和運輸程序更容易的將共享單車系統(tǒng)添加到對應平臺中。 目前世界上有 60 多個不同的共享系統(tǒng)使用該規(guī)范。
Feed 流由幾個簡單的 JSON 數(shù)據(jù)文件組成,其中包含系統(tǒng)狀態(tài)的信息。 Feed 流以一個*** JSON 文件開頭,其引用了子數(shù)據(jù)流的 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
的數(shù)據(jù)將共享單車站的信息加載到 Redis 中。
system_information
提供系統(tǒng) ID,系統(tǒng) ID 是一個簡短編碼,可用于為 Redis 鍵名創(chuàng)建命名空間。 GBFS 規(guī)范沒有指定系統(tǒng) ID 的格式,但確保它是全局唯一的。許多共享單車數(shù)據(jù)流使用諸如“coastbikeshare”,“boisegreenbike” 或者 “topekametro_bikes” 這樣的短名稱作為系統(tǒng) ID。其他的使用常見的有地理縮寫,例如 NYC 或者 BA,并且使用通用唯一標識符(UUID)。 這個共享單車應用程序使用該標識符作為前綴來為指定系統(tǒng)構造唯一鍵。
station_information
數(shù)據(jù)流提供組成整個系統(tǒng)的共享單車站的靜態(tài)信息。車站由具有多個字段的 JSON 對象表示。車站對象中有幾個必填字段,用于提供物理單車站的 ID、名稱和位置。還有幾個可選字段提供有用的信息,例如最近的十字路口、可接受的付款方式。這是共享單車應用程序這一部分的主要信息來源。
建立數(shù)據(jù)庫
我編寫了一個示例應用程序 loadstationdata.py,它模仿后端進程中從外部源加載數(shù)據(jù)時會發(fā)生什么。
查找共享單車站
從 GitHub 上 GBFS 倉庫中的 systems.csv 文件開始加載共享單車數(shù)據(jù)。
倉庫中的 systems.csv 文件提供已注冊的共享單車系統(tǒng)及可用的 GBFS 數(shù)據(jù)流的發(fā)現(xiàn) URL。 這個發(fā)現(xiàn) URL 是處理共享單車信息的起點。
load_station_data
程序獲取系統(tǒng)文件中找到的每個發(fā)現(xiàn) URL,并使用它來查找兩個子數(shù)據(jù)流的 URL:系統(tǒng)信息和車站信息。 系統(tǒng)信息提供提供了一條關鍵信息:系統(tǒng)的唯一 ID。 (注意:系統(tǒng) ID 也在 systems.csv
文件中提供,但文件中的某些標識符與數(shù)據(jù)流中的標識符不匹配,因此我總是從數(shù)據(jù)流中獲取標識符。)系統(tǒng)上的詳細信息,比如共享單車 URL、電話號碼和電子郵件, 可以在程序的后續(xù)版本中添加,因此使用 ${system_id}:system_info
這個鍵名將數(shù)據(jù)存儲在 Redis 中。
載入車站數(shù)據(jù)
車站信息提供系統(tǒng)中每個車站的數(shù)據(jù),包括該系統(tǒng)的位置。load_station_data
程序遍歷車站數(shù)據(jù)流中的每個車站,并使用 ${system_id}:station:${station_id}
形式的鍵名將每個車站的數(shù)據(jù)存儲到 Redis 中。 使用 GEOADD
命令將每個車站的位置添加到共享單車的地理空間索引中。
更新數(shù)據(jù)
在后續(xù)運行中,我不希望代碼從 Redis 中刪除所有 Feed 數(shù)據(jù)并將其重新加載到空的 Redis 數(shù)據(jù)庫中,因此我仔細考慮了如何處理數(shù)據(jù)的原地更新。
代碼首先加載所有需要系統(tǒng)在內(nèi)存中處理的共享單車站的信息數(shù)據(jù)集。 當加載了一個車站的信息時,該站就會按照 Redis 鍵名從內(nèi)存中的車站集合中刪除。 加載完所有車站數(shù)據(jù)后,我們就剩下一個包含該系統(tǒng)所有必須刪除的車站數(shù)據(jù)的集合。
程序迭代處理該數(shù)據(jù)集,并創(chuàng)建一個事務刪除車站的信息,從地理空間索引中刪除該車站的鍵名,并從系統(tǒng)的車站列表中刪除該車站。
代碼重點
在示例代碼中有一些值得注意的地方。 首先,使用 GEOADD
命令將所有數(shù)據(jù)項添加到地理空間索引中,而使用 ZREM
命令將其刪除。 由于地理空間類型的底層實現(xiàn)使用了有序集合,因此需要使用 ZREM 刪除數(shù)據(jù)項。 需要注意的是:為簡單起見,示例代碼演示了如何在單個 Redis 節(jié)點工作; 為了在集群環(huán)境中運行,需要重新構建事務塊。
如果你使用的是 Redis 4.0(或更高版本),則可以在代碼中使用 DELETE
和 HMSET
命令。 Redis 4.0 提供 UNLINK
命令作為 DELETE
命令的異步版本的替代。 UNLINK
命令將從鍵空間中刪除鍵,但它會在另外的線程中回收內(nèi)存。 在 Redis 4.0 中 HMSET 命令已經(jīng)被棄用了而且 HSET 命令現(xiàn)在接收可變參數(shù)(即,它接受的參數(shù)個數(shù)不定)。
通知客戶端
處理結束時,會向依賴我們數(shù)據(jù)的客戶端發(fā)送通知。 使用 Redis 發(fā)布/訂閱機制,通知將通過 geobike:station_changed
通道和系統(tǒng) ID 一起發(fā)出。
數(shù)據(jù)模型
在 Redis 中構建數(shù)據(jù)時,最重要的考慮因素是如何查詢信息。 共享單車程序需要支持的兩個主要查詢是:
- 找到我們附近的車站
- 顯示車站相關的信息
Redis 提供了兩種主要數(shù)據(jù)類型用于存儲數(shù)據(jù):哈希和有序集。 哈希類型很好地映射到表示車站的 JSON 對象;由于 Redis 哈希不使用固定的數(shù)據(jù)結構,因此它們可用于存儲可變的車站信息。
當然,在地理位置上尋找站點需要地理空間索引來搜索相對于某些坐標的站點。 Redis 提供了幾個使用有序集數(shù)據(jù)結構構建地理空間索引的命令。
我們使用 ${system_id}:station:${station_id}
這種格式的鍵名存儲車站相關的信息,使用 ${system_id}:stations:location
這種格式的鍵名查找車站的地理空間索引。
獲取用戶位置
構建應用程序的下一步是確定用戶的當前位置。 大多數(shù)應用程序通過操作系統(tǒng)提供的內(nèi)置服務來實現(xiàn)此目的。 操作系統(tǒng)可以基于設備內(nèi)置的 GPS 硬件為應用程序提供定位,或者從設備的可用 WiFi 網(wǎng)絡提供近似的定位。
查找車站
找到用戶的位置后,下一步是找到附近的共享單車站。 Redis 的地理空間功能可以返回用戶當前坐標在給定距離內(nèi)的所有車站信息。 以下是使用 Redis 命令行界面的示例。
想象一下,我正在紐約市第五大道的蘋果零售店,我想要向市中心方向前往位于西 37 街的 MOOD 布料店,與我的好友 Swatch 相遇。 我可以坐出租車或地鐵,但我更喜歡騎單車。 附近有沒有我可以使用的單車共享站呢?
蘋果零售店位于 40.76384,-73.97297。 根據(jù)地圖顯示,在零售店 500 英尺半徑范圍內(nèi)(地圖上方的藍色)有兩個單車站,分別是陸軍廣場中央公園南單車站和東 58 街麥迪遜單車站。
我可以使用 Redis 的 GEORADIUS
命令查詢 500 英尺半徑范圍內(nèi)的車站的 NYC
系統(tǒng)索引:
127.0.0.1:6379> GEORADIUS NYC:stations:location -73.97297 40.76384 500 ft
1) "NYC:station:3457"
2) "NYC:station:281"
Redis 使用地理空間索引中的元素作為特定車站的元數(shù)據(jù)的鍵名,返回在該半徑內(nèi)找到的兩個共享單車站。 下一步是查找兩個站的名稱:
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"
查找與這些鍵名關聯(lián)的名稱會生成一個我可以從中選擇的車站的有序列表。 Redis 不提供方向和路線的功能,因此我使用設備操作系統(tǒng)的路線功能繪制從當前位置到所選單車站的路線。
GEORADIUS
函數(shù)可以很輕松的在你喜歡的開發(fā)框架的 API 里實現(xiàn),這樣就可以向應用程序添加位置功能了。
其他的查詢命令
除了 GEORADIUS
命令外,Redis 還提供了另外三個用于查詢索引數(shù)據(jù)的命令:GEOPOS
、GEODIST
和 GEORADIUSBYMEMBER
。
GEOPOS
命令可以為 地理哈希 中的給定元素提供坐標(LCTT 譯注:geohash 是一種將二維的經(jīng)緯度編碼為一位的字符串的一種算法,常用于基于距離的查找算法和推薦算法)。 例如,如果我知道西 38 街 8 號有一個共享單車站,ID 是 523,那么該站的元素名稱是 NYC:station:523
。 使用 Redis,我可以找到該站的經(jīng)度和緯度:
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
命令類似,但該命令不是采用一組坐標,而是采用索引的另一個成員的名稱,并返回以該成員為中心的給定半徑內(nèi)的所有成員。 要查找陸軍廣場中央公園南單車站 1000 英尺范圍內(nèi)的所有車站,請輸入以下內(nèi)容:
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"