五、Redis新类型之 GEO
一、简介
GEO是Redis处理地理坐标的数据类型。
地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],只要我们确定一个点的经纬度就可以名曲他在地球的位置。
例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近r公里范围内部的车辆
使用如下SQL:
select taxi from position where x0-r < x < x0 + r and y0-r < y < y0+r
但是这样会有什么问题呢?
- 查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库的。
- 这个查询的是一个矩形访问,而不是以我为中心r公里为半径的圆形访问。
- 精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差。
二、原理
核心思想就是将球体转换为平面,区块转换为一点。
主要分为三步:
1.将三维的地球变为二维的坐标。
2.再将二维的坐标转换为一维的点块。
3.最后将一维的点块转换为二进制再通过base32编码。
GeoHash核心原理解析,参考:https://www.cnblogs.com/LBSer/p/3310455.html
三、基础知识
经度和维度
经度(longitude):东西经,东经为正数,西经为负数, (-180, 180]。
纬度(latitude):南北纬,北纬为正数,南纬为负数,(-90, 90]。
四、常用命令
如何获得某个地址的经纬度:
- http://api.map.baidu.com/lbsapi/getpoint/
- https://jingweidu.bmcx.com/
1、GEOADD
geoadd,用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中。
语法:
GEOADD key longitude latitude member [longitude latitude member ...]
2、GEOPOS
geopos,用于从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回null。
语法:
GEOPOS key member [member ...]
3、GEOHASH
Redis GEO使用 geohash 来保存地理位置的坐标。geohash 命令,用于获取一个或多个位置元素的 geohash 值。
语法:
GEOHASH key member [member ...]
4、GEODIST
geodist,用于返回两个给定位置之间的距离。
语法:
GEODIST key member1 member2 [m|km|ft|mi] #单位
5、GEORADIUS(核心)
georadius,以给定的经纬度为中心,返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
语法:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUS Sicily 15 37 200 km WITHDIST
1) 1) "Palermo"
2) "190.4424"
2) 1) "Catania"
2) "56.4413"
redis> GEORADIUS Sicily 15 37 200 km WITHCOORD
1) 1) "Palermo"
2) 1) "13.361389338970184"
2) "38.115556395496299"
2) 1) "Catania"
2) 1) "15.087267458438873"
2) "37.50266842333162"
redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
2) "190.4424"
3) 1) "13.361389338970184"
2) "38.115556395496299"
2) 1) "Catania"
2) "56.4413"
3) 1) "15.087267458438873"
2) "37.50266842333162"
范围可以使用以下其中一个单位:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
WITHDIST
::在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。WITHCOORD
::将位置元素的经度和维度也一并返回。WITHHASH
::以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
ASC:
根据中心的位置, 按照从近到远的方式返回位置元素。DESC:
根据中心的位置, 按照从远到近的方式返回位置元素。
6、GEORADIUSBYMEMBER(核心)
GEORADIUSBYMEMBER ,和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER
的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心。
语法:
redis> GEOADD Sicily 13.583333 37.316667 "Agrigento" (integer) 1 redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km 1) "Agrigento" 2) "Palermo" redis>
五、Demo
@RestController @Slf4j 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(); } @GetMapping(value = "/geopos") public Point position(String member) { //获取经纬度坐标 List<Point> list = this.redisTemplate.opsForGeo().position(CITY, member); return list.get(0); } @GetMapping(value = "/geohash") public String hash(String member) { //geohash算法生成的base32编码值 List<String> list = this.redisTemplate.opsForGeo().hash(CITY, member); return list.get(0); } @GetMapping(value = "/geodist") 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 */ @GetMapping(value = "/georadius") public GeoResults radiusByxy() { //这个坐标是北京王府井位置 Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier()); //返回50条 RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50); GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = this.redisTemplate.opsForGeo().radius(CITY, circle, args); return geoResults; } /** * 通过地方查找附近 */ @GetMapping(value = "/georadiusByMember") public GeoResults radiusByMember() { String member = "天安门"; //返回50条 RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50); //半径10公里内 Distance distance = new Distance(10, Metrics.KILOMETERS); GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = this.redisTemplate.opsForGeo().radius(CITY, member, distance, args); return geoResults; }
}