一、概述
Redis的Geo功能主要用于存储地理位置信息,并对其进行操作。该功能在Redis 3.2版本新增。Redis Geo操作方法包括:
- geoadd:添加地理位置的坐标;
- geopos:获取地理位置的坐标;
- geodist:计算两个位置之间的距离;
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合;
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合;
- geohash:返回一个或多个位置对象的geohash值。
二、Redis Geo功能案例:
1、案例1
查找某个城市下的门店信息。
比如有的一家门店存储的Redis结构如下:
store:
- member: poscon
- member: shangxixxi
其中poscon表示该门店的坐标信息,shangxixxi表示该门店的名称。
现在需要查找某个城市下的门店信息,可以使用Redis的Geo功能实现:
GEOADD store 116.406890 39.909195 "poscon" "shangxixxi"
GEOADD store 116.420853 39.892662 "poscon1" "shangxixxi1"
GEOADD store 116.401415 39.915726 "poscon2" "shangxixxi2"
GEOADD store 116.426988 39.919938 "poscon3" "shangxixxi3"
GEOADD store 116.410057 39.904425 "poscon4" "shangxixxi4"
GEORADIUS store 116.409729 39.908256 10000km WITHCOORD WITHDIST
以上代码表示在(116.409729, 39.908256)半径为10000km的圆内查找门店信息,返回结果会包含每个门店的坐标信息和距离。可以根据返回结果筛选出自己需要的门店信息。
2、案例2
添加下面几条数据:
- 北京南站 ( 116.378248 39.865275
- 北京站 ( 116.42803 39.903738 )
- 北京西站(116.322287 39.893729 )
127.0.0.1:6379> GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx
(integer) 3
- 计算北京西站到北京站的距离
127.0.0.1:6379> GEODIST g1 bjz bjx km
"9.0916"
- 搜索天安门(116.397904 39.909005 )附近1km内的所有火车站,并按照距离升序排序
127.0.0.1:6379> GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST
1) 1) "bjz"
2) "2.6361"
2) 1) "bjn"
2) "5.1452"
3) 1) "bjx"
2) "6.6723"
- 计算北京西的坐标与hash值
127.0.0.1:6379> GEOPOS g1 bjz
1) 1) "116.42802804708480835"
2) "39.90373880538094653"
127.0.0.1:6379> GEOHASH g1 bjz
1) "wx4g12k21s0"
三、实战搜索附近商铺
1、接口名称
2、商家列表展示距离
当我们点击美食之后,会出现一系列的商家,商家中可以按照多种排序方式,我们此时关注的是距离,这个地方就需要使用到我们的GEO,向后台传入当前app收集的地址(我们此处是写死的) ,以当前坐标作为圆心,同时绑定相同的店家类型type,以及分页信息,把这几个条件传入后台,后台查询出对应的数据再返回。
如图:
3、约定数据存储规则
我们要做的事情是:将数据库表中的数据导入到redis中去,redis中的GEO,GEO在redis中就一个menber和一个经纬度,我们把x和y轴传入到redis做的经纬度位置去,但我们不能把所有的数据都放入到menber中去,毕竟作为redis是一个内存级数据库,如果存海量数据,redis还是力不从心,所以我们在这个地方存储他的id即可。
但是这个时候还有一个问题,就是在redis中并没有存储type,所以我们无法根据type来对数据进行筛选,所以我们可以按照商户类型做分组,类型相同的商户作为同一组,以typeId为key存入同一个GEO集合中即可
- 测试:
void loadShopData() {
// 1.查询店铺信息
List<Shop> list = shopService.list();
// 2.把店铺分组,按照typeId分组,typeId一致的放到一个集合
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
// 3.分批完成写入Redis
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
// 3.1.获取类型id
Long typeId = entry.getKey();
String key = SHOP_GEO_KEY + typeId;
// 3.2.获取同类型的店铺的集合
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
// 3.3.写入redis GEOADD key 经度 纬度 member
for (Shop shop : value) {
// stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(), shop.getY())
));
}
stringRedisTemplate.opsForGeo().add(key, locations);
}
}
- 结果:
4、实现附近商户功能
4.1 第一步:导入pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-data-redis</artifactId>
<groupId>org.springframework.data</groupId>
</exclusion>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.6.RELEASE</version>
</dependency>
4.2 第二步:写接口ShopController
传入参数:
- typeId:商品类型id
- current: 当前页码
- x : 经度
- y : 纬度
@GetMapping("/of/type")
public Result queryShopByType(
@RequestParam("typeId") Integer typeId,
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam(value = "x", required = false) Double x,
@RequestParam(value = "y", required = false) Double y
) {
return shopService.queryShopByType(typeId, current, x, y);
}
4.3 第三步:实现
- 判断是否需要根据坐标查询
- 计算分页参数
- 查询redis、按照距离排序、分页。结果:shopId、distance
- 解析出id
- 根据id查询Shop
- 返回店铺数据
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// 1.判断是否需要根据坐标查询
if (x == null || y == null) {
// 不需要坐标查询,按数据库查询
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
return Result.ok(page.getRecords());
}
// 2.计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
// 3.查询redis、按照距离排序、分页。结果:shopId、distance
String key = SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 4.解析出id
if (results == null) {
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
if (list.size() <= from) {
// 没有下一页了,结束
return Result.ok(Collections.emptyList());
}
// 4.1.截取 from ~ end的部分
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result -> {
// 4.2.获取店铺id
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
// 4.3.获取距离
Distance distance = result.getDistance();
distanceMap.put(shopIdStr, distance);
});
// 5.根据id查询Shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
// 6.返回
return Result.ok(shops);
}
四、源码下载
https://gitee.com/charlinchenlin/koo-erp