基于本文回答
0
评论

滴滴打车中,“查找附近3公里范围内的司机”这种基于LBS的功能,Redis中是如何存储和查询的?

知识点图片

在滴滴打车、美团外卖等需要处理海量位置信息的LBS(Location-Based Services,基于位置的服务)应用中,Redis 主要通过其内置的 GEO(地理位置)数据结构来实现“查找附近3公里范围内的司机”这一功能。

Redis GEO 是在 Redis 3.2 版本引入的,下面我将从指令使用、底层原理、以及真实工业场景下的架构设计三个维度为你深度剖析。


一、 Redis 是如何存储和查询的?(基础用法)

1. 存储司机位置:GEOADD

当司机打开滴滴App,手机会以极高的频率(例如每3~5秒)向服务器上报一次GPS坐标。后端接收到坐标后,会写入 Redis。

命令格式:
GEOADD key longitude latitude member

实际应用示例:
假设我们将北京市的在线空闲司机存在一个特定的 key 中:

plaintext
# 司机1001在天安门附近
GEOADD driver:locations:beijing 116.397128 39.916527 "driver_1001"
# 司机1002在王府井附近
GEOADD driver:locations:beijing 116.410886 39.914316 "driver_1002"

注意:如果司机的坐标发生变化,再次执行 GEOADD 即可更新其位置。

2. 查询附近3公里的司机:GEOSEARCH (Redis 6.2+ 推荐)

当乘客发起打车请求时,系统会获取乘客当前的坐标,并向 Redis 发起查询。
(注:老版本使用 GEORADIUS,Redis 6.2 之后推荐使用功能更强大的 GEOSEARCH)

命令格式:

plaintext
GEOSEARCH key FROMLONLAT 乘客经度 乘客纬度 BYRADIUS 3 km ASC WITHDIST WITHCOORD LIMIT 100

参数解释:

  • BYRADIUS 3 km:查找3公里范围内的目标。
  • ASC:按照距离由近到远排序(滴滴通常优先派给最近的司机)。
  • WITHDIST:返回结果时带上具体的距离。
  • LIMIT 100:只返回最近的100名司机(为了性能和后续的ETA计算,不需要返回所有人)。

二、 Redis GEO 的底层原理是什么?

Redis 并没有为了 GEO 开发一种全新的内存数据结构,Redis GEO 的底层实际上是一个 Sorted Set(有序集合 ZSET)。

这就引出了一个问题:二维的经纬度,是如何变成一维的 ZSET 的 score(分数)的呢?
答案是:Geohash 算法

  1. 降维打击 (Geohash):
    Geohash 算法将地球不断地进行二分划分为网格,把二维的经纬度编码成一个一维的、52位的整数。
    特点:地理位置越接近的两个点,其 Geohash 编码的前缀重合度越高。
  2. 存入 ZSET:
    执行 GEOADD 时,Redis 会计算出该坐标的 52 位 Geohash 整数值,并将其作为 ZSET 的 score,司机 ID 作为 member 存入。
  3. 查询过程 (Geosearch):
    当查询“附近3公里”时:
    • Redis 会以乘客坐标为中心,画一个半径3公里的圆,计算出这个圆覆盖了哪些 Geohash 网格。
    • 根据这些网格的 Geohash 前缀,转换成 ZSET 中 score 的范围区间。
    • 利用 ZSET 的 ZRANGEBYSCORE 快速查出这些区间内的所有司机。
    • 最后过滤: 因为网格是方形的,而查询是圆形的,Redis 会在内存中对查出的司机进行一次精确的距离计算(Haversine公式),剔除掉在网格内但在3公里圆外的司机,最后排序返回。

三、 滴滴的真实工业场景中,光靠 Redis GEO 够吗?

在滴滴这种千万级日活、百万级司机同时在线的复杂场景下,简单的 GEOADDGEOSEARCH 是远远不够的。通常会面临以下挑战及解决方案:

1. 司机的状态管理(过滤载客中的司机)

Redis GEO 只能存位置,但滴滴不仅需要“附近3公里”,还需要“附近3公里且当前空闲”的司机。

  • 做法: 通常业务系统中,只有处于“听单状态”的空闲司机才会被加入到 GEO 集合中。司机一旦接单,后台会立刻调用 ZREM (因为GEO底层是ZSET) 将该司机从 GEO 集合中剔除,避免重复派单。

2. 数据的过期与清理(司机下线或断网)

Redis GEO 中的元素(司机)没有单独的 TTL(过期时间)。如果一个司机突然拔掉手机卡断网,他的位置会永久留在 Redis 里。

  • 做法: 滴滴后台会维护另一个普通的 ZSET 或者 Redis Hash,记录每个司机最后一次心跳的时间。通过定时任务(或延迟队列)定期扫描,把心跳超时(例如超过30秒未上报位置)的司机从 GEO 集合中 ZREM 清理掉。

3. 海量并发写请求的处理(分库分表/分片)

北京几十万司机每3秒上报一次坐标,单个 Redis key (如 driver:beijing) 会成为热点(Hot Key),导致单节点 CPU 打满。

  • 做法(网格化分片): 不会将整个北京市的司机放在一个 Key 里,而是按照城市区域甚至更小的 Geohash 网格进行分片。
    例如将北京拆分为 driver:geo:bj:chaoyangdriver:geo:bj:haidian,利用 Redis Cluster 将写压力分散到多个 Redis 节点上。查询时,如果乘客在边界,可能需要并发查询相邻的几个网格。

4. 直线距离 vs 实际行驶距离 (ETA)

Redis GEO 算出来的是两点之间的直线距离。在现实中,直线距离3公里,实际开车可能要绕路8公里(中间隔着一条河或高架桥)。

  • 真实派单架构:
    1. 粗排: 乘客发起请求 -> 查询 Redis GEO -> 快速圈出直线距离最近的 100 名空闲司机。
    2. 精排 (ETA计算): 把这 100 个司机的坐标和乘客坐标发给地图导航引擎(Routing Engine)。导航引擎结合实时路况,计算出这 100 个司机到达乘客上车点的真实行驶时间(ETA)和距离
    3. 派单策略: 结合司机的星级、服务分、是否顺路、真实ETA等复杂策略(通常使用机器学习模型),选出最合适的那1名司机进行派单。

总结

在滴滴打车的场景中,Redis GEO 凭借基于 Geohash 和 ZSET 的底层设计,完美承担了“海量高频位置更新”“空间范围初筛(粗排)”的重任。它是整个庞大智能派单系统中最核心、最前置的基础设施之一。

右滑查看面试常问