当前位置: 首页 > 科技观察

Redis实战篇:GEO帮我认识附近的女神

时间:2023-03-18 12:06:50 科技观察

很多人说“干活还不如做PPT的”。其实老板也不傻。为什么他们更认可做PPT的人?因为他们站在老板的角度思考问题。对他来说,他需要的是一个“解决方案”。多站在创作者的角度思考问题,而不是局限于程序员的角度;多想想这个东西给人们提供了什么价值,而不是“我怎么实现它”。当然,如何实现是必须的,但通常不是最重要的。什么是面向LBS的应用经纬度就是将经度和纬度组合起来形成一个坐标系。也称为地理坐标系,是用三维球面来定义地球上空间的球坐标系。可标记地球上任意位置(小数点后7位,精度可达1厘米)。经度范围为(-180,180],纬度范围为(-90,90)。东为正,西为负。附近的人常称LBS(LocationBasedServices,基于位置的服务),基于用户当前的地理位置数据,为用户提供精准的偶遇服务,附近人的核心思想如下:以“我”为中心,搜索附近的Ta;基于“我”的当前地理位置”,计算别人和“我”的距离;按照“我”和别人的距离排序,过滤掉我最近的用户。MySQL实现了“附近的人”的计算,通过一个坐标计算出这个坐标附近的其他数据,并按距离排序如何开始呢?以用户为中心,画一个半径为1000米的圆,那么这个圆矩形区域内的用户就是我们要认识的“附近的人”。将经纬度存储到MySQL:CREATETABLE`nearby_user`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(255)DEFAULTNULLCOMMENT'name',`longitude`doubleDEFAULTNULLCOMMENT'longitude',`latitude`doubleDEFAULTNULLCOMMENT'latitude',`create_time`datetimeDEFAULTNULLONUPDATECURRENT_TIMESTAMPCOMMENT'创建时间',PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;但是不可能遍历所有“女神”的经纬度数据,根据距离计算出自己的经纬度数据,计算量太大了。我们可以通过区域筛选出有限的“女神”坐标数据,然后对矩形区域内的数据进行全距离计算和排序,这样的计算量是显而易见的。矩形区域如何划分?在圆形外衣上套上正方形,根据用户经纬??度(经度、纬度+距离)的最大值和最小值作为过滤条件过滤数据,轻松搜索到“女神”信息在广场。额外的区域呢?额外区域中的用户与圆点之间的距离必须大于圆的半径。然后我们计算用户的中心点到正方形内所有用户的距离,过滤掉所有距离小于等于半径的用户,圆形区域内的所有用户都是附近符合要求的人。为了满足高性能的矩形区域算法,数据表需要在经纬度坐标上添加一个复合索引(经度、纬度),这样可以最大化查询性能。实战中使用第三方类库根据经纬度和距离获取外接矩形的最大最小经纬度,根据经纬度计算j距离:com.spatial4jspatial4j0.5得到外接矩形后,利用矩形的最大最小经纬度值进行搜索正方形区域内的用户,然后剔除超出指定距离的用户,即最终附近的人。/***获取附近人x米**@paramdistance搜索距离范围单位km*@paramuserLng当前用户经度*@paramuserLat当前用户纬度*/publicStringnearBySearch(doubledistance,doubleuserLng,doubleuserLat){//1.获取外部SquareRectanglerectangle=getRectangle(distance,userLng,userLat);//2.获取位置在正方形内的所有用户Listusers=userMapper.selectUser(rectangle.getMinX(),rectangle.getMaxX(),rectangle.getMinY(),rectangle.getMaxY());//3.剔除半径超过指定距离的多余用户());returnJSON.toJSONString(users);}//获取外接矩形privateRectanglegetRectangle(doubledistance,doubleuserLng,doubleuserLat){returnspatialContext.getDistCalc().calcBoxByDistFromPt(spatialContext.makePoint(userLng,userLat),distance*DistanceUtils.KM_TO_DEG,spatialContext,null);}/****在球体中,两点之间的距离*@paramlongitudelongitude1*@paramlatitudelatitude1*@paramuserLnglongitude2*@paramuserLatlatitude2*@return返回以公里为单位的距离*/privatedoublegetDistance(双经度,双纬度,doubleuserLng,doubleuserLat){returnsspatialContext.calcDistance(spatialContext.makePoint(userLng,userLat),spatialContext.makePoint(longitude,latitude))*DistanceUtils.DEG_TO_KM;}由于业务代码中实现了用户之间距离的排序,可以看到SQL语句也很简单SELECT*FROMnearby_userWHERE1=1AND(longitudeBETWEEN#{minlng}AND#{maxlng})AND(latitudeBETWEEN#{minlat}AND#{maxlat}),但是数据库查询性能毕竟有限,如果“附近的人”查询请求太多了,在高并发的情况下这可能不是一个很好的解决方案。尝试Redis哈希失败。下面我们一起来分析一下LBS数据的特点:每个“女神”都有一个ID号,每个ID对应着经纬度信息。当“宅男”登录APP获取“性感女郎”时,APP会根据“宅男”的经纬度搜索附近的“女神”。获取到位置匹配的“女神”ID列表后,从数据库中获取该ID对应的“女神”信息返回给用户。数据的特点是一个女神(用户)对应一组经纬度,这让我想起了Redis的Hash结构。即一个key(女神ID)对应一个value(经纬度)。Hash看似可以实现,但是LBS应用除了记录经纬度外,还需要对Hash集合中的数据进行范围查询,根据经纬度换算成距离进行排序。Hash集合中的数据是无序的,这显然是不可取的。SortedSet初识SortedSet类型合适吗?因为它可以排序。SortedSet类型也是一个key对应一个value,key元素的内容,value`是元素的权重分值。SortedSet可以根据元素的权重得分对元素进行排序,这似乎可以满足我们的需求。比如SortedSet的元素是“女神ID”,元素对应的权重分就是经纬度信息。![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/redis/SortedSet实例LBS.png)问题是SortedSet元素的权重值为浮点数,纬度和longitude既??是经度又是纬度值,怎么办?经纬度可以转换成浮点数吗?思路是对的,Redis为了实现经纬度的比较,采用了业界广泛使用的GeoHash编码,分别对经纬度进行编码,最后将各自的经纬度组合编码成一个最终编码。这样就把经纬度转化成了一个值,Redis的GEO类型的底层数据结构是通过SortedSet来实现的。我们来看看GeoHash是如何对经纬度进行编码的。关于GeoHash的GEOHash编码可以参考:https://en.wikipedia.org/wiki/GeohashGeoHash算法将二维经纬度数据映射为一维整数,这样所有的元素都会被挂在一条线上,接近于二维坐标映射到一维后点之间的距离会很近。当我们要计算“附近的人”时,首先将目标位置映射到这条直线上,然后在这条一维直线上得到附近的点。GeoHash编码会将一个经度值编码成一个N位的二进制值。让我们对经度范围[-180,180]进行N次二元划分操作,其中N可以自定义。在进行第一次二次划分时,会将经度范围[-180,180]划分为两个子区间:[-180,0)和[0,180](我称之为左右划分)。此时,我们可以检查待编码的经度值是落在左分区还是右分区。如果落在左分区,我们用0表示;如果它落在正确的分区中,我们用1来表示它。这样,每次完成第二个分区时,我们都可以获得一个1位的编码值(0或1)。然后对经度值所属的分区做二次分区,同时在二次分区后再次检查经度值是落在左分区还是右分区,然后根据做一个1位码刚才的规则。经过N次二等分后,经度值可以用N位数来表示。所有地图元素坐标都将放置在唯一的正方形中。网格越小,坐标越精确。然后对这些方块进行整数编码,方块越近编码越接近。编码后,每个地图元素的坐标都会变成一个整数,通过它可以还原出元素的坐标。整数越长,恢复坐标值的损失越小。对于“附近的人”功能来说,损失一点点的精度可以忽略不计。例如对等于169.99的经度值进行4位编码(N=4,做4个分区),将经度区间[-180,180]分为左分区[-180,0]和右分区[0,180]。169.99属于右分区,用1表示第一个分区码;然后继续将第一次划分后169.99所属的[0,180]区间划分为[0,90)和[90,180],169.99仍在正确的区间内,Code'1'。将[90,180]分为[90,135)和[135,180],这次落在左边的分区上,编码为'0'。这样,我们最终得到了一个4位的代码。纬度的编码思路和经度一样,这里不再赘述。结合经纬度代码如果计算出的经纬度代码分别为11011和00101`,则目标代码的第0位将从经度的第0位开始为值1作为目标值,而目标代码的第1位targetcode将从纬度0的第0位开始为目标值,依此类推:这样经纬度(35.679,114.020)可以用1010011011表示,这个值可以作为权重值SortedSet实现排序。RedisGEO通过使用GeoHash编码的经纬度组合值作为SortedSet元素的得分权重来实现GEO类型。RedisGEO有哪些指令?我们需要在SortedSet中存储登录app的女孩ID和对应的经纬度。更多GEO类型命令请参考:https://redis.io/commands#geoGEOADDRedis提供了GEOADDkeylongitudelatitudemember命令,将一组经纬度信息和对应的“女神ID”记录到GEO类型中采集,如下:一次性记录多个用户(苍井空、波多野结衣)的经纬度信息。GEOADDgirl:localtion13.36138938.115556"Aoi"15.08726937.502669"HatanoYui"GEORADIUS我登录了应用程序并获取了我自己的经纬度信息。如何找到以这个经纬度为中心的一定范围内的其他用户?RedisGEO类型提供了GEORADIUS命令。假设你的经纬度是(15.08726937.502669),你需要获取附近10公里内的“女神”返回给LBS应用:GEORADIUSgirlo:locations15.08726937.502669kmASCCOUNT10ASC可以根据经纬度对“女神”信息进行排序这个距离由近到远。COUNT选项表示返回的“女神”数量,防止附近“女神”过多,节省带宽资源。如果你觉得自己需要更多的女神,没有限制,但你需要注意自己的身体,多吃鸡蛋来弥补。用户下线后,删除下线“女神”的经纬度怎么办?这是一个很好的问题。GEO类型是基于SortedSet实现的,所以可以使用ZREM命令删除地理位置信息。比如删除“苍井空”的位置信息:ZREMgirl:localtion"苍井空"总结GEO本身并没有设计新的底层数据结构,而是直接使用了SortedSet集合类型。GEO类型使用GeoHash编码方式实现经纬度到SortedSet中元素权重得分的转换。两个关键机制是将二维地图划分为区间并对区间进行编码。一组经纬度落在某个区间后,用区间的编码值表示,编码值作为SortedSet元素的权重分值。在一个地图应用中,可能有数以千万计的汽车数据、餐厅数据、人物数据。如果使用Redis的Geo数据结构,它们都会被放在一个zset集合中。在Redis集群环境中,集合可能会从一个节点迁移到另一个节点。如果单个key的数据量过大,对集群的迁移影响很大。在集群环境下,单个key对应的数据量不宜超过1M,否则会导致集群迁移卡顿,影响线上服务的正常运行。因此,建议使用单独的Redis集群实例部署Geo数据。如果数据量超过1亿甚至更大,就需要对Geo数据进行拆分,按国家、按省、按市,甚至在人口多的城市按区划分。这可以显着减少单个zset集合的大小。本文转载自微信公众号“代码字节”,可通过以下二维码关注。转载本文请联系码哥字节公众号。