Redis GEO(地理坐标使用Redis存贮及其运用)
Redis GEO 是 Redis 3.2 版本引入的地理位置功能,基于 Sorted Set(ZSET) 实现。它允许存储地理位置坐标(经纬度),并进行距离计算、范围查询等操作。
核心数据结构
底层实现:Geohash + Sorted Set
Geohash 编码:将二维的经纬度转换为一维字符串,保留空间邻近性
Sorted Set:使用 64 位整数 score 存储 Geohash 编码
成员(member):地点标识
分值(score):Geohash 的 52 位整数表示
主要命令
1. GEOADD - 添加地理位置
# 语法
GEOADD key longitude latitude member [longitude latitude member ...]
# 示例:添加北京、上海、广州的坐标
GEOADD cities 116.397128 39.916527 "北京" 121.473701 31.230416 "上海" 113.264385 23.129112 "广州"
一次可添加多个位置
如果成员已存在,会更新坐标
2. GEODIST - 计算两点距离
# 语法
GEODIST key member1 member2 [unit]
# 示例:计算北京到上海的距离
GEODIST cities "北京" "上海" km
3. GEOPOS - 获取坐标
# 语法
GEOPOS key member [member ...]
# 示例:获取北京的坐标
GEOPOS cities "北京"
# 返回:116.39712822437286377 39.91652735625285437
Redis GEO 核心指令详解
一、核心指令深度解析
1. GEOADD - 添加地理位置
# 完整语法
GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]
# 参数说明
NX:仅添加新成员,不更新已存在成员
XX:仅更新已存在成员,不添加新成员
CH:返回变更数量(新增+更新),默认只返回新增数量
# 示例
# 添加北京坐标
GEOADD cities 116.397128 39.916527 "北京"
# 使用NX参数(北京已存在则忽略)
GEOADD cities NX 116.397128 39.916527 "北京" 121.473701 31.230416 "上海"
# 使用XX参数(只更新北京,上海不存在则忽略)
GEOADD cities XX 116.40 39.92 "北京" 121.47 31.23 "上海"
# 使用CH参数查看变更总数
GEOADD cities CH 116.397128 39.916527 "北京" 113.264385 23.129112 "广州"
# 返回:2(如果北京已存在且坐标不同,广州是新增)
# 批量添加
GEOADD cities 116.397128 39.916527 "北京" \
121.473701 31.230416 "上海" \
113.264385 23.129112 "广州" \
120.155069 30.274084 "杭州"
底层原理:
将经纬度转换为 52位 Geohash 整数
使用 Sorted Set 存储,member 作为成员,Geohash 作为 score
实际存储格式:ZADD key geohash_int member
2. GEODIST - 计算距离
# 语法
GEODIST key member1 member2 [m|km|mi|ft]
# 示例
# 计算北京到上海的距离(千米)
GEODIST cities "北京" "上海" km
# 返回:1068.2986(约1068公里)
# 计算北京到广州的距离(米)
GEODIST cities "北京" "广州" m
# 返回:1887002.18(约1887公里)
# 不指定单位,默认返回米
GEODIST cities "北京" "上海"
# 返回:1068298.5792
距离计算原理:
使用 Haversine 公式 计算大圆距离
地球半径:6372797.560856米
公式:distance = 2R * arcsin(√(sin²(Δφ/2) + cosφ₁·cosφ₂·sin²(Δλ/2)))
3. GEOPOS - 获取坐标
# 语法
GEOPOS key member [member ...]
# 示例
# 获取单个城市的坐标
GEOPOS cities "北京"
返回: "116.39712822437286377" 2) "39.91652735625285437"
# 获取多个城市的坐标
GEOPOS cities "北京" "上海" "广州"
# 返回:
# 1) "116.39712822437286377" 2) "39.91652735625285437"
# 1) "121.47370129823684692" 2) "31.23041559259875756"
# 1) "113.26438432931900024" 2) "23.12911163277030071"
# 获取不存在的成员
GEOPOS cities "纽约"
# 返回:(nil)
精度说明:
返回的坐标精度比输入的更高
这是 Geohash 解码的固有特性
实际使用中可以截断到所需精度
4. GEORADIUS / GEOSEARCH - 范围查询
# GEORADIUS(传统语法)
GEORADIUS cities 116.397128 39.916527 500 km WITHDIST WITHCOORD COUNT 10 ASC
# 示例1:从成员位置查询
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 500 km WITHDIST WITHCOORD
# 示例2:从坐标查询
GEOSEARCH cities FROMLONLAT 116.397128 39.916527 BYRADIUS 500 km WITHDIST
# 示例3:矩形范围查询
GEOSEARCH cities FROMMEMBER "北京" BYBOX 100 100 km WITHDIST
# 示例4:带分页和排序
GEOSEARCH cities FROMLONLAT 116.397128 39.916527 BYRADIUS 1000 km WITHDIST COUNT 5 ASC
# 示例5:获取所有信息
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 300 km WITHCOORD WITHDIST WITHHASH
5. GEOHASH - 获取Geohash字符串
# 语法
GEOHASH key member [member ...]
# 示例
GEOHASH cities "北京"
# 返回:1) "wx4g0b7xrt0"
GEOHASH cities "北京" "上海" "广州"
# 返回:
# 1) "wx4g0b7xrt0"
# 2) "wtw3sjt9qs0"
# 3) "ws0e89curg0"
# 使用场景:可用于前端直接调用地图API
二、底层原理深度解析
1. Geohash 编码原理
编码过程:
经纬度(北京): 116.397128, 39.916527
↓
二进制交错合并
经度二进制: 110100101110000101011...
纬度二进制: 101110010111000010101...
交错后: 1 1 0 1 0 1 1 0 1 0 1 1 ... (经度、纬度交替)
↓
Base32编码
每5位一组: 11100 -> w, 10111 -> x, 00100 -> 4, ...
最终: wx4g0b7xrt0
特性:
前缀匹配:相同前缀表示地理位置相近
精度与长度关系:
1位:±2500km
2位:±630km
3位:±78km
4位:±20km
5位:±2.4km
6位:±610m
7位:±76m
8位:±19m
9位:±2.4m
10位:±0.6m
11位:±0.074m
2. Sorted Set 存储结构
// Redis 内部表示
typedef struct zset {
dict *dict; // 哈希表,用于O(1)查找成员
zskiplist *zsl; // 跳表,用于范围查询
} zset;
// GEO 在 ZSET 中的存储
key = "cities"
member = "北京"
score = geohash_to_int64("wx4g0b7xrt0") // 52位整数
内存布局:
+-------------------+ +-------------------+
| ZSET | | 跳表节点 |
| dict: ptr--------|---->| member: "北京" |
| zsl: ptr--------|---->| score: 40698849..|
+-------------------+ | level[0]->下一个 |
+-------------------+
3. 范围查询算法
GEORADIUS 执行步骤:
1. 计算中心点的 Geohash (52位整数)
2. 根据半径计算 Geohash 精度级别
- 小半径:使用高精度(更多位数)
- 大半径:使用低精度(较少位数)
3. 生成8个方向的边界框 Geohash
4. 在 Sorted Set 中查询该范围内的成员
5. 使用 Haversine 公式精确过滤
6. 按距离排序并返回结果
时间复杂度:
O(log(N) + M):N是总元素数,M是返回元素数
跳表查询:O(log(N))
范围扫描:O(M)
4. 精度与误差控制
误差来源:
Geohash 边界问题:相同前缀可能跨越边界
# 解决方案:查询8个相邻区域
# Redis 内部自动处理
三、高级用法示例
1. 结合 ZSET 命令操作
# 1. 查看所有地理位置成员
ZRANGE cities 0 -1
# 返回:1) "广州" 2) "上海" 3) "北京"
# 2. 按 Geohash 范围查询(自定义范围)
ZRANGEBYSCORE cities 4069880000000000 4069890000000000
# 可以获取特定 Geohash 范围的成员
# 3. 删除地理位置
ZREM cities "北京"
# 4. 获取成员分数(Geohash 整数)
ZSCORE cities "北京"
# 返回:"4069884985356763"
# 5. 统计数量
ZCARD cities
2. 实现地理围栏
# 用户签到场景
# 1. 添加地理围栏
GEOADD geofence:store 116.397128 39.916527 "store_001"
# 2. 用户签到
GEOPOS user:location "user_123"
# 返回用户当前位置
# 3. 判断是否在围栏内
GEODIST geofence:store "store_001" user:location "user_123" m
# 如果距离 < 50米,允许签到
# 4. 批量检查多个围栏
GEORADIUS geofence:all 116.397 39.916 100 m WITHDIST
# 返回100米内所有围栏及距离
3. 分片策略
# 城市级别分片
GEOADD cities:beijing 116.397128 39.916527 "天安门"
GEOADD cities:shanghai 121.473701 31.230416 "外滩"
# 网格分片(基于 Geohash 前缀)
# Geohash 前3位相同的在同一个分片
# wx4(北京区域)、wtw(上海区域)
四、性能优化建议
1. 查询优化
# 避免大范围查询
# 不推荐:查询5000公里范围
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 5000 km
# 推荐:先估算合理半径
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 50 km COUNT 100
# 使用 COUNT 限制结果集
GEOSEARCH key FROMLONLAT 116.397 39.916 BYRADIUS 10 km COUNT 50 ASC
2. 内存优化
# 定期清理过期数据
# 方法1:使用时间戳作为 score 的一部分
GEOADD locations:20240101 116.397 39.916 "user_123"
# 方法2:使用 ZREMRANGEBYSCORE 清理
# 假设 score 包含时间戳
ZREMRANGEBYSCORE locations:20240101 0 1704067200000
3. 集群优化
# 使用 hash tag 确保相关数据在同一节点
# 所有北京的数据在同一个 slot
GEOADD {cities}:beijing 116.397128 39.916527 "location1"
GEOADD {cities}:beijing 116.398 39.917 "location2"
# 查询时也要使用相同的 hash tag
GEOSEARCH {cities}:beijing FROMMEMBER "location1" BYRADIUS 10 km
实际应用示例
场景:附近的人/商家搜索
# 1. 添加商家位置
GEOADD stores 116.403963 39.915119 "王府井店" 116.40853 39.9155 "东单店" 116.416337 39.928171 "三里屯店"
# 2. 用户当前位置查询附近3公里内的商家
GEOSEARCH stores FROMLONLAT 116.406 39.915 BYRADIUS 3 km WITHDIST ASC
# 3. 计算用户到具体店面的距离
GEODIST stores "王府井店" "东单店" km
场景:车辆轨迹管理
# 记录车辆实时位置
GEOADD fleet:locations 116.397128 39.916527 "car_001"
GEOADD fleet:locations 121.473701 31.230416 "car_002"
# 查询某中心点50公里内的所有车辆
GEOSEARCH fleet:locations FROMLONLAT 116.4 39.9 BYRADIUS 50 km WITHCOORD
性能特点
优点
高性能:查询复杂度 O(log(N) + M),N是元素数,M是返回数
高精度:使用 WGS84 坐标系统,误差在 0.5m 内
灵活查询:支持半径查询、矩形查询、距离排序
与ZSET兼容:可使用所有ZSET命令操作GEO数据
局限性
数据量限制:单节点适合千万级数据,更大数据需分片
地球范围:只能处理有效经纬度(-180到180,-85到85)
距离计算:采用Haversine公式,计算大圆距离(球面)
最佳实践
1. 键设计
# 业务前缀:数据类型:区域
geo:stores:beijing
geo:users:online
geo:vehicles:active
2. 数据过期策略
# GEO本身不支持TTL,可结合EXPIRE或使用ZSET命令
EXPIRE geo:users:online 3600 # 1小时过期
3. 批量操作优化
# 使用管道(pipeline)批量添加
cat locations.txt | redis-cli --pipe
# locations.txt内容:
GEOADD geo:points 116.397 39.916 "point1"
GEOADD geo:points 121.473 31.230 "point2"
4. 集群部署注意事项
相同查询的成员应放在同一slot(使用hash tag)
# 使用{}确保相关数据在同一节点
GEOADD geo:{city}:stores 116.397 39.916 "store1"
GEOADD geo:{city}:stores 116.398 39.917 "store2"
与其他方案对比
| 特性 | Redis GEO | MongoDB 2dsphere | PostGIS |
|---|---|---|---|
| 数据结构 | Sorted Set | GeoJSON | Geometry类型 |
| 查询类型 | 半径/矩形 | 丰富的地理查询 | 最完整 |
| 性能 | 极高 | 高 | 中等 |
| 部署 | 简单 | 中等 | 复杂 |
| 适用场景 | 实时查询、简单地理围栏 | 文档+地理位置 | 复杂GIS分析 |
https://blog.csdn.net/m0_75259372/article/details/157697082