使用 Redis 和 Python 构建共享单车应用

了解如何使用 Redis 和 Python 构建位置感知应用。
297 位读者喜欢这篇文章。
A tour of Google's 2016 open source releases

Travis Wise。CC BY-SA 2.0

我经常出差。我不太喜欢汽车,所以当我有空闲时间时,我更喜欢在城市里步行或骑自行车。我去过的许多城市都有共享单车系统,你可以租用几个小时的自行车。大多数这些系统都有一个应用程序来帮助用户定位和租用自行车,但对于像我这样的用户来说,如果有一个地方可以获取城市中所有可供出租的自行车的信息,将会更有帮助。

为了解决这个问题并展示开源为 Web 应用程序添加位置感知功能的强大之处,我结合了公开可用的共享单车数据、Python 编程语言和开源 Redis 内存数据结构服务器来索引和查询地理空间数据。

由此产生的共享单车应用程序整合了来自许多不同共享系统的数据,包括纽约市的 Citi Bike 共享单车。它利用 Citi Bike 系统提供的通用共享单车信息源,并使用其数据来演示可以使用 Redis 构建的一些功能,以索引地理空间数据。Citi Bike 数据根据 Citi Bike 数据许可协议 提供。

通用共享单车信息源规范

通用共享单车信息源规范 (GBFS) 是由 北美共享单车协会 开发的 开放数据规范,旨在使地图和交通应用程序更容易将共享单车系统添加到其平台中。目前,全球有 60 多个不同的共享系统正在使用该规范。

信息源由几个简单的 JSON 数据文件组成,其中包含有关系统状态的信息。信息源从一个顶级 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,这是一个短代码,可用于为 Redis 键创建命名空间。GBFS 规范没有指定系统 ID 的格式,但保证它是全局唯一的。许多共享单车信息源使用诸如 coast_bike_share、boise_greenbike 或 topeka_metro_bikes 之类的短名称作为系统 ID。其他系统使用熟悉的地理缩写,例如 NYC 或 BA,还有一个系统使用通用唯一标识符 (UUID)。共享单车应用程序使用标识符作为前缀,为给定系统构造唯一键。

station_information 信息源提供有关构成系统的共享单车站的静态信息。站点由 JSON 对象表示,其中包含多个字段。站点对象中有几个必填字段,提供物理单车站的 ID、名称和位置。还有几个可选字段,提供有用的信息,例如最近的十字路口或接受的付款方式。这是共享单车应用程序这一部分的主要信息来源。

构建数据库

我编写了一个示例应用程序 load_station_data.py,它模拟了从外部源加载数据的后端进程中会发生的情况。

查找共享单车站

加载共享单车数据从 systems.csv 文件开始,该文件来自 GitHub 上的 GBFS 仓库

仓库的 systems.csv 文件提供了已注册的共享单车系统的发现 URL,这些系统具有可用的 GBFS 信息源。发现 URL 是处理共享单车信息的起点。

load_station_data 应用程序获取 systems 文件中找到的每个发现 URL,并使用它来查找两个子信息源的 URL:系统信息和站点信息。系统信息信息源提供了一个关键信息:系统的唯一 ID。(注意:系统 ID 也包含在 systems.csv 文件中,但该文件中的某些标识符与信息源中的标识符不匹配,因此我始终从信息源中获取标识符。) 有关系统的详细信息,如共享单车 URL、电话号码和电子邮件,可以在应用程序的未来版本中添加,因此数据存储在使用键 ${system_id}:system_info 的 Redis 哈希中。

加载站点数据

站点信息提供有关系统中每个站点的数据,包括系统的位置。load_station_data 应用程序遍历站点信息源中的每个站点,并将每个站点的数据存储在使用 ${system_id}:station:${station_id} 形式的键的 Redis 哈希中。每个站点的位置都使用 GEOADD 命令添加到共享单车的地理空间索引中。

更新数据

在后续运行中,我不希望代码从 Redis 中删除所有信息源数据并将其重新加载到空的 Redis 数据库中,因此我仔细考虑了如何处理数据的就地更新。

代码首先加载数据集,其中包含有关正在处理的系统的所有共享单车站的信息到内存中。当加载站点的的信息时,站点(按键)将从内存中的站点集中删除。加载所有站点数据后,我们剩下的是一个包含所有必须为该系统删除的站点数据的集合。

应用程序遍历此站点集,并创建一个事务来删除站点信息,从地理空间索引中删除站点键,并从系统的站点列表中删除站点。

代码注释

示例代码 中,有几点值得注意。首先,使用 GEOADD 命令将项目添加到地理空间索引,但使用 ZREM 命令删除。由于地理空间类型的底层实现使用排序集,因此使用 ZREM 删除项目。警告:为简单起见,示例代码演示了如何使用单个 Redis 节点;事务块需要重新构建才能在集群环境中运行。

如果您使用的是 Redis 4.0(或更高版本),则可以使用代码中 DELETEHMSET 命令的一些替代方案。Redis 4.0 提供了 UNLINK 命令作为 DELETE 命令的异步替代方案。UNLINK 将从键空间中删除键,但它会在单独的线程中回收内存。HMSET 命令在 Redis 4.0 中已弃用,并且 HSET 命令现在是可变参数的(即,它接受无限数量的参数)。

通知客户端

在流程结束时,会向依赖我们数据的客户端发送通知。使用 Redis 发布/订阅机制,通知通过 geobike:station_changed 通道发出,并带有系统 ID。

数据模型

在 Redis 中构建数据时,最重要的事情是考虑如何查询信息。共享单车应用程序需要支持的两个主要查询是

  • 查找我们附近的站点
  • 显示有关站点的信息

Redis 提供了两种主要数据类型,可用于存储我们的数据:哈希和排序集。哈希类型 非常适合表示站点的 JSON 对象;由于 Redis 哈希不强制执行模式,因此它们可用于存储可变的站点信息。

当然,按地理位置查找站点需要地理空间索引,以搜索相对于某些坐标的站点。Redis 提供了 几个命令,用于使用 排序集 数据结构构建地理空间索引。

我们使用 ${system_id}:station:${station_id} 格式构造键,用于包含有关站点的信息的哈希,并使用 ${system_id}:stations:location 格式构造键,用于查找站点的地理空间索引。

获取用户的位置

构建应用程序的下一步是确定用户的当前位置。大多数应用程序通过操作系统提供的内置服务来实现这一点。操作系统可以根据设备内置的 GPS 硬件或从设备可用的 WiFi 网络估算的位置为应用程序提供位置。

查找站点

找到用户的位置后,下一步是定位附近的共享单车站。Redis 的地理空间函数可以返回给定距离内用户当前坐标附近的站点信息。以下是使用 Redis 命令行界面的示例。

Map of Apple NYC store location

opensource.com

假设我在纽约市第五大道的 Apple Store,我想去西 37 街的 Mood 与我的好友 Swatch 会面。我可以乘坐出租车或地铁,但我更喜欢骑自行车。在商店 500 英尺半径范围内(上图蓝色区域)是否有任何附近的共享单车站可以租用自行车?

Apple Store 的位置是 40.76384, -73.97297。根据地图,两个共享单车站——Grand Army Plaza & Central Park South 和 East 58th St. & Madison——位于商店 500 英尺半径范围内(上图蓝色区域)。

我可以使用 Redis 的 GEORADIUS 命令查询 NYC 系统索引,以查找 500 英尺半径范围内的站点

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 还提供了其他三个命令来查询索引中的数据:GEOPOSGEODISTGEORADIUSBYMEMBER

GEOPOS 命令可以从地理哈希中提供给定元素的坐标。例如,如果我知道在西 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 命令提供索引的两个元素之间的距离。如果我想找到 Grand Army Plaza & Central Park South 站和 East 58th St. & Madison 站之间的距离,我将发出以下命令

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

最后,GEORADIUSBYMEMBER 命令类似于 GEORADIUS 命令,但该命令不是采用一组坐标,而是采用索引中另一个成员的名称,并返回以该成员为中心给定半径内的所有成员。要查找 Grand Army Plaza & Central Park South 站 1,000 英尺范围内的所有站点,请输入以下内容

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 来解析数据并构建共享单车系统位置的索引,但它可以很容易地推广到定位餐馆、公共交通或开发者希望帮助用户查找的任何其他类型的地点。


本文基于今年我在罗利举行的 Open Source 101 大会上的 我的演讲

标签
User profile image.
一位自称数据库极客的人,负责 Redis Labs 的开发者关系计划。在 Redis Labs,他专注于通过社区聚会、技术培训和开发者布道来支持和发展 Redis 开发者社区。

评论已关闭。

© . All rights reserved.