写在前面
在LBS(location based service)应用,如滴滴打车应用,需要根据用户的位置信息来获取某些数据,如获取距离当前用户指定距离范围内的所有车辆信息,该类的应用就可以使用本文我们要学习的GEO了,接下来一起看下。
1:实战
1.1:geoadd
添加位置信息,格式GEOADD key longitude latitude member [longitude latitude member ...]
,可以指定多组经度+纬度+位置名称
,如下:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
1.2:geopos
获取指定位置的经纬度,如果是不存在的位置则返回nil
,格式GEOPOS key member [member ...]
,如下测试:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOPOS Sicily Palermo Catania NonExisting
1) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "15.08726745843887329"
2) "37.50266842333162032"
3) (nil)
redis>
1.3:geodist
geodist 用于返回两个给定位置之间的距离。geodist 语法格式为GEODIST key member1 member2 [m|km|ft|mi]
,参数说明如下:
m:米,默认单位。
km:千米。
mi:英里。
ft:英尺。
如果是不存在的地点则返回nil
,如下测试:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEODIST Sicily Palermo Catania
"166274.1516"
redis> GEODIST Sicily Palermo Catania km
"166.2742"
redis> GEODIST Sicily Palermo Catania mi
"103.3182"
redis> GEODIST Sicily Foo Bar
(nil)
1.4:georadius
以给定的经纬度为中心,并指定距离范围,返回该距离范围内的所有地点信息,格式如下:
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明如下:
m:米,默认单位。
km:千米。
mi:英里。
ft:英尺。
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
WITHCOORD: 将位置元素的经度和纬度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
COUNT 限定返回的记录数。
ASC: 查找结果根据距离从近到远排序。
DESC: 查找结果根据从远到近排序。
测试如下:
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.36138933897018433"
2) "38.11555639549629859"
2) 1) "Catania"
2) 1) "15.08726745843887329"
2) "37.50266842333162032"
redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
2) "190.4424"
3) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "Catania"
2) "56.4413"
3) 1) "15.08726745843887329"
2) "37.50266842333162032"
1.5:geohash
geohash算法是一种将经纬度转换为一个整数值的算法,转换后数值接近的则说明地理位置上更接近,则当我们要按照距离来获取地点时就可以使用了。当使用redis的geohash命令获取经纬度对应的geohash值后,就可以将其作为score存储到底层的zset中了,语法格式为GEOHASH key member [member ...]
,测试如下:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"
2:原理
我们知道,经度的范围是[-180,180]
,纬度的范围是[-90,90]
,我们可以将其想想为如下的一个矩形,矩形中的每个点都有一个经纬度的信息,如下图:
但是这个图里每一个点目前都是用经度和纬度来表示的,如果是我们能够将矩形划分成很多个区域,然后每个区域使用一个整数来表示,则就很容易方便查找周边经纬度了,而redis就使用GeoHash编码方法实现了这个需求,基本原理是二分区间,区间编码
,二分区间的意思是将经度或纬度的范围平分为两份,区间编码就是对每一份给一个编码,具体的,redis会分别对经度和纬度执行GeoHash,然后将二者的编码合并为最终的编码。
GeoHash会将经度和纬度划分为N位的二进制编码,其中N就是需要对区间划分的次数,这里我们假设N为5来编码,假设要编码的经纬度是(116.37,39.86)
看下这个过程:
首先对经度编码,如果落在左区间则编码为0,如果落在右区间则编码为1:
二分[-180,180]为[-180,0),[0,180]左右两个区间,116.37落在右区间,所以编码为1
二分[0,180]为[0,90),[90,180]左右两个区间,116.37落在右区间,所以编码为1
二分[90,180]为[90,135),[135,180]左右两个区间,116.37落在左区间,所以编码为0
二分[90,135]为[90,112.5),[112.5,135]左右两个区间,116.37落在右区间,所以编码为1
二分[112.5,135]为[112.5,123.75),[123.75,135]左右两个区间,116.37落在左区间,所以编码为0
所以最终的编码是11010
,该过程也可以用下图表示:
纬度同该编码过程,最终结果是10111
,该过程可以用下图表:
最终二者组合的规则是:从左到右偶数位依次取经度的编码,奇数位依次取纬度的编码,因此我们先来取经度的编码,为1?1?0?1?0?
,接着我们将其中的?
,即奇数位使用纬度的编码替换,结果就是1110011101
,整个过程如下图:
这样,一个经纬度就会被转换为一个整数,而相近的整数在实际的地理位置上也是更接近的,这里我们生成编码的N如果是越大,则矩形的区域也会被划分的越细,距离的匹配也会更加精确。
其实GEO底层使用的数据结构正是zset,而DeoHash的结果是作为其score的,这样当我们查找某地附近的地点时,就可以直接上前向后直接查找了。
3:打车场景
假定用户打车场景中根据用户所在定位查找车辆,先模拟库中的车辆信息:
127.0.0.1:6379> GEOADD car:location 13.361389 38.115556 "张师傅" 15.087269 37.502669 "李师傅" 17.087269 35.502669 "刘师傅" 117.087269 65.502669 "孙小姐"
现在顾客芒果小朋友
要打车,假设经纬度是13.361399 38.115565
,则查找其5公里范围内的车辆,如下:
127.0.0.1:6379> GEORADIUS car:location 13.361399 38.115565 5 km
张师傅
可以看到只有张师傅
符合要求,那么就可以给张师傅派单了。