今天我来给大家介绍一下Redis的GEO数据类型的实现原理以及使用方法。
大家都一定用过美团APP,经常会使用“附近的餐馆”,也一定在滴滴上打过车,这些都是基于位置信息服务(LBS)的应用。LBS应用访问的数据是和人或者物关联的一组经纬度信息。GEO数据类型就非常适合应用在LBS服务的场景中,下面我们来看一下它的底层结构。
GEO的底层结构:
一般来说,在设计一个数据类型的底层结构时,我们首先要知道,要处理的数据有什么访问特点。我以叫车服务为例,来分析下LBS应用中经纬度的存取特点。
每一辆网约车都有一个编号(例如10),网约车需要实时的将自己的经纬度信息(,35)发给叫车应用。
用户在叫车的时候,叫车应用会根据用户的经纬度信息(例如经度,纬度38)查找用户的附近车辆,并进行匹配。
等把位置相近的用户和车辆匹配上以后,叫车应用就会根据车辆的编号,获取车辆的信息,并返回给用户。
可以看到,一辆车(或者一个用户)对应着一组经纬度信息,并且随着车(或者用户)的移动,相应的经纬度也会发生变化。这种数据的特点是一个key(例如车的编号)对应着一个value(经纬度信息)。当有很多车辆的信息需要保存的时候,就需要有一个集合来保存一系列的key和value。在Redis中Hash集合类型可以快速访问一系列的key和value,正好可以用来记录一系列的车辆ID和对应的经纬度信息。所以,我们把不同车辆的ID和其对应的经纬度值存在Hash集合中。如下图所示:
Hash集合类型,可以通过HSet命令,将对相应的key设置成相应的value值。所以我们可以使用该命令快速的更新车辆变化的经纬度信息。这样看来,Hash类型看起来是个不错的选择,但是有这么一个问题,对于一个LBS应用来说,除了记录经纬度信息,还需要根据用户的经纬度信息在车辆的Hash集合中进行范围查询。一旦涉及到范围查询,我们就要求集合里的数据是有序的。但是Hash元素是无序的,所以是不能满足我们的需求的。接下来我们看一下SortedSet是否可以满足我们的需求呢?SortedSet类型也支持key-value形式,其中key是SortedSet中的元素,value则是元素的权重分数。SortedSet可以根据权重来排序,支持范围查询。所以可以满足LBS服务中查找相邻位置的需求了。你有没有发现这么一个问题,由于SortedSet中权重是一个浮点数,没法去存储一组经纬度的值,那该如何处理呢?这时就要用的GEO类型中的GeoHash编码了。
GeoHash的编码方式
GeoHash编码的基本原理就是“二分区间,区间编码”。当我们要对一组经纬度进行GeoHash编码时,我们要先对经度和纬度分别进行编码,然后再把经度和纬度各自的编码组合成一个最终的编码。下面我们来看一下经度和纬度的单独编码过程。
对于一个地理位置信息来说,它的经度范围是[-,]。GeoHash编码会把一个经度的值编码成N位的二进制数(把经度范围进行N次二分区操作)。我们进行第一次二分区时,经度范围[-,]会被分成2个子区间[-,0)和[0,]。然后看经度值落在哪个范围内,如果落在[-,0),我们用0表示,如果落在(0,],我们用1来表示。这样,每做一次二分区,就得到1位编码值。当做N次二分区,就可以得到N位编码值。
举个例子,假如我们要编码的经度值是,我们用5位编码值来编码。
我们先做第一次二分区操作,把经度[-,]分成左分区[-,0)和右分区[0,],此时处在右分区,所以,我们用1来表示第一次分区后的编码值。接下来,我们做第二次分区,把所在的分区[0,]区间,分成[0,90)和[90,],此时经度值落在[90,]区间,所以第二次分区后的编码值仍然为1。接下来进行第三次分区,我们对[90,]进行二分区,值落在[90,)之间,所以第三次分区后的编码值为0。然后进行第四次分区,我们对[90,)进行二分区,值落在[90,.5)之间,所以第四次分区后的编码为0。最后进行第五次分区,我们对[90,.5)进行二分区,值落在[.25,.5]之间,所以第五次分区后的编码为1。
接下来,我们也用5位编码值来对纬度35进行编码。
当一组经纬度的值都编码完成后,我们再把他们组合在一起。组合的
规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值。其中,偶数位从0开始,奇数位从1开始。
用了GeoHash编码后,原来无法用一个权重分数表示一组经纬度(,35)就可以用0这一个值来表示,就可以保存为SortedSet的权重分数。使用GeoHash编码后,我们相当于把整个地理空间划分成了一个个小方格,每个方格对应了GeoHash中的一个分区。
举个例子,我们把经度区间[-,]做一次二分区,把维度区间[-90,90]做一次二分区,就会得到4个分区。我们来看下它们的经度和纬度范围以及对应的GeoHash组合编码。
分区一:[-,0)和[-90,0),编码00。
分区二:[-,0)和[0,90],编码01。
分区三:[0,]和[-90,0),编码10。
分区四:[0,]和[0,90],编码11。
这4个分区对应了4个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的GeoHash编码值基本也是接近的,如下图所示:
所以,我们使用SortedSet范围查询得到的相近的编码值,在地理空间上,也是相邻的方格。所以可以实现基于LBS应用打车的功能了。
好了,到目前位置,我们就知道了,GEO类型是把经纬度所在的区间编码作为SortedSet中元素的权重分数,把和经纬度相关的车辆ID作为SortedSet中元素本身的值保存下来。这样相邻经纬度的查询就可以通过编码值的大小范围查询来实现了。接下来我们来看一下如何操作GEO类型。
GEO类型操作命令
GEO类型经常会用到两个命令,分别是GEOADD和GEORADIUS。
GEOADD命令:用于把一组经纬度信息和相对应的一个ID记录到GEO类型集合中。
GEORADIUS命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素。当然,我们可以自己定义这个范围。
GEOADDlocations3510
代表把ID为10号的车辆的当前经纬度(,35)存入key为locations的GEO集合中。
GEORADIUSlocations355kmASCCOUNT10
Redis会根据输入的用户的经纬度信息(,35),查找以这个经纬度为中心的5公里内的车辆信息。使用ASC选项,让返回的车辆信息按照距离这个中心位置从近到远的方式来排序,以方便选择最近的车辆;使用COUNT选项,指定返回的车辆信息的数量。
到目前为止,我们的GEO数据类型已经学完了。更多的硬核知识,请