首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >第七章 LBS及GEO介绍

第七章 LBS及GEO介绍

原创
作者头像
RookieCyliner
发布2025-06-08 22:25:40
发布2025-06-08 22:25:40
1760
举报
文章被收录于专栏:redisredis

LBS出现的背景

  1. 移动互联网时代LBS应用越来越多,所在位置附近三公里的药店、交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等,那这种附近各种形形色色的XXX地址位置选择是如何实现的?
  2. 地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],只要我们确定一个点的经纬度就可以名曲他在地球的位置
  3. 例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,
  4. 然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近r公里范围内部的车辆
代码语言:txt
复制
# 使用如下SQL即可
select taxi from position where x0-r < x < x0 + r and y0-r < y < y0+r
  • 但是这样会有什么问题呢?
  1. 查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库的
  2. 这个查询的是一个矩形访问,而不是以我为中心r公里为半径的圆形访问
  3. 精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差

重新认识经纬度

  • 经纬度:经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置

经纬度
经纬度

经线和纬线

  • 是人们为了在地球上确定位置和方向的,在地球仪和地图上画出来的,地面上并线
  • 和经线相垂直的线叫做纬线(纬线指示东西方向)。纬线是一条条长度不等的圆圈。最长的纬线就是赤道
  • 因为经线指示南北方向,所以经线又叫子午线。 国际上规定,把通过英国格林尼治天文台原址的经线叫做0°所以经线也叫本初子午线
  • 在地球上经线指示南北方向,纬线指示东西方向

经度、纬度

  • 经度(longitude):东经为正数,西经为负数。东西经。
  • 通常说的纬度是大地纬度。其数值在0—90度之间。位于赤道以北的点的纬度叫北纬,记为N;位于赤道以南的点的纬度称南纬,记为S。
经纬度
经纬度
经纬线
经纬线

在一定误差范围内,通常情况下,经纬线和米的换算为:经度或者纬度0.00001度,约等于1米。以下表格列出更细致的换算关系

经纬线和米的换算关系
经纬线和米的换算关系

认识GeoHash

GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存

区域信息
区域信息

字符串越长,表示的范围越精确。如图所示,5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)

10平方千米范围的矩形区域
10平方千米范围的矩形区域

字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,一个在城区,一个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串相似程度要低些

区域信息
区域信息

通过上面的介绍我们知道了GeoHash就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近,回到我们的案例,根据所在位置查询来查询附近餐馆时,只需要将所在位置经纬度转换成GeoHash字符串,并与各个餐馆的GeoHash字符串进行前缀匹配,匹配越多的距离越近

Geohash算法介绍

  1. GeoHash是空间索引的一种方式,其基本原理是将地球理解为一个二维平面,通过把二维的空间经纬度数据编码为一个字符串,可以把平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码
  2. 以GeoHash方式建立空间索引,可以提高对空间poi数据进行经纬度检索的效率
  3. 编码规则为:先将纬度范围(-90, 90)平分成两个区间(-90, 0)和(0, 90),如果目标维度位于前一个区间,则编码为0,否则编码为1,然后根据目标纬度所落的区间再平均分成两个区间进行编码,以此类推,直到精度满足要求,经度也用同样的算法,对(-180, 180)依次细分,然后合并经度和纬度的编码,奇数位放纬度,偶数位放经度,组成一串新的二进制编码,按照Base32进行编码

目前Geohash使用的精度说明如下:

Geohash使用的精度说明
Geohash使用的精度说明
  1. GeoHash用一个字符串表示经度和纬度两个坐标, 比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于某位置附近,又不至于暴露自己的精确坐标,有助于隐私保护
  2. 编码过程中,通过二分范围匹配的方式来决定某个经纬坐标是编码为1还是0,因此某些邻近坐标的编码是相同的,因此GeoHash表示的并不是一个点,而是一个矩形区域。 GeoHash编码的前缀可以表示更大的区域。例如wm3vzg,它的前缀wm3vz表示包含编码wm3vzg在内的更大范围。 这个特性可以用于附近地点搜索
  3. 如果把某个区域或整个地图上的地理位置都按照Geohash编码,则会得到一个网格,编码递归粒度越细,网格的矩形区域越小,geohash编码的长度越大,则Geohash编码越精确。 不同的编码长度,生成的网格与实际地理的精度如下(Geohash字符串编码长度对应网格大小)
geohash算法长度与精度关系
geohash算法长度与精度关系

邻近网格位置

根据Geohash的编码规则将经纬度分解到二进制,结合地理常识,中心网格在南北(上下)方向上体现为纬度的变化,往北则维度的二进制加1,往南则维度的二进制减1,在东西(左右)方向上体现为经度的变化,往东则经度的二进制加1,往西则减1,可以计算出上下左右四个网格经纬度的二进制编码,再将加减得出的经纬度两两组合,计算出左上、左下、右上和右下四个网格的经纬度二进制编码,从而就可以根据Geohash的编码规则计算出周围八个网格的字符串

添加元素 - GEOADD

  • 添加:GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member …]
  • 该命令的参数格式是固定的,即(longitude latitude member),经度要在纬度之前
  • GEOADD 坐标是有限的: 非常接近两极的区域是无法被索引的。坐标被 EPSG:900913 / EPSG:3785 / OSGEO:41001规范限制, 合法值如下:
代码语言:txt
复制
有效的经度介于-180度至180度之间
有效的纬度介于-85.05112878度至85.05112878度之间

代码语言:txt
复制
# 当给定的经纬度超出上述合法范围时,会返回error
127.0.0.1:6379> geoadd city 89.0 99 nanji
(error) ERR invalid longitude,latitude pair 89.000000,99.000000
  • Redis GEO没有删除命令 GEODEL,因为底层使用的是Sorted Set,所以完全可以使用ZREM 命令删除
代码语言:txt
复制
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing  121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3

返回经纬度 - GEOPOS

  1. 命令格式:GEOPOS key member [member …]
  2. 返回给定元素对应的经纬度
  3. 使用GEOADD添加的元素,会被GeoHash转化为52位比特值,因此使用GEOPOS取出值并转为经纬度时,可能与添加的经纬度值有少许差异
  4. 命令接收多个可变参数,返回值始终是数组形式;数组:存在的元素返回经纬度,不存在的元素返回nil
代码语言:txt
复制
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing  121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3

127.0.0.1:6379> geopos city beijing nanjing
1) 1) "116.41667157411575317"
   2) "39.91667095273589183"
2) (nil)

返回距离 - GEODIST

  1. 命令格式:GEODIST key member1 member2 [m|km|ft|mi]
  2. 返回两个给定元素之间的距离
  3. 距离度量支持如下参数 :m: 米(默认值)、km: 千米、ft: 英尺、mi:英里。
  4. 在计算距离时会假设地球为完美的球形,在极限情况下最大会造成 0.5%的误差
  5. 如果给定的元素中,有元素不存在,返回nil
代码语言:txt
复制
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing  121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3

127.0.0.1:6379> geodist city beijing shanghai
"748346.9287"
127.0.0.1:6379> geodist city beijing shanghai km
"748.3469"

返回哈希 - GEOHASH

  • 命令格式:GEOHASH key member [member …]
  • 返回给定元素,GeoHash编码值对应的字符串表示
代码语言:txt
复制
127.0.0.1:6379> geohash city beijing 
1) "wx4g14s53n0"

半径 - GEORADIUS

  • georadius:以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素
  • 命令:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
代码语言:txt
复制
WITHDIST: 在返回位置元素的同时,将位置元素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致
WITHCOORD: 将位置元素的经度和维度也一并返回
WITHHASH: 以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值这个选项主要用于底层应用或者调试,实际中的作用并不大
COUNT限定返回的记录数
代码语言:txt
复制
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
127.0.0.1:6379> GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
1) 1) "beijing"
   2) "0.2772"
   3) (integer) 4069885649163649
   4) 1) "116.41667157411575317"
      2) "39.91667095273589183"
  • 简单来说就是查询指定位置一定距离内的元素,例如查询当前位置5公里内的银行

GEORADIUSBYMEMBER

  • 命令格式:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  • 该命令与GEORADIUS命令基本一样,唯一不同的是:GEORADIUSBYMEMBER给定的是Sorted Set中的一个元素,而GEORADIUS给定的是具体经纬度。通过给定元素,其实就可以得到存储的经纬度,进而进行查询
代码语言:txt
复制
127.0.0.1:6379> geoadd city 116.408 39.904 beijing 116.298 39.959 haidian 116.443 39.922 chaoyang 121.445 31.213 shanghai 121.23 31.07 minhang 117.246 39.117 tianjin
(integer) 6

127.0.0.1:6379> GEORADIUSBYMEMBER city beijing 500 km withdist asc
1) 1) "beijing"
   2) "0.0000"
2) 1) "chaoyang"
   2) "3.5948"
3) 1) "haidian"
   2) "11.2004"
4) 1) "tianjin"
   2) "113.2837"

美团地图位置附近的酒店推送

需求分析:

  1. 微信附近的人或者一公里以内的各种营业厅、加油站、理发店、超市…
  2. 附近的酒店
  3. 某公司附近三公里药店查询

核心代码逻辑编写

代码语言:txt
复制
@RestController
public class GeoController {
    public  static final String CITY ="city";

    @Autowired
    private RedisTemplate redisTemplate;

    @ApiOperation("新增天安门故宫长城经纬度")
    @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();
    }

    @ApiOperation("获取地理位置的坐标")
    @RequestMapping(value = "/geopos",method = RequestMethod.GET)
    public Point position(String member) {
        //获取经纬度坐标
        List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
        return list.get(0);
    }

    @ApiOperation("geohash算法生成的base32编码值")
    @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);
    }

    @ApiOperation("计算两个位置之间的距离")
    @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,这里为了方便讲课,故意写死
     */
    @ApiOperation("通过经度,纬度查找附近的")
    @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;
    }

    /**
     * 通过地方查找附近
     */
    @ApiOperation("通过地方查找附近")
    @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
    public GeoResults radiusByMember() {
        String member="天安门";
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
        //RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortDescending().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;
    }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • LBS出现的背景
  • 重新认识经纬度
  • 认识GeoHash
  • Geohash算法介绍
  • 邻近网格位置
  • 添加元素 - GEOADD
  • 返回经纬度 - GEOPOS
    • 返回距离 - GEODIST
  • 返回哈希 - GEOHASH
  • 半径 - GEORADIUS
  • GEORADIUSBYMEMBER
  • 美团地图位置附近的酒店推送
    • 需求分析:
    • 核心代码逻辑编写
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档