Redis 命令及数据类型 -- Geo

摘要

Geo 数据类型

  • Redis Geo 是基于有序集合(zset) 实现的地理空间操作功能,底层用geohash编码存储经纬度,核心支持 6 个基础操作 + 2 个扩展操作,兼顾精准存储距离计算范围筛选等核心需求,直接对接实际场景(如附近门店、同城好友)。

  • 有效经度(longitude)为 -180 ~ 180,有效纬度(latitude)为 -85.05112878 ~ 85.05112878

  • Redis 内部实现中:

1
2
3
GEO 数据 ≈ ZSET
score = GeoHash(52 位bit ≈ 11 个字符)
member = 实际成员名

Geohash 是什么?

  • Geohash 是一种将二维地理坐标(经度、纬度)编码为一维字符串或整数的空间索引算法,核心目标是:

1
2
3
将「位置」映射为「可排序的值」
相近的地理位置 → 前缀相同或接近
便于 范围查询、邻近查询、索引存储
  • 核心思想

不断对经纬度区间进行二分,并交叉编码
编码顺序:经度 → 纬度 → 经度 → 纬度 → …

1
2
3
4
5
6
每一步:
1.取当前区间的中点
2.大于中点记为 1
3.小于中点记为 0
4.缩小区间,继续下一位
最终得到一个 bit 序列。
  • Redis 内部的 Geohash 字符 = 52bit,即 经度 26 bit,纬度 26 bit

1
2
3
4
5
6
每个字符 = 5 bit
内部 52 bit → 按 5 bit 分组 → 52 ÷ 5 = 10 余 2 bit

Redis 默认在输出字符串时:
会把 剩余的 2 bit 填充成完整字符,即末尾补三个 0
因此最终得到 11 个 Base32 字符

Geohash 计算过程示例

1
2
longitude = 116.397128
latitude = 39.916527
  • 为了简化计算过程,我们这里固定一个常用精度:

    • 精度选择:5 个 Geohash 字符 = 25 个 bit ⇒ 经度 13 bit,纬度 12 bit(奇数位经度)
  • 逐位计算(关键过程)
    1️⃣ 经度 bit(13 位)

位次 区间 mid 判断 bit
1 [-180,180] 0 116 ≥ 0 1
2 [0,180] 90 116 ≥ 90 1
3 [90,180] 135 116 < 135 0
4 [90,135] 112.5 116 ≥ 112.5 1
5 [112.5,135] 123.75 116 < 123.75 0
6 [112.5,123.75] 118.125 116 < 118.125 0
7 [112.5,118.125] 115.3125 116 ≥ 115.3125 1
8 [115.3125,118.125] 116.71875 116 < 116.71875 0
9 [115.3125,116.71875] 116.015625 116 ≥ 116.015625 1
10 [116.015625,116.71875] 116.3671875 116 ≥ 116.3671875 1
11 [116.3671875,116.71875] 116.54296875 116 < 116.54296875 0
12 [116.3671875,116.54296875] 116.455078125 116 < 116.455078125 0
13 [116.3671875,116.455078125] 116.4111328125 116 < 116.4111328125 0

经度 bit(13 位):1101001011000

2️⃣ 纬度 bit(12 位)

位次 区间 mid 判断 bit
1 [-90,90] 0 39 ≥ 0 1
2 [0,90] 45 39 < 45 0
3 [0,45] 22.5 39 ≥ 22.5 1
4 [22.5,45] 33.75 39 ≥ 33.75 1
5 [33.75,45] 39.375 39 < 39.375 0
6 [33.75,39.375] 36.5625 39 ≥ 36.5625 1
7 [36.5625,39.375] 37.96875 39 ≥ 37.96875 1
8 [37.96875,39.375] 38.671875 39 ≥ 38.671875 1
9 [38.671875,39.375] 39.0234375 39 < 39.0234375 0
10 [38.671875,39.0234375] 38.84765625 39 ≥ 38.84765625 1
11 [38.84765625,39.0234375] 38.935546875 39 ≥ 38.935546875 1
12 [38.935546875,39.0234375] 38.9794921875 39 ≥ 38.9794921875 1

纬度 bit(12 位):101101110111

  • 交叉合并(最终 bit 序列)

按规则:经度 → 纬度 → 经度 → 纬度 …

1
2
3
4
5
经度: 1 1 0 1 0 0 1 0 1 1 0 0 0
纬度: 1 0 1 1 0 1 1 1 0 1 1 1

交叉后得到 25 bit:11 10 01 11 00 01 11 01 10 11 01 01 01
合并为一行:1110011100011101101010101
  • 每 5 bit → 1 个 Base32 字符

1
2
3
4
5
6
7
8
# 从左到右,每 5 位一组:
11100 | 11100 | 01110 | 11010 | 10101
# 转10进制
28 | 28 | 14 | 26 | 21
# 转 Base32
w | w | f | u | p
# 最终结果:
wwfup

Geohash 使用的 Base32 字符集(固定,不是 RFC Base32):

1
2
3
4
5
6
7
8
9
10
11
Index:  0 1 2 3 4 5 6 7 8 9
Char : 0 1 2 3 4 5 6 7 8 9

Index: 10 11 12 13 14 15 16 17 18 19
Char : b c d e f g h j k m

Index: 20 21 22 23 24 25 26 27 28 29
Char : n p q r s t u v w x

Index: 30 31
Char : y z
  • Geohash Base32 字符集 为什么缺少 a, i, l, o
    • 为了避免在视觉上引起数字混淆
    1
    2
    3
    4
    a 容易和 4 混淆
    i 容易和 1 混淆
    l 容易和 1 混淆
    o 容易和 0 混淆
    • 去掉 a, i, l, o 后刚好是 32 个字符,因为 Geohash 需要 2⁵ = 32 个字符 对应 5 bit 精度

Geo 核心基础操作(必用)

1. GEOADD:添加地理位置坐标(核心写入操作)

◦ 语法:

1
2
3
4
5
6
7
8
9
10
GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]
# 参数说明:
# key: key名称
# NX:如果已存在,则不执行写入操作
# XX: 如果不存在,则执行写入操作,与 NX 互斥
# CH: (CH是更改的缩写)返回新增和修改的成员数量,修改经纬度也算修改,如果不加CH,则只计算新增成员数量
# longitude: 经度 (-180 ~ 180)
# latitude: 纬度 (-85.05112878 ~ 85.05112878)
# member: 成员名称,位置唯一标识(字符串)
# 返回值:(不带CH)成功新增的 member 数量(已存在不会重复计算),(带CH)返回新增和修改的成员数量

◦ 示例:

1
2
3
4
# 给shop集合加王府井、西单2个门店的经纬度
GEOADD shop 116.403963 39.915112 wangfujing 116.391248 39.906217 xidan
# 输出
(integer) 2

◦ 关键:经纬度顺序不能反,存储后会自动给每个成员生成geohash编码。

2. GEOPOS:获取指定成员的经纬度(精准查询坐标)

◦ 语法:

1
GEOPOS key [member [member ...]]

◦ 示例:

1
2
3
4
5
# 返回王府井的经纬度数组 [经度, 纬度]
GEOPOS shop wangfujing
# 输出
1) 1) "116.40396326780319214"
2) "39.91511209922290249"

◦ 关键:返回结果与查询成员顺序一致,不存在的成员返回nil,可批量查询提升效率。

3. GEODIST:计算两个成员之间的距离(核心计算操作)

◦ 语法:

1
2
3
4
5
6
GEODIST key member1 member2 [M|KM|FT|MI]
# [M|KM|FT|MI] : 距离单位
# M : 米,默认
# KM : 千米
# FT : 英尺
# MI : 英里

◦ 示例:

1
2
3
4
5
6
7
8
9
# 返回王府井和西单之间的距离,单位为米
GEODIST shop wangfujing xidan
# 输出
"1468.0611"

# 计算王府井到西单的距离,单位千米
GEODIST shop wangfujing xidan km
# 输出
"1.4681"

◦ 关键:返回浮点型结果,成员不存在返回nil,支持跨区域距离计算(如不同城市门店)。

4. GEOHASH:获取指定成员的geohash编码(底层编码查询)

◦ 语法:

1
GEOHASH key [member [member ...]]

◦ 示例:

1
2
3
4
# 返回王府井的geohash字符串,共11位
GEOHASH shop wangfujing
# 输出
1) "wx4g0f6f2u0"

◦ 关键:geohash编码越长精度越高,Redis默认精度足够日常使用;编码相同的成员,地理位置极近,可用于快速判近。

5. GEORADIUS:按指定经纬度为中心,筛选指定范围的成员(范围查询1,按中心点坐标)

  • 即将弃用,请使用 GEOSEARCH / GEOSEARCHSTORE 替代
    ◦ 语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GEORADIUS key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key|STOREDIST key]
# 参数说明
# key: key名称
# longitude: 经度
# latitude: 纬度
# radius: 半径
# M|KM|FT|MI: 单位
## 可选参数
# WITHCOORD: 返回经纬度
# WITHDIST: 带距离
# WITHHASH: 带geohash
# COUNT count: 限制返回数量
# ANY: 随机返回数量
# ASC/DESC: 按距离正/倒序
# STORE key: 存储结果到指定key
# STOREDIST key: 存储结果到指定key,结果为距离

◦ 核心可选参数:WITHDIST(返回距离)、WITHCOORD(返回经纬度)、WITHHASH(返回geohash)、COUNT n(限制返回数量)、ASC/DESC(按距离正/倒序)

◦ 示例:

1
2
3
4
5
6
7
# 以天安门附近为中心,查5km内10个门店,按距离从近到远返回并带距离
GEORADIUS shop 116.397 39.91 5 km WITHDIST COUNT 10 ASC
# 输出
1) 1) "xidan"
2) "0.6463"
2) 1) "wangfujing"
2) "0.8223"

6. GEORADIUSBYMEMBER:按已有成员为中心,筛选指定范围的成员(范围查询2,按已有节点,更常用)

  • 即将弃用,请使用 GEOSEARCH / GEOSEARCHSTORE 替代
    ◦ 语法:

1
2
3
4
5
6
7
GEORADIUSBYMEMBER key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key|STOREDIST key]
# 语法说明:
# key: key名称
# member: 已有成员名称
# radius: 半径
# M|KM|FT|MI: 单位
## 可选参数说明同 GEORADIUS

◦ 可选参数与GEORADIUS一致,场景更贴合实际(如查“我附近”的门店,先存自己的坐标为成员,再用此命令)
◦ 示例:

1
2
3
4
5
6
7
# 查王府井3km内的门店,按距离排序
GEORADIUSBYMEMBER shop wangfujing 3 km WITHDIST ASC
# 输出
1) 1) "wangfujing"
2) "0.0000"
2) 1) "xidan"
2) "1.4681"

Geo 扩展操作(实战常用)

1. GEOSEARCH(Redis 6.2+ 新增,替代 GEORADIUS/GEORADIUSBYMEMBER)

◦ 优势:功能更全、语法更统一,支持两种查询模式,是未来主流用法

1
2
3
GEOSEARCH key <FROMMEMBER member | FROMLONLAT longitude latitude>
<BYRADIUS radius <M | KM | FT | MI> | BYBOX width height <M | KM | FT | MI>>
[ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]

◦ 语法1(按坐标中心):GEOSEARCH key FROMLONLAT 经度 纬度 BYRADIUS 距离 单位 [可选参数]

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查天安门3km内的门店,按距离排序
GEOSEARCH shop FROMLONLAT 116.397 39.91 BYRADIUS 3 km WITHDIST WITHCOORD WITHHASH ASC COUNT 10
# 输出
1) 1) "xidan"
2) "0.6463"
3) (integer) 4069885362016563
4) 1) "116.39124959707260132"
2) "39.90621776267477827"
2) 1) "wangfujing"
2) "0.8223"
3) (integer) 4069885555089518
4) 1) "116.40396326780319214"
2) "39.91511209922290249"

◦ 语法2(按成员中心):GEOSEARCH key FROMMEMBER 中心成员 BYRADIUS 距离 单位 [可选参数]

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查王府井3km内的门店,按距离排序
GEOSEARCH shop FROMMEMBER wangfujing BYRADIUS 3 km WITHDIST WITHCOORD WITHHASH ASC COUNT 10
# 输出
1) 1) "wangfujing"
2) "0.0000"
3) (integer) 4069885555089518
4) 1) "116.40396326780319214"
2) "39.91511209922290249"
2) 1) "xidan"
2) "1.4681"
3) (integer) 4069885362016563
4) 1) "116.39124959707260132"
2) "39.90621776267477827"

◦ 新增特性:支持 BYBOX(按矩形范围查询),适配更多场景(如查询某片区内的门店)。

1
2
3
4
5
6
7
8
9
# 查王府井2.0*2.0km内的门店,按距离排序,这里以 王府井 为矩形的中心,查询2.0*2.0km内的门店
# 西单距离王府井 1.4681 km,所以不在结果中
GEOSEARCH shop FROMMEMBER wangfujing BYBOX 2.0 2.0 km WITHDIST WITHCOORD WITHHASH ASC COUNT 10
# 输出
1) 1) "wangfujing"
2) "0.0000"
3) (integer) 4069885555089518
4) 1) "116.40396326780319214"
2) "39.91511209922290249"

2. GEOSEARCHSTORE(Redis 6.2+ 新增)

◦ 作用:将 GEOSEARCH 的查询结果,直接存储到指定zset中,方便后续二次处理(如分页、排序)
◦ 语法:GEOSEARCHSTORE 目标key 源key 查询条件(同GEOSEARCH)

1
2
3
4
GEOSEARCHSTORE destination source
<FROMMEMBER member | FROMLONLAT longitude latitude>
<BYRADIUS radius <M | KM | FT | MI> | BYBOX width height <M | KM | FT | MI>>
[ASC | DESC] [COUNT count [ANY]] [STOREDIST]

◦ 示例:

1
2
# 把王府井3km内的门店,存到near_shop集合
GEOSEARCHSTORE near_shop shop FROMMEMBER wangfujing BYRADIUS 3 km

底层核心与实战注意事项

  1. 底层本质:所有Geo操作的key,本质都是zset,因此zset的命令(如ZREM、ZSCORE)可直接用于Geo key,例如 ZREM shop xidan 可删除西单的地理位置(Geo无单独删除命令,依赖ZREM)。

  2. 精度限制:经纬度支持小数点后多位,但Redis内部会做精度取舍,日常场景(如打车、门店)完全够用,无需额外处理。

  3. 性能优化:大范围查询(如100km以上)建议加COUNT限制返回数量;高频查询可将结果缓存到普通key,减少Geo计算开销。

  4. 适用场景:附近门店、同城社交、物流定位,不适用高精度场景(如军事、测绘),此类场景需用专业GIS系统。

典型实战场景示例

  • 需求:搭建“附近餐饮”查询功能,支持添加餐饮坐标、查询当前位置3km内餐饮并按距离排序。

1
2
3
4
5
6
7
8
# 1. 批量添加餐饮坐标
GEOADD restaurant 116.405 39.916 aaa 116.402 39.913 bbb 116.408 39.918 ccc

# 2. 查当前位置(116.404,39.915)3km内餐饮(带距离、正序)
GEOSEARCH restaurant FROMLONLAT 116.404 39.915 BYRADIUS 3 km WITHDIST ASC

# 3. 删除某餐饮
ZREM restaurant aaa

Geo 命令

  • SpringBoot 的 StringRedisTemplate.opsForGeo() 中 Geo 数据类型 的操作方法与 Redis 原生命令的对应关系如下:

注意这里一定要用 StringRedisTemplate 来操作 Geo

写入 / 删除类操作

方法功能 方法 Redis 原始命令 备注
添加单个坐标 add(K key, Point point, M member) GEOADD key longitude latitude member 返回 新增成员数量(已存在则返回 0)
添加单个位置 add(K key, GeoLocation<M> location) GEOADD key longitude latitude member GeoLocation 内部封装了 Point + member
批量添加 add(K key, Map<M, Point> map) GEOADD key lon1 lat1 member1 [lon2 lat2 member2 ...] 推荐,一次网络 IO
批量添加 add(K key, Iterable<GeoLocation<M>> locations) GEOADD key lon1 lat1 member1 [lon2 lat2 member2 ...] 与 Map 方式等价
删除成员 remove(K key, M... members) ZREM key member [member ...] GEO 本质是 Sorted Set
1
2
3
4
5
Point point = new Point(longitude, latitude);
redisTemplate.opsForGeo().add(key, point, member);

RedisGeoCommands.GeoLocation<String> geoLocation= new RedisGeoCommands.GeoLocation<>(member, point);
redisTemplate.opsForGeo().add(key, geoLocation);

距离 / 坐标 / 哈希查询

方法功能 方法 Redis 原始命令 说明
两点距离 distance(K key, m1, m2) GEODIST key m1 m2 默认单位米
指定单位距离 distance(K key, m1, m2, metric) GEODIST key m1 m2 unit m / km / mi / ft
获取 GeoHash hash(K key, M... members) GEOHASH key member 用于调试
获取坐标 position(K key, M... members) GEOPOS key member lon / lat

半径查询(旧接口,已不推荐)

Redis 6.2 起,官方不推荐继续使用 GEORADIUS / GEORADIUSBYMEMBER,但 Spring 仍保留接口以兼容。

  • 1️⃣ 基于坐标点

方法功能 方法 Redis 原始命令
半径查询 radius(K key, Circle within) GEORADIUS key lon lat radius
半径 + 参数 radius(K key, Circle within, args) GEORADIUS
  • 2️⃣ 基于成员

方法功能 方法 Redis 原始命令
半径查询 radius(K key, member, radius) GEORADIUSBYMEMBER
指定单位 radius(K key, member, Distance) GEORADIUSBYMEMBER
带参数 radius(K key, member, Distance, args) GEORADIUSBYMEMBER

搜索查询(推荐使用 GEOSEARCH)

替代 GEORADIUS / GEORADIUSBYMEMBER

  • 1️⃣ 按圆形范围搜索

方法功能 方法 Redis 原始命令 说明
圆形搜索 search(K key, Circle within) GEOSEARCH 新推荐接口
指定参考点 search(K key, GeoReference, Distance) GEOSEARCH FROMMEMBER / FROMLONLAT
带参数 search(K key, reference, radius, args) GEOSEARCH 支持排序、limit
  • 2️⃣ 按矩形范围搜索

方法功能 方法 Redis 原始命令
矩形搜索 search(K key, reference, BoundingBox) GEOSEARCH
带参数 search(K key, reference, BoundingBox, args) GEOSEARCH
  • 通用搜索(底层能力)

方法功能 方法 Redis 原始命令
任意 GeoShape search(K key, reference, GeoShape, args) GEOSEARCH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 基于给定的坐标搜索
public void search(String key, double longitude, double latitude, double radius) {
// 中心点
Point point = new Point(longitude, latitude);
// 半径
Distance distance = new Distance(radius, RedisGeoCommands.DistanceUnit.METERS);
// 创建圆形
Circle circle = new Circle(point, distance);
// 创建地理参考
GeoReference<String> objectGeoReference = GeoReference.fromCircle(circle);
// 创建地理形状
GeoShape geoShape = GeoShape.byRadius(distance);
// 创建参数
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeCoordinates() // 返回坐标
.includeDistance() // 返回距离
.sortAscending() // 排序
.limit(10); // 限制返回数量

// 查询
final GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().search(key, objectGeoReference, geoShape, args);
if (results != null) {
results.forEach(result -> {
// 获取成员名称
System.out.println(result.getContent().getName());
// 获取坐标
System.out.println(result.getContent().getPoint());
// 获取距离
System.out.println(result.getDistance());
});
}
}

// 基于成员的坐标搜索
public void search(String key, String member, double radius) {
// 半径
Distance distance = new Distance(radius, RedisGeoCommands.DistanceUnit.METERS);
// 创建地理参考
GeoReference<String> objectGeoReference = GeoReference.fromMember(member);
// 创建地理形状
GeoShape geoShape = GeoShape.byRadius(distance);
// 创建参数
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeCoordinates() // 返回坐标
.includeDistance() // 返回距离
.sortAscending() // 排序
.limit(10); // 限制返回数量

// 查询
final GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().search(key, objectGeoReference, geoShape, args);
if (results != null) {
results.forEach(result -> {
// 获取成员名称
System.out.println(result.getContent().getName());
// 获取坐标
System.out.println(result.getContent().getPoint());
// 获取距离
System.out.println(result.getDistance());
});
}
}

搜索并存储(GEOSEARCHSTORE)

这是 搜索 + 写入 的组合操作,结果会写入新的 ZSet

  • 1️⃣ 圆形范围存储

方法功能 方法 Redis 原始命令
搜索并存储 searchAndStore(K key, destKey, Circle) GEOSEARCHSTORE
指定参考点 searchAndStore(K key, destKey, reference, radius) GEOSEARCHSTORE
带参数 searchAndStore(K key, destKey, reference, radius, args) GEOSEARCHSTORE
  • 2️⃣ 矩形范围存储

方法功能 方法 Redis 原始命令
矩形存储 searchAndStore(K key, destKey, reference, BoundingBox) GEOSEARCHSTORE
带参数 searchAndStore(K key, destKey, reference, BoundingBox, args) GEOSEARCHSTORE
  • 3️⃣ 通用 GeoShape 存储

方法功能 方法 Redis 原始命令
通用存储 searchAndStore(K key, destKey, reference, GeoShape, args) GEOSEARCHSTORE