1、Redis之GEO
1.1、简介
移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等,那这种附近各种形形色色的XXX地址位置选择是如何实现的?
地球上的地理位置是使用二维的经纬度表示,经度范围(-180,180],纬度范围(-90,90],只要我们确定一个点的经纬度就可以确定他在地球的位置。
例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,
然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近公里范围内部的车辆
使用如下SQL即可:
select taxi from position where x0-r<x<x0+r and y0-r<y<y0+r
但是这样会有什么问题呢?
1.查询性能问题,如果高并发,数据量大这种查询是要搞垮数据库的
2.这个查询的是一个矩形访问,而不是以我为中心r公里为半径的圆形访问。
3.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球这种矩形计算在长距离计算时会有很大误差
1.2、Redis在3.2版本以后增加了地理位置的处理
1.3、原理
主要分为三步
将三维的地球变为二维的坐标
在将二维的坐标转换为一维的点块
最后将一维的点块转换为二进制再通过base32编码
1.4、命令
GEOADD 多个经度(longitude),纬度(latitude),位置名称(member)添加到指定的key中
GEOPOS 从键里面返回所有给定位置元素的位置(经度和纬度)
GEODIST 返回两个给定位置之间的距离
GEORADIUS 以给定的经纬度为中心,返回与中心的距离不超过给定最大距离的所有位置元素
GEORADIUSBYMEMBER 跟GEORADIUS类似
GEOHASH 返回一个或多个位置元素的Geohash表示
1.5、命令实操
#添加经纬度坐标
127.0.0.1:6379> GEOADD city 116.404177 39.909652 "天安门" 116.407991 39.921472 "故宫" 115.939392 40.260532 "长城"
(integer) 3
#类型是zset
127.0.0.1:6379> type city
zset
#直接获取中文非正常显示
127.0.0.1:6379> ZRANGE city 0 -1
1) "\xe9\x95\xbf\xe5\x9f\x8e"
2) "\xe5\xa4\xa9\xe5\xae\x89\xe9\x97\xa8"
3) "\xe6\x95\x85\xe5\xae\xab"
127.0.0.1:6379> quit
# --raw可以正常显示中文
[root@shuidi150 bin]# ./redis-cli -h 127.0.0.1 -p 6379 --raw
127.0.0.1:6379> ZRANGE city 0 -1
长城
天安门
故宫
#返回经纬度
127.0.0.1:6379> GEOPOS city 天安门 故宫
116.40417784452438354
39.90965230984568279
116.40799194574356079
39.92147171461167687
#坐标hash编码
127.0.0.1:6379> GEOHASH city 天安门
wx4g0c6ftf0
#两个位置之间的距离 km,m距离单位
127.0.0.1:6379> GEODIST city 天安门 长城 km
55.5649
127.0.0.1:6379> GEODIST city 天安门 长城 m
55564.9228
#以半径为中心,查找附近的xxx (116.418017 39.914402 当前位置)
WITHDIST:在返回位置元素的同时,将位置素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致。
WITHCOORD:将位置元素的经度和维度也一并返回。
WITHHASH:以52位有符号整数的形式,返回位置元素经过原始 geohash编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用并不大
COUNT: 限定返回的记录数。
127.0.0.1:6379> GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
天安门
1.2935
4069885550849194
116.40417784452438354
39.90965230984568279
故宫
1.1618
4069885569588029
116.40799194574356079
39.92147171461167687
#找出位于指定范围内的元素,中心点是由给定的位置元素决定
127.0.0.1:6379>GEORADIUSBYMEMBER city 天安门 10 km withdist withcoord count 10 withhash
天安门
0.0000
4069885550849194
116.40417784452438354
39.90965230984568279
故宫
1.3543
4069885569588029
116.40799194574356079
39.92147171461167687
2、案例实战:美团地图位置附近的酒店推送
1.微信附近的人或者一公里以内的各种营业厅、加油站、理发店、超市....
2.摇个妹子
3.找个单车
4.附近的酒店
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Circle;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class GeoController
{
public static final String CITY ="city";
@Autowired
private RedisTemplate redisTemplate;
/**
* 新增天安门故宫长城经纬度
*/
@RequestMapping(value = "/geoadd",method = RequestMethod.POST)
public String geoAdd()
{
Map<String, Point> map= new HashMap<>();
map.put("天安门",new Point(116.403963,39.915119));
map.put("故宫",new Point(116.403414 ,39.924091));
map.put("长城" ,new Point(116.024067,40.362639));
redisTemplate.opsForGeo().add(CITY,map);
return map.toString();
}
/**
* 获取地理位置的坐标
* @param member
* @return
*/
@RequestMapping(value = "/geopos",method = RequestMethod.GET)
public Point position(String member) {
//获取经纬度坐标
List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
return list.get(0);
}
/**
* geohash算法生成的base32编码值
* @param member
* @return
*/
@RequestMapping(value = "/geohash",method = RequestMethod.GET)
public String hash(String member) {
//geohash算法生成的base32编码值
List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
return list.get(0);
}
/**
* 计算两个位置之间的距离
* @param member1
* @param member2
* @return
*/
@RequestMapping(value = "/geodist",method = RequestMethod.GET)
public Distance distance(String member1, String member2) {
Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
/**
* 通过经度,纬度查找附近的
* 北京王府井位置116.418017,39.914402,这里为了方便讲课,故意写死
*/
@RequestMapping(value = "/georadius",method = RequestMethod.GET)
public GeoResults radiusByxy() {
//这个坐标是北京王府井位置
Circle circle = new Circle(116.418017, 39.914402, Metrics.MILES.getMultiplier());
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
return geoResults;
}
/**
* 通过地方查找附近
*/
@RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
public GeoResults radiusByMember() {
String member="天安门";
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
//半径10公里内
Distance distance=new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
return geoResults;
}
}