前段时间自己在做附近直播相关业务,其中有一个核心的点就是检索用户附近的主播,也是主要召回池。针对业务场景的特殊性,最后决定使用Redis的GEO技术来完成这个功能。主要考虑点在于每天在线直播的主播数量是固定的差不多一万这个量级,使用配置好一点的单机Redis单key存储是没问题的。数据操作主要有两个:一是主播开播的时候写入主播Id的经纬度,二是主播关播的时候删除主播Id元素。这样就维护了一个具有位置信息的在线主播集合提供给线上检索。下面详细介绍一下。
Redis GEO 命令
【GEOADD key longitude latitude member】添加一个空间元素,这里具体业务就是 主播的经纬度和主播的Id。需要注意的是Redis的纬度有效范围不是[-90,90]而是[-85,85]
【GEORADIUS key longitude latitude radius】以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。具体业务可以根据当前用户的用户的经纬度,搜索附近100km的主播。
再有就是删除操作,Redis GEO结构的数据类型没有提供删除操作。不过我们可以直接使用【ZREM key member】这里的member就是主播Id。可以这样操作的原因是Redis GEO 数据类型内部的存储是ZSet结构。
以上就是业务实现的主要操作。Redis更加详细的命令操作参考(http://redisdoc.com/geo/index.html)
下面我们具体聊聊其中的原理。
Redis GEO 原理
讲Redis GEO实现之前需要先明白一些关于空间索引的算法GEOHASH的知识。针对索引我们日常所见都是一维的字符,那么如何对三维空间里面的坐标点建立索引呢,直接点就是三维变二维,二维变一维。
地球纬度区间是[-90,90],经度区间是[-180,180]。 将它展开想象成一个矩形。
通过上面的方法将地球的表面转换成二维空间的平面,那接下来就是如何将二维换行成一维了。我们先将平面切割成四个正方形,然后用简单的 01 编码来标识这个四个正方形,最后按照编码的大小将四个正方形连接起来,这样整个平面就转换成了一条Z曲线,变成了一维。我们递归对每个正方形做同样的操作,递归的层次越深,整个平面就逐渐被Z曲线填充。我们的点也会落在每个小正方形里面,小正方形越小,精度就越高。如下图所示:
转成一维以后接下来就如何建立索引了。当我们拿到一个经纬度之后按照如下方式进行编码。
从上面的图可以发现二分的次数越多就越接近经纬度的实际值,和前面提到的不断递归正方形是一个意思。按照上面的方式我们选定一个二分的深度(也就是精度)分别对经纬度进行编码。然后按照以奇数为纬度,偶数为经度组合成一个二进制序列,再将获取到的经纬度组合二进制序列以每5个数为一组,将每一组都进行转换成十进制数字,最后采用Base32对应编码规则进行转换可得到编码,也就是最后的索引。
通过上面几个步骤介绍了一下GeoHash具体的流程、有了上面这个知识点,理解Redis GEO原理就很简单了,Redis使用ZSet的方式存储Geo类型的数据,有序集合里面的member是具体的业务对象,score就是该业务对象的经纬度进行GeoHash编码之后将二级制序列转成52位整数值数据。当我们想要获取某个经纬度附近的元素时候,先根据当前经纬度计算出对应的GeoHash块(52位整数值),在根据半径计算出当前hash块周围的8个hash块,然后在根据score值获取这8个hash块范围内的元素返回。
GEO HASH 延伸
对于一个经纬度,如果我们编码的时候选择对经度二分3次(3位二进制),对维度二分2次(2位二进制),最后组合成一个5位的二级进序列,经过Base32编码得到一个字符。那么这个字符的一共有2^5=32个,这样就将地图划分为32个块。如下图所示
GeoHash将每一个区域画成一块块矩形块,每个矩形块使用一个字符串表示,当我们需要查询附近的点时,通过自己的坐标计算出一个字符串,根据这个字符串定位到我们所在的矩形块,然后返回这个矩形块中的点。然后根据编码的深度来确定精度,或者根据Base32编码之后字符的长度来确定块的所表示的区域大小。
length | width | height |
---|---|---|
1 | 5000km | 5000km |
2 | 1250km | 625km |
3 | 156km | 156km |
4 | 39.1km | 19.5km |
5 | 4.89km | 4.89km |
6 | 1.22km | 0.61km |
7 | 153m | 153m |
8 | 38.2m | 19.1m |
9 | 4.77m | 4.77m |
10 | 1.19m | 0.596m |
11 | 149mm | 149mm |
12 | 37.2mm | 18.6mm |
对于这样的编码方式有一定的局限性:在拥有局部保序性的同时,具有突变性。导致一些邻近点真实并不是距离较近的点。
参考
http://geohash.gofreerange.com/
https://halfrost.com/go_spatial_search/
https://www.cnblogs.com/LBSer/p/3310455.html