目录
1. GEOADD
2. GEODIST
3. GEOHASH
3. GEOHASH
4. GEOPOS
6. GEOSEARCH
7. GEOSEARCHSTORE
应用场景
代码的逻辑分解:
比较难懂的部分:
Redis GEO 查询与分页
results 的结构:
分页处理与截取数据
附加距离信息
1. GEOADD
功能:向指定的 key 中添加地理空间信息。
参数:
- 经度(longitude):地理位置的经度(范围:-180 到 180)。
- 纬度(latitude):地理位置的纬度(范围:-85.05112878 到 85.05112878)。
- 值(member):与此经纬度相关联的唯一标识符。
2. GEODIST
功能:计算两个位置之间的距离。
参数:
- 第一个 member。
- 第二个 member。
- 距离单位(可选):
m
(米)、km
(千米)、mi
(英里)、ft
(英尺)。 - 结果:返回两点之间的距离,单位为指定的单位。
3. GEOHASH
功能:返回指定成员的 GeoHash 值。
GeoHash 是一种将经纬度编码为字符串的算法,用于地理位置的高效存储和查询。
参数:
- 一个或多个 member。
结果:返回两点之间的距离,单位为指定的单位。
3. GEOHASH
功能:返回指定成员的 GeoHash 值。
GeoHash 是一种将经纬度编码为字符串的算法,用于地理位置的高效存储和查询。
参数:
- 一个或多个 member。
结果:返回一个字符串表示的 GeoHash 值。
4. GEOPOS
功能:返回指定成员的经纬度。
参数:
- 一个或多个 member。
结果:返回对应的经纬度数组,例如
[13.361389, 38.115556]
6. GEOSEARCH
功能:在指定的范围内搜索成员。
- 参数:
- 中心点(可以是经纬度或某个 member)。
- 范围(单位:
m
、km
等)。 - 排序规则(
ASC
或DESC
)。 - 可选参数:
WITHDIST
、WITHCOORD
。
7. GEOSEARCHSTORE
功能:将 GEOSEARCH
的结果存储到新的 key 中。
- 参数:
- 目标 key:存储结果的 key。
- 源 key:原始数据的 key。
- 查询条件:与
GEOSEARCH
相同。
应用场景
- 基于位置的服务(LBS):例如,寻找某地点附近的商店、餐馆或加油站。
- 物流管理:计算两个地址之间的距离。
- 社交应用:匹配同一城市或区域的用户。
注意事项:
- GEO 数据结构在存储时使用的是 Redis 的有序集合(Sorted Set),经纬度被编码为 52 位的 GeoHash,然后作为分值(score)存储。
- 支持的查询范围有限,主要适用于地球范围内的点查询和距离计算。
代码的逻辑分解:
-
判断是否存在地理坐标 (
x
和y
):if(x == null && y == null){ // 数据库分页查询 }
- 如果
x
和y
均为空,说明不需要按地理位置查询店铺,此时直接从数据库中按店铺类型(typeId
)分页查询。 - 分页参数:
current
为当前页,分页大小为常量SystemConstants.DEFAULT_PAGE_SIZE
。
数据库分页查询逻辑:
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
处理带有地理坐标的情况: 如果提供了地理坐标,则按以下步骤处理:
-
计算分页范围:
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
-
根据当前页计算数据截取的起始位置(
from
)和结束位置(end
)。 -
从 Redis 查询 GEO 数据:
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search( key, GeoReference.fromCoordinate(x, y), // 圆心为地理坐标x, y new Distance(5000), // 搜索范围5km RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end) );
-
通过 Redis 的 GEO 查询:
- 按照距离排序。
- 限制返回的最大结果数量为
end
。 - 返回结果中包含店铺 ID 和距离。
-
检查 Redis 查询结果是否为空:
if(results == null) return Result.ok(Collections.emptyList());
截取分页内容:
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent(); if(content.size() < from) return Result.ok(Collections.emptyList());
检查总结果是否足够多以满足当前分页,如果不足则返回空列表。
提取店铺 ID 和距离:
content.stream().skip(from).forEach(result -> {
String shopId = result.getContent().getName(); // 获取店铺ID
Distance distance = result.getDistance(); // 获取距离
ids.add(Long.valueOf(shopId));
distanceMap.put(shopId, distance);
});
- 使用
skip(from)
跳过from
之前的结果。 - 将分页内的店铺 ID 和距离分别存入
ids
和distanceMap
。
附加距离信息:
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
返回查询结果:
return Result.ok(shops);
比较难懂的部分:
Redis GEO 查询与分页
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
key, // Redis 中存储地理位置数据的键
GeoReference.fromCoordinate(x, y), // 查询的圆心坐标 (经度, 纬度)
new Distance(5000), // 查询的半径范围为 5000 米(5 公里)
RedisGeoCommands.GeoSearchCommandArgs // 额外的查询参数
.newGeoSearchArgs()
.includeDistance() // 返回结果中包含距离
.limit(end) // 限制返回的最多结果数为 end
);
- 作用: 使用 Redis 的 GEO 数据结构查询指定范围内的地理位置,并按距离排序。
limit(end)
表示查询的结果最多为end
条。 - 这段代码的核心作用是基于地理位置的店铺查询:
- 找到以
(x, y)
为中心,半径 5 公里的店铺。 - 按照距离排序,最多返回
end
条记录。 - 每条记录包含店铺 ID 和距离信息。
- 找到以
results
是 GeoResults<RedisGeoCommands.GeoLocation<String>>
类型,表示 Redis GEO 查询的结果集。它包含了多个 GeoResult
对象,每个 GeoResult
对应一个位置的详细信息。
我们来具体说明 results
中的内容及其结构。
results
的结构:
results
的类型是 GeoResults<RedisGeoCommands.GeoLocation<String>>
,它包含:
content
:查询结果的列表,是一个List<GeoResult<RedisGeoCommands.GeoLocation<String>>>
。
每个 GeoResult
包括:
content
:具体的位置信息,类型是RedisGeoCommands.GeoLocation<String>
。- 包含位置的唯一标识(如店铺 ID)。
distance
:从查询圆心到该位置的距离,类型是Distance
。
results
├── content: List<GeoResult<RedisGeoCommands.GeoLocation<String>>>
├── GeoResult 1
│ ├── content: RedisGeoCommands.GeoLocation<String>
│ │ ├── name: "101" // 店铺 ID
│ │ ├── point: Point(x=120.5, y=30.0) // 经纬度坐标
│ ├── distance: Distance(value=1200.0, unit=METERS) // 距离
├── GeoResult 2
│ ├── content: RedisGeoCommands.GeoLocation<String>
│ │ ├── name: "102"
│ │ ├── point: Point(x=120.6, y=30.1)
│ ├── distance: Distance(value=1500.0, unit=METERS)
└── ...
- 难点:
- Redis GEO 查询结果不直接支持分页,因此需要手动跳过一部分数据(
skip(from)
)以实现分页效果。
- Redis GEO 查询结果不直接支持分页,因此需要手动跳过一部分数据(
分页处理与截取数据
以下代码的主要作用是从 content
中提取分页后的店铺信息,并将店铺的 ID 和 距离 分别存储到两个集合中,供后续使用。distanceMap
是一个 Map<String, Distance>
,用于记录每个店铺的 ID 和距离。ids
是一个 ArrayList<Long>
,存储所有分页后的店铺 ID。
content.stream().skip(from).forEach(result -> {
String shopId = result.getContent().getName();
Distance distance = result.getDistance();
ids.add(Long.valueOf(shopId));
distanceMap.put(shopId, distance);
});
- 作用: 手动处理分页逻辑,跳过
from
条数据,提取目标页的数据。 - 难点: 理解为什么需要
skip(from)
,因为 Redis 查询结果已经包含了end
条数据,但需要从from
开始取当前页。
附加距离信息
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
- 遍历从数据库查询出来的每个店铺。
- 根据店铺的 ID,从
distanceMap
中找到对应的距离值。 - 将距离值设置到当前店铺对象的
distance
属性中。