作为 Redis 的开发者布道师,我经常出差! 我不太喜欢汽车,所以当我有空闲时间时,我更喜欢在城市里步行或骑自行车。 我在出差时访问过的许多城市都有自行车共享系统,你可以借用几个小时的自行车。 大多数这些系统都有一个用于租自行车的应用程序,但它们只分享其系统的详细信息。 这让我想到——使用公开的自行车共享信息来构建一个“应用程序”,向你展示全球信息,这将是演示 Redis 的地理空间功能的一种有趣方式。 有了 GeoBike,Redis 自行车共享应用程序就诞生了。
GeoBike 整合了来自许多不同共享系统的数据,包括纽约市的 CitiBike 自行车共享。 我们将利用 Citi Bike 系统提供的通用自行车共享Feed,并使用他们的数据来演示我们可以使用 Redis 构建的一些功能来索引地理空间数据。 CitiBike 数据根据 NYCBS 数据使用政策提供。
通用自行车共享 Feed 规范
通用自行车共享 Feed 规范 (GBFS) 是一个由 北美自行车共享协会开发的 开放数据规范,旨在使地图和交通应用程序更容易地将自行车共享系统添加到其平台中。 该规范目前已被全球 60 多个不同的共享系统使用。
该 Feed 由几个简单的 JSON 数据文件组成,其中包含有关系统状态的信息。 该 Feed 从一个顶级 JSON 文件开始,该文件引用了子 Feed 数据的 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_information 和 station_information Feed 的数据。
该 system_information Feed 将为我们提供系统 ID,这是一个简短的代码,我们将使用它来为我们的 Redis 键创建命名空间。 GBFS 规范没有指定系统 ID 的格式,但保证它是全局唯一的。 许多自行车共享 Feed 使用诸如 coast_bike_share、boise_greenbike 或 topeka_metro_bikes 等短名称作为系统 ID。 其他人使用熟悉的地理缩写,例如 NYC 或 BA,还有人使用 UUID。 我们的代码使用该标识符作为前缀来为给定系统构造唯一的键。
该 station_information Feed 提供了有关构成系统的共享站的静态信息。 站点由具有多个字段的 JSON 对象表示。 站点对象中有几个强制性字段,这些字段提供了物理自行车站的 ID、名称和位置。 还有几个可选字段提供了有用的信息,例如“交叉路口”或“接受的付款方式”。 这是自行车共享应用程序此部分的主要信息来源。
构建我们的数据库
我编写了一个示例应用程序 load_station_data.py,它模拟了从外部源加载数据的后端进程中会发生的情况。
查找自行车共享站
加载自行车共享数据从 systems.csv 文件开始,该文件来自 Github 上的 GBFS 存储库。
该 systems.csv 文件为已注册的自行车共享系统提供发现 URL,这些系统具有可用的 GBFS Feed。 发现 URL 是处理自行车共享信息的起点。
该 load_station_data 应用程序获取在 systems 文件中找到的每个发现 URL,并使用它来查找两个子 Feed 的 URL:系统信息和站点信息。 系统信息 Feed 为我们提供了一个关键信息:系统的唯一 ID。注意,系统 ID 也在 systems.csv 文件中提供,但该文件中的某些标识符与 Feed 中的标识符不匹配,因此我们将始终从 Feed 中获取标识符。 有关系统的信息,例如租赁 URL、电话号码和电子邮件,在未来版本的应用程序中可能很有用,因此我们将使用键 ${system_id}:system_info 将数据存储在 Redis 哈希中。
加载站点数据
站点信息为我们提供了有关系统中每个站点的数据,包括系统的位置。 该 load_station_data 应用程序迭代站点 Feed 中的每个站点,并将有关站点的数据存储到 Redis 哈希中,该哈希的键格式为 ${system_id}:station:${station_id}。 使用 GEOADD 命令将每个站点的位置添加到自行车共享的 地理空间 索引中。
更新数据
在后续运行中,我们不希望我们的代码从 Redis 中删除所有 Feed 数据并将其重新加载到空的 Redis 数据库中,因此我们需要考虑如何处理数据的就地更新。
我们的代码首先加载包含当前正在处理的系统的所有自行车共享站信息的集合到内存中。 加载站信息时,将从内存中的站集合中删除站(按键)。 加载所有站点数据后,我们将得到一个集合,其中包含必须删除的此系统的所有站点数据。
我们的应用程序迭代此站点集合,并创建一个事务来删除站点信息,从地理空间索引中删除站点键,并从系统的站点列表中删除站点。
关于代码的注释
在示例代码中,我们应该指出一些有趣的事情。 首先,你会注意到我们使用 GEOADD 命令将项目添加到地理空间索引,但使用 ZREM 命令将其删除。 地理空间类型的底层实现使用排序集,因此使用 ZREM 删除项目。 警告:为简单起见,示例代码演示了如何使用单个 Redis 节点; 需要重新构建事务块才能在集群环境中运行。
如果您使用的是 Redis 4.0(或更高版本),则可以使用一些替代此代码中使用的 DELETE 和 HMSET 命令的方法。 Redis 4.0 提供了 UNLINK 命令,作为 DELETE 命令的异步替代方案。 UNLINK 将从键空间中删除键,但在单独的线程中回收内存。 HMSET 命令在 Redis 4.0 中已被弃用,并且 HSET 命令现在是可变参数的。
通知客户端
在此过程结束时,我们会向依赖我们数据的客户端发送通知。 使用 Redis Pub/Sub 机制,我们在 geobike:station_changed 通道上发送系统 ID 通知。
数据模型
在 Redis 中构建数据时,最重要的是考虑如何查询信息。 对于我们的共享单车应用程序,我们需要支持的两个主要查询是
Redis 提供了两种主要数据类型,可用于存储我们的数据,即哈希和排序集。 哈希类型 可以很好地映射到代表车站的 JSON 对象,并且由于 Redis 哈希不强制执行架构,因此我们可以使用它们来存储我们的可变车站信息。
当然,为了按地理位置查找车站,我们将要构建一个地理空间索引,以搜索相对于某些坐标的车站。 Redis 提供了 多个命令,以使用 排序集 数据结构构建地理空间索引。
我们将使用格式 ${system_id}:station:${station_id} 来构造包含有关车站信息的哈希的键,并使用格式 ${system_id}:stations:location 来构造用于查找车站的地理空间索引的键。
映射结果
让我们通过生成加载到 Redis 中的数据的地图来检查数据加载的结果。 我们可以通过构建一个 KML(Keyhole 标记语言) 文件并将其加载到 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 系统的地图。
地图示例
如果我们在自定义地图上寻找 6th and Canal Bike Station,我们会看到坐标 40.72242, -74.00566,并且在地图基础图层上的 CitiBike 车站顶部有一个蓝色图钉。 当然,这不是一个完整的 QA 周期,但这是目视检查数据以建立对我们代码的信心的好方法。
在下一篇 GeoBike 文章中,我们将介绍开发人员如何查询数据库中的数据,以向应用程序添加有趣的功能。 同时,您可以从此帖子在 Github 上获取相关代码。 如果您对此帖子有任何疑问,请在 Twitter 上与我 (@tague) 联系。