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