[业务] 关于 "搜索附近几公里的订单/商户" 等业务场景

业务背景

      在我们平常开发工作中,会遇到如下场景:
(1)配送系统需要将一笔订单匹配到附近3公里的骑士
(2)用户需要查看到附近5公里的商户信息

解决思路

方案一:由于我们数据是保存在关系型数据库中的(mysql),因此我们最直观的解决方法便是通过 “纬度与经度每增加0.01度,线性距离便增加1000m” 计算出一批符合规则的经纬度,然后再进行筛选。

double km = 3.0;
double precision = 0.01 * km;

SELECT * FROM `order`
WHERE latitude > latitude - precision && 
      latitude < latitude + precision &&
      longitude > longitude - precision && 
      longitude < longitude + precision;
复制代码

上述的解决方案,在请求量较大的情况下,性能是很不理想的。


方案二:前期根据经纬度,先计算出其对应的geohash值(12位/52位的geohash字符串),然后通过数据库的索引查询出对应的数据

上描解决方法的缺点,如果业务方需要对筛选出来的数据再进行按距离排序,这时候就需要我们二次处理了。


方案三:使用搜索引擎自带的经纬度查询


方案四:基于MongoDB的查询(MonggoDB原生也支持地理位置索引,支持位置查询及排序)

db.runCommand({ 
    geoNear: "places", 
    near: [30.545162, 104.062018], num:1000 
})
复制代码

业界比较通用的解决地理位置排序算法是GeoHash算法

什么是geohash

      GeoHash是一种对地理坐标进行编码的方法,它将二维坐标映射为一个字符串。每个字符串代表一个特定的矩形,在该矩形范围内的所有坐标都共用这个字符串。字符串越长精度越高,对应的矩形范围越小。

      对一个地理坐标编码时,按照初始区间范围纬度[-90,90]和经度[-180,180],计算目标经度和纬度分别落在左区间还是右区间。落在左区间则取0,右区间则取1。然后,对上一步得到的区间继续按照此方法对半查找,得到下一位二进制编码。当编码长度达到业务的进度需求后,根据“偶数位放经度,奇数位放纬度”的规则,将得到的二进制编码穿插组合,得到一个新的二进制串。最后,根据base32的对照表,将二进制串翻译成字符串,即得到地理坐标对应的目标GeoHash字符串。

以坐标“30.280245, 120.027162”为例,计算其GeoHash字符串。首先对纬度做二进制编码:

(1)将[-90,90]平分为2部分,“30.280245”落在右区间(0,90],则第一位取1
(2)将(0,90]平分为2分,“30.280245”落在左区间(0,45],则第二位取0
... 不断重复以上步骤,得到的目标区间会越来越小,最终得到:得到的纬度二进制编码为10101 01100 01000

      按照“偶数位放经度,奇数位放纬度”的规则,将经纬度的二进制编码穿插,得到完成的二进制编码为:11100 11001 10011 10010 00111 00010。由于后续要使用的是base32编码,每5个二进制数对应一个32进制数,所以这里将每5个二进制位转换成十进制位,得到28,25,19,18,7,2。 对照base32编码表,得到对应的编码为:wtmk72。(代码实现相对简单,核心思想是在二分法)

geohash的长度对应的精度:

Redis中对geohash的运用

(1)geoadd 增加经纬度

GEOADD key longitude latitude member [longitude latitude member ...]

时间复杂度:O(log (N) )

数据存储在redis soret set 集合中,需要留意的是,redis没有提供相应的删除方法,需要使用ZREM来进行删除 (zrem key member)
复制代码

(2)geopos 获取元素位置

GEOPOS key member [member ...]

时间复杂度:O(log (N) )

这边需要注意的一个细节是。由于我们是使用geoadd添加经纬度的,是将经纬度转换成52bit的geohash值,因此从geohash转换的时候,会有一定的经度损失
复制代码

(3) geodist 返回两个经纬度之间的距离

GEODIST key member1 member2 [unit]

时间复杂度:O(log (N) )
复制代码

(4) geohash 获取经纬度的hash值

GEODIST key member1 member2 [unit]

时间复杂度:O(log (N) )

需要注意的是:
[1] geohash返回11个字符串的GeoHash字符串,可以缩短删除右侧的字符,它会失去进度,但是还是可以指向同一个区域

[2] 获取到的结果,可以使用 http://geohash.org/geohash值进行查询

[3] 带有相似前缀的字符串在附近,但是相反的情况并非如此,有可能前缀不同的字符串也在附近
复制代码

(5) georadius & georadiusbymember

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

时间复杂度: O(N+log(M))
其中N是由中心和半径界定的圆形区域的边界框内的元素的数量,M 是索引内的项目的数量

复制代码
注意事项

      我使用redis geohash在搜索附近商户门店的功能上,当前全国的门店信息在30w左右。整体来说,对内存的消耗不会太多。我在 #老錢资深洞主 @ 公众号【码洞】# 的公众号上看过他的建议,下面直接引用了:

      如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。

所以建议Geo的数据使用单独的Redis实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对Geo数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。 (这也是我们筛新列表的做法,有一种业务是骑士要看到附近3公里的订单,我们的做法就是按照一个区域+时间段进行数据的存储

转载于:https://juejin.im/post/5c98a23d6fb9a070cf6bdab8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值