首页 › AI System Design › 第16章:设计邻近服务功能

第16章:设计邻近服务功能

简介​

邻近服务旨在查找附近的地点,例如餐馆、酒店、加油站及其他商家。此功能广泛应用于Google 地图和Yelp等应用中,帮助用户在指定半径范围内发现各类场所。​

步骤1:理解问题并明确范围​

根据用户位置(纬度、经度)和搜索半径搜索企业。​

允许企业主添加、更新或删除企业(非实时)。​

在请求时提供详尽的业务信息。​

步骤2:概要设计​

API Design​

搜索附近商家​

GET /v1/search/nearby​

•

请求参数:​

◦

latitude:用户的地理位置纬度。​

◦

longitude:用户的地理位置经度。​

◦

radius:搜索半径(默认值:5000米)。​

High-Level System Architecture​

该系统由两部分组成:基于位置的服务(LBS)和业务相关服务。​

​

飞书文档 - 图片

​

•

基于位置的服务(LBS):​

◦

处理基于位置的搜索查询。​

◦

读多写少的服务,无写请求。​

◦

QPS较高,尤其是在人口密集区域的高峰时段,且系统为无状态架构。​

•

业务服务:处理两类请求。​

◦

业务负责人负责创建、更新或删除业务。​

◦

客户查看某项业务的详细信息。​

•

负载均衡器:将流量路由至LBS和业务服务。​

•

数据库集群:​

◦

用途采用主从架构以应对读密集型工作负载。​

◦

通过LBS读取的数据与主数据库写入的数据之间可能存在一定差异。​

​

​

步骤3:获取附近商家的算法​

Option 1: Two-Dimensional Search (Naive Approach)​

​

飞书文档 - 图片

​

最直观的方法是绘制一个具有预设半径的圆,并找出该圆内的所有商家。​

SQL 查询:​

​

​

问题:​

•

效率低下:需要扫描整个数据库。​

•

受限于一维索引(纬度/经度)。​

Better Approach​

一种潜在的改进方案是在经度和纬度列上建立索引,尽管这样会略有改善,但速度仍然非常慢。​

•

<b 上述方法的问题在于,数据库索引只能在单一维度上提升查询速度。​

•

一种最优的方法是利用地理空间索引,将二维数据映射为一维。​

◦

哈希:均匀网格、Geo Hash​

◦

树:四叉树、Google S2、RTree​

​

​

Option 2: Evenly Divided Grid​

​

​

•

将世界划分为固定大小的网格.​

•

问题:业务分布不均(城市密度高,农村稀疏)。​

Option 3: Geohash​

•

将地球沿本初子午线和赤道划分为四个象限,再将每个象限进一步细分为四个更小的网格。​

•

每个网格都可以通过调整经度和纬度的位数来表示。​

•

重复此细分​

​

​

​

​

51%

​

​

​

​

49%

​

​

•

将纬度和经度编码为一个单一的字母数字字符串。它具有12个精度级别(共12级)​

•

分层网格结构可实现高效查询。​

•

根据表格选择合适的精度,采用最小的Geohash长度。​

​

​

•

地理哈希保证:两个地理哈希的共同前缀越长,它们之间的距离就越近。​

•

挑战:​

​

​

•

边界问题(靠近网格边缘的业务可能会被排除在外)。​

◦

两个地点可能相距很近,但却完全没有共同的前缀(甚至可能位于赤道的另一侧)​

◦

两个地点可能拥有很长的共同前缀,但属于不同的地理哈希。​

•

解决方案:需搜索相邻网格。​

Option 4: Quadtree​

四叉树是一种树形数据结构,它通过递归方式将二维空间划分为四个象限,每个内部节点恰好拥有四个子节点,分别对应该空间的四个子区域。​

•

四叉树是一种内存中的数据结构,它在每台LBS服务器上运行,并在服务器启动时构建。​

​

​

•

根节点被递归地划分为4个象限,直到每个象限中不再存在超过x个业务的节点(本例中为100个)。​

​

​

•

四叉树索引占用的内存不多(通常以GB为单位),完全可以容纳在一台服务器中。​

•

由于构建该树的时间复杂度为nlogn,因此构建过程可能需要几分钟。​

•

适用于k近邻搜索查询(例如,查找最近的加油站)。​

​

​

运行时注意事项​

•

对于大约2亿家企业而言,服务器启动时构建一棵四叉树可能只需几分钟。​

•

在构建四叉树期间,它无法对外提供服务,因此新版本应以渐进式方式逐步推送到部分服务器。​

•

在更新业务或新增功能时,最简便的做法是逐步重建四叉树。(这会导致大量缓存失效)​

•

也可以在运行时动态更新四叉树,但实现起来更为复杂。(需要加锁机制)​

Option 5: Google S2​

它基于希尔伯特曲线将球面映射为一维索引。在希尔伯特曲线上彼此接近的两个点,在一维空间中也彼此接近。​

​

​

​

​

46%

​

​

​

​

54%

​

​

•

利用希尔伯特曲线将地球划分为多个小单元.​

•

非常适合地理围栏,因为它能够以不同的精度覆盖任意区域。​

•

地理围栏还允许定义围绕感兴趣区域的参数。​

•

另一个优势是,与其采用固定的精度级别,我们可以在S2中指定最小、最大精度级别以及最大单元格数量。​

权衡比较​

地理哈希码​

•

易于使用且易于实现——无需构建或重建树结构​

•

支持固定半径的查询结果​

•

索引更新简便。​

•

无法根据人口密度动态调整网格大小。​

四叉树​

•

实现难度略高。​

•

支持获取k个最近的商家。​

•

可以根据人口密度动态调整网格大小。​

•

更新索引更加复杂,可能需要重建整棵树。​

​

​

步骤4:数据库与缓存策略的扩展​

Scaling the Business Table​

•

按业务ID进行分片可确保数据均匀分布。​

•

我们在表中为每个业务单独设置一行。​

​

​

Scaling the Geospatial Index​

•

可能不太适合使用地理哈希表。在这种情况下,所有数据都可以容纳在单台服务器上,因此没有进行分片的技术理由。​

•

更好的做法是部署只读副本,以分担读取负载。​

​

​

Cache Strategy​

最直观的缓存键选择是位置坐标,但它存在一些问题:​

•

来自GPS的位置坐标并不精确。​

•

用户移动时,位置坐标会发生变化。​

•

更好的密钥是地理哈希码。​

​

​

​

​

Step 5: Deployment Strategy and Final Architecture​

Region and Availability Zones​

•

Deploy LBS and Business Service across multiple regions.​

Handling Real-Time Updates​

•

Business updates are batch processed daily.​

Final System Architecture​

​

​

最终的算法如下:​

获取附近商家的步骤​

用户请求:​

◦

用户搜索半径为500米.​

◦

客户端将纬度(37.776720)、经度(-122.416730)以及半径(500米)发送至负载均衡器.​

请求转发:​

◦

负载均衡器(LB)将请求转发至基于位置的服务(LBS)。​

Geohash 计算:​

◦

LBS确定与半径相匹配的地理哈希长度。​

◦

使用参考表,500米对应于geohash长度=6.​

获取相邻的地理哈希值:​

◦

LBS计算邻近的geohash以涵盖周边区域。​

◦

结果是一个列表:​

从 Redis 获取商家 ID:​

◦

对于列表中的每个格雷沙,LBS会向格雷沙 Redis 服务器发起查询,以获取商家 ID.​

◦

采用并行查询以最大限度地降低延迟。​

检索并排序商家:​

◦

LBS 从业务信息 Redis 服务器中获取完整的业务详情。​

◦

企业按与用户位置的距离进行排序。​

◦

排序后的结果被返回给客户端。​

关键优化​

•

并行调用 Redis:缩短响应时间。​

•

Geohash 索引:确保高效的空间查询。​

•

缓存:加快业务数据的查找与检索速度。​

该方法可实现低延迟、可扩展的用户周边商家检索功能。​

​

​

Choosing the Best Indexing Method​

​

​

​