8. 使用Redis查询附近的人或商家

楔子

查询附近的人或者附近的商家等等是一个非常常用并且实用的功能,比如:我们经常使用高德地图、百度地图或者其它地图,去查询我们想去的目的地在什么位置,并且还会显示距离。如果我们去的地方有多个,比如我们想去招商银行,但如果附近有多个招商银行,那么地图会显示附近的所有银行,并默认按照距离进行排序,然后我们可以选择离我们最近的一个。

我们以在美团上搜索自助餐为例:

我们看到美团帮我们把商店和离我们当前位置的距离都显示在了上面,当然它没有纯粹地按照距离排序,而是按照距离以及其它指标(比如:价格、人气、评分等等)进行的综合排序。当然它怎么排序的我们不关心,我们想要知道的是如何查询附近的人、或者商店呢?

在Redis3.2版本中,提供了一个新的类型:GEO,用于存储和查询地理位置,一些相关操作可以帮我们实现这一点。不过在介绍GEO类型以及命令使用之前,我们必须要了解一下关于经纬度方面的地理知识,否则下面的内容学起来会很费劲。

经纬度

我们都知道,地球上的任何一个位置都可以使用经度和纬度来标识:纬度的范围是-90度到90度,经度的范围是-180度到180度。纬度以赤道为界,赤道以北纬度为正数,赤道以南纬度为负数;经度以本初子午线(英国格林尼治天文台)为界,东边为正数,西边为负数。

首先纬线和经线都有无数条,假定地球是一个规则的球体,那么我们看到所有的纬线都是一个和赤道平行的圆,并且它们还是一个同心圆。当然赤道也是纬线、也是一个圆,并且它是最长的一条纬线,只是为了便于标记,把这条最长的纬线称之为赤道。并且纬线越靠近南北两极,其周长就越小,直到缩短为0。

而经线显然长度都是一样的,将南极和北极两点相连得到的线就是经线,经线就是球上的一个半圆,当然图上面画的不标准。而本初子午线显然也属于经线,它的长度和其它经线也是一样的,并且如果将经线对应的半圆补齐,那么所有经线对应的圆都和赤道垂直,并且它们的圆心都重合。

总结一下纬线和经线

  • 指示方向:纬线指示东西方向,经线指示南北方向。
  • 是否等长:纬线是不等长的,赤道最长,越靠近两极,长度越短,直到为0;而所有经线的长度是都是相等的。
  • 是圆还是半圆:纬线自成一个圆,所有纬线都是彼此平行的同心圆,同一条纬线上的所有纬度都是一样的;而经线是连接南北两极组成的半圆,同一条经线上的所有经度也是一样的。但我们说经线是一个半圆,如果将这个半圆补齐,那么这个圆上的两条经线上的度数相加正好等于180。比如:两条经线组成了一个和赤道垂直的一个圆,其中一条经线的度数是西经20度,那么另一条经线的度数就是东经160度。

所以我们如果想把地球按照南北分开,只需要一条纬线即可,因为它是一个圆;但是想按照东西分开,则需要两条经线,两条经线组成一个圆,并且这两条经线上的度数相加为180,一个是东经一个是西经,如果相加不等于180,那么这两条经线不可能会组成一个圆。

另外从这里我们也看出为什么纬度是-90到90,而经度是-180到180。首先绕地球一圈相当于转了360度,而经线是个半圆,想象本初子午线绕着地球旋转,它必须要完整的转一圈才可以会到原来的位置,所以是360(根据方向分为-180到180);而纬线的话,想象一下赤道从北极跑到南极,所以是180,因为纬线本身就是个圆。或者我们只看纬线的一半的话,那么相当于它只围绕着地球转了半圈,因此根据方向的话,范围是-90到90。

南、北半球

  • 赤道以北称为北半球(赤道上方,纬度为正),赤道以南称为南半球(赤道下方,纬度为负)。所以一般纬度为负数的话,比如-78,我们会说南纬78度;纬度为正的话,比如87,我们会说北纬87度。
  • 本初子午线的东边经度为正,西边为负。所以经度为负数的话,比如-32,我们会说西经32度;经度为正数的话,比如54,我们会说东经54度。但是东半球和西半球并不是以本初子午线为准,而是以西经20度、东经160度为分界线。西经20度以东、东经160度以西的部分是东半球;西经20度以西、东经160度以东的部分是西半球。不好理解的话,想象一下上北下南左西右东,而且地球是自西向东转的,也就是按照逆时针转动的。西经20度的经线逆时针转动(向东)、东经160度所在经线顺时针转动(向西),直到重合,然后它们扫过的部分就是东半球;西半球同理,就是西经20度所在经线顺时针转动、东经160度所在经线逆时针转动,扫过的部分。

有了纬度和经度,我们就能表示地球上的任何一个位置,并且还可以计算两个位置之间的距离。

GEO类型以及相关操作

所以我们只需要查询出附近几个点和自己的距离,再进行排序就可以实现查询附近人的功能了,然而使用 Redis 让这一切更简单了,Redis 为我们提供了专门用来存储地理位置的类型 GEO,我们使用它以及它所内置的方法就可以轻松地实现查询附近的人了。

GEO类型是专门用来存储地理位置信息的,不过关于 GEO 的命令不多,主要包含以下 6 个:

  • 1. geoadd:添加地理位置
  • 2. geopos:查询位置信息
  • 3. geodist:距离统计
  • 4. georadius:查询某位置内的其他成员信息
  • 5. geohash:查询位置的哈希值
  • 6. zrem:删除地理位置

添加地理位置

关于添加的命令可以使用:geoadd key 经度1 纬度1 地点1 经度2 纬度2 地点2···,一次性可以添加多个。

我们先用百度地图提供的经纬度查询工具,地址:http://api.map.baidu.com/lbsapi/getpoint/index.html,随便找4个点吧。

  • 天安门:116.404269,39.913164
  • 月坛公园:116.36,39.922461
  • 北京欢乐谷:116.499705,39.874635
  • 香山公园:116.193275,39.996348

然后进行添加,代码如下:

127.0.0.1:6379> geoadd position 116.404269 39.913164 tian_an_men
(integer) 1
127.0.0.1:6379> geoadd position 116.36 39.922461 yue_tan_gong_yuan
(integer) 1
127.0.0.1:6379> geoadd position 116.499705 39.874635 bei_jing_huan_le_gu
(integer) 1
127.0.0.1:6379> geoadd position 116.193275 39.996348 xiang_shan_gong_yuan
(integer) 1
127.0.0.1:6379> 

查询位置信息

添加的时候,使用命令:geoadd key 经度 维度 地点,查询的时候通过,命令:geopos key 地点即可返回经纬度。

127.0.0.1:6379> geopos position tian_an_men xiang_shan_gong_yuan
1) 1) "116.40426903963088989"
   2) "39.91316289865137179"
2) 1) "116.19327574968338013"
   2) "39.99634737765855874"
127.0.0.1:6379> # 可以同时返回多个 

距离统计

如何计算两个位置之间的距离呢?使用命令:geodist key 位置1 位置2 km即可算出两个位置之间距离多少千米,结尾的km表示单位,这里选择千米。除了km之外,还有m表示米、mi表示英里、ft表示英尺,一般我们使用km和m就可以了。

127.0.0.1:6379> geodist position tian_an_men xiang_shan_gong_yuan km
"20.2293"
127.0.0.1:6379> 

这里Redis告诉我们天安门和香山公园之间的距离为20.2293千米,有兴趣的话可以自己用地图测试一下看看准不准,当然最好使用经纬度进行定位。

注意:此命令统计的距离为两个位置的直线距离。

查询某位置内的其他成员信息

重点来了,我们还可以查询某个地点附近的成员。

命令:georadius  key  经度  纬度  半径  单位(km、m、mi、ft)  返回内容(withcoord、withdist、withhash)  count  数量(返回的成员个数,默认是全部返回)  顺序(asc、desc)

这个命令有点长,但是很好理解,唯一不清晰的就是返回内容。

  • withcoord:返回满足条件位置的经纬度信息。
127.0.0.1:6379> georadius position 116.405419 39.913164 5 km withcoord
1) 1) "tian_an_men"
   2) 1) "116.40426903963088989"
      2) "39.91316289865137179"
2) 1) "yue_tan_gong_yuan"
   2) 1) "116.36000186204910278"
      2) "39.92246025586381819"
127.0.0.1:6379> # 表示距离(116.405419 39.913164)不超过5km的地方,当然返回的内容我们要使用geoadd添加进去
  • withdist:说明:返回满足条件的位置与查询位置的直线距离。
127.0.0.1:6379> georadius position 116.405419 39.913164 5 km withdist
1) 1) "tian_an_men"
   2) "0.0981"
2) 1) "yue_tan_gong_yuan"
   2) "4.0100"
127.0.0.1:6379> 
  • withhash:说明:返回满足条件位置的哈希信息。
127.0.0.1:6379> georadius position 116.405419 39.913164 5 km withhash
1) 1) "tian_an_men"
   2) (integer) 4069885552230465
2) 1) "yue_tan_gong_yuan"
   2) (integer) 4069879797297521
127.0.0.1:6379> 

然后我们再看看数量,显然这是限制返回数量的。

127.0.0.1:6379> georadius position 116.405419 39.913164 50 km withdist
1) 1) "yue_tan_gong_yuan"
   2) "4.0100"
2) 1) "xiang_shan_gong_yuan"
   2) "20.3165"
3) 1) "tian_an_men"
   2) "0.0981"
4) 1) "bei_jing_huan_le_gu"
   2) "9.1163"
127.0.0.1:6379> georadius position 116.405419 39.913164 50 km withdist count 1
1) 1) "tian_an_men"
   2) "0.0981"
127.0.0.1:6379> # 设置50km,显然全部返回了。如果指定了count 1,那么只会返回一个

然后是排序。

127.0.0.1:6379> georadius position 116.405419 39.913164 50 km withdist asc
1) 1) "tian_an_men"
   2) "0.0981"
2) 1) "yue_tan_gong_yuan"
   2) "4.0100"
3) 1) "bei_jing_huan_le_gu"
   2) "9.1163"
4) 1) "xiang_shan_gong_yuan"
   2) "20.3165"
127.0.0.1:6379> georadius position 116.405419 39.913164 50 km withdist desc
1) 1) "xiang_shan_gong_yuan"
   2) "20.3165"
2) 1) "bei_jing_huan_le_gu"
   2) "9.1163"
3) 1) "yue_tan_gong_yuan"
   2) "4.0100"
4) 1) "tian_an_men"
   2) "0.0981"
127.0.0.1:6379> # asc从近到远,desc从远到近

除此之外,Redis还可以让我们查询哈希值。

127.0.0.1:6379> geohash position yue_tan_gong_yuan
1) "wx4epgdv0t0"
127.0.0.1:6379> # 可以同时查询多个

删除地理位置

zrem key 地点1 地点2······

127.0.0.1:6379> geohash position yue_tan_gong_yuan
1) "wx4epgdv0t0"
127.0.0.1:6379> zrem position yue_tan_gong_yuan
(integer) 1
127.0.0.1:6379> geohash position yue_tan_gong_yuan
1) (nil)
127.0.0.1:6379> 

使用Python操作Redis的GEO

老规矩,让我们看看如何使用Python操作Redis中的GEO类型的数据。

import redis

client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")

# 1. 添加
client.geoadd("好地方",
              116.404269, 39.913164, "天安门",
              116.36, 39.922461, "月坛公园",
              116.499705, 39.874635, "北京欢乐谷",
              116.193275, 39.996348, "香山公园")

# 2. 查询经纬度
print(client.geopos("好地方", "天安门", "香山公园"))
"""
[(116.40426903963089, 39.91316289865137), (116.19327574968338, 39.99634737765856)]
"""

# 3. 距离统计
print(client.geodist("好地方", "天安门", "香山公园", "km"))  # 20.2293

# 4. 查询某位置内的其他成员信息
print(client.georadius("好地方", 116.405419, 39.913164, 5, "km", withcoord=True))
"""
[['天安门', (116.40426903963089, 39.91316289865137)], ['月坛公园', (116.3600018620491, 39.92246025586382)]]
"""
print(client.georadius("好地方", 116.405419, 39.913164, 5, "km", withdist=True))
"""
[['天安门', 0.0981], ['月坛公园', 4.01]]
"""
print(client.georadius("好地方", 116.405419, 39.913164, 5, "km", withhash=True))
"""
[['天安门', 4069885552230465], ['月坛公园', 4069879797297521]]
"""

# 5. 限制返回数量
print(client.georadius("好地方", 116.405419, 39.913164, 50, "km", withdist=True))
"""
[['月坛公园', 4.01], ['香山公园', 20.3165], ['天安门', 0.0981], ['北京欢乐谷', 9.1163]]
"""
print(client.georadius("好地方", 116.405419, 39.913164, 50, "km", withdist=True, count=1))
"""
[['天安门', 0.0981]]
"""

# 6. 排序
print(client.georadius("好地方", 116.405419, 39.913164, 50, "km", withdist=True, sort="ASC"))
"""
[['天安门', 0.0981], ['月坛公园', 4.01], ['北京欢乐谷', 9.1163], ['香山公园', 20.3165]]
"""
print(client.georadius("好地方", 116.405419, 39.913164, 50, "km", withdist=True, sort="DESC"))
"""
[['香山公园', 20.3165], ['北京欢乐谷', 9.1163], ['月坛公园', 4.01], ['天安门', 0.0981]]
"""

# 7. 查询哈希值
print(client.geohash("好地方", "月坛公园"))  # ['wx4epgdv0t0']

# 8. 删除地理位置
print(client.geohash("好地方", "天安门"))  # ['wx4g0cgp000']
client.zrem("好地方", "天安门")
print(client.geohash("好地方", "天安门"))  # [None]

总结

GEO 是 Redis 3.2 版本中新增的功能,只有升级到 3.2+ 才能使用,GEO 本质上是基于 ZSet 实现的,这点在 Redis 源码找到相关信息,我们可以 GEO 使用实现查找附近的人或者附近的地点,还可以用它来计算两个位置相隔的直线距离。

个人觉得通过两个经纬度可以直接计算出距离还是很方便的,至于使用场景也很简单,无非就是查询附近的人、商家,计算相关距离信息等等。

posted @ 2020-07-15 23:58  古明地盆  阅读(1253)  评论(0编辑  收藏  举报