dot 未来速度的革新即将在您所在的城市举行活动。

加入我们在Redis发布会上

GeoBike:使用Redis构建位置感知应用程序

我作为Redis的开发者倡导者经常出差! 我不是一个汽车迷,所以当我有空闲时间时,我更喜欢在城市里散步或骑自行车。 我出差访问过的许多城市都有自行车共享系统,可以让您借用自行车几个小时。 这些系统中的大多数都有用于租借自行车的应用程序,但它们只共享其系统的详细信息。 这让我思考——使用公开可用的自行车共享信息来构建一个“应用程序”,向您展示全球信息,将是一种有趣的方式来演示 Redis 的地理空间功能。 于是,GeoBike,Redis自行车共享应用程序诞生了。

GeoBike整合了来自许多不同共享系统的数据,包括纽约市的CitiBike 自行车共享。 我们将利用 Citi Bike 系统提供的通用自行车共享提要,并使用其数据来演示使用 Redis 索引地理空间数据可以构建的一些功能。 CitiBike 数据是在NYCBS 数据使用政策下提供的。

通用自行车共享提要规范

通用自行车共享提要规范 (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
}

我们要关注的第一件事是将有关自行车共享站的信息加载到 Redis 中。 对于应用程序的这部分,我们需要来自system_informationstation_information提要的数据。

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,它模拟了在用于从外部来源加载数据的后端进程中会发生的情况。

查找自行车共享站

加载自行车共享数据从来自Github 上的 GBFS 存储库systems.csv文件开始。

systems.csv文件提供了已注册自行车共享系统的发现 URL,这些系统提供可用的 GBFS 提要。 发现 URL 是处理自行车共享信息的起点。

load_station_data应用程序获取 systems 文件中找到的每个发现 URL,并使用它查找两个子提要的 URL:系统信息和站点信息。 系统信息提要为我们提供了一个关键信息:系统的唯一 ID。  注意,系统 ID 也在 systems.csv 文件中提供,但该文件中的某些标识符与提要中的标识符不匹配,因此我们始终会从提要中获取标识符。  有关系统的信息,例如租用 URL、电话号码和电子邮件可能在应用程序的未来版本中很有用,因此我们将使用${system_id}:system_info键将数据存储在 Redis 哈希中。

加载站点数据

站点信息为我们提供了有关系统中每个站点的數據,包括系统的地址。 该load_station_data应用程序遍历站点提要中的每个站点,并将有关站点的數據存储在 Redis 哈希中,使用${system_id}:station:${station_id}形式的键。 每个站点的地址将添加到自行车共享的地理空间索引中,使用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 提供两种主要数据类型,对存储我们的数据很有用:Hashes 和 Sorted Sets。 hash 类型 很适合映射代表车站的 JSON 对象,并且由于 Redis hashes 不强制执行模式,因此我们可以使用它们来存储我们可变的车站信息。

当然,为了在地理位置上查找车站,我们想要构建一个地理空间索引,以便根据某些坐标搜索车站。 Redis 提供 几个命令 来使用 Sorted Set 数据结构构建地理空间索引。

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

映射结果

让我们通过生成加载到 Redis 的数据的映射来检查数据加载的结果。 我们可以通过构建一个 KML(Keyhole Markup Language) 文件并将其加载到 Google 地图 中来创建数据的映射。 我已经提供了 generate_station_kml.py 脚本,用于为车站 ID 生成车站位置的 KML 文件。 Google 地图将 KML 文件限制为 10 个图层和 5000 个要素,因此 KML 生成器应用程序只会为单个系统生成文件。

该应用程序使用 redis-py scan_iter 迭代车站键(与模式 ${system_id}:station:* 匹配的键),并使用 Python minidom 包 来构建输出 XML。

在运行 load_station_data.py 后,我对我的 Redis 实例运行了 generate_station_kml.py 脚本,并生成了以下纽约市 CitiBike 系统的地图。

地图示例

如果我们在自定义地图上查找 第 6 大街和运河自行车站,我们看到坐标 40.72242,-74.00566,以及地图底层 CitiBike 车站顶部的蓝色图钉。 当然,这不是一个完整的 QA 循环,但它是一个很好的方法,可以仔细查看数据,从而对我们的代码更有信心。

在下一篇文章 GeoBike 中,我们将看看开发人员如何查询数据库中的数据,为应用程序添加有趣的特性。 同时,您可以在 Github 上获取与本文相关的代码。 如果您对本文有任何疑问,请在 Twitter 上与我联系(@tague)。