Redis 实战篇:Geo 算法查找附近的人

一、什么是面向 LBS 应用

        经纬度是经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置(小数点后7位,精度可以到1厘米)。

经度的范围在 (-180, 180],纬度的范围 在(-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治天文台) 为界,东正西负。

附近的人 也就是常说的 LBS (Location Based Services,基于位置服务),它围绕用户当前地理位置数据而展开的服务,为用户提供精准的邂逅服务。

附近的人核心思想如下:

  1. 以 “我” 为中心,搜索附近的 Ta;

  2. 以 “我” 当前的地理位置为准,计算出别人和 “我” 之间的距离;

  3. 按 “我” 与别人距离的远近排序,筛选出离我最近的用户。

二、MySQL 实现

    计算「附近的人」,通过一个坐标计算这个坐标附近的其他数据,按照距离排序,如何下手呢?

以用户为中心,给定一个 1000 米作为半径画圆,那么圆形区域内的用户就是我们想要邂逅的「附近的人」。

将经纬度存储到 MySQL

CREATE TABLE `nearby_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`longitude` double DEFAULT NULL COMMENT '经度',
`latitude` double DEFAULT NULL COMMENT '纬度',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

可是总不能遍历所有的「女神」经纬度与自己的经纬度数据计算在根据距离排序,这个计算量也太大了。

我们可以通过区域来过滤出有限「女神」坐标数据,再对矩形区域内的数据进行全量距离计算再排序,这样计算量明显降低。

在圆形外套上一个正方形,根据用户经、纬度的最大最小值(经、纬度 + 距离),作为筛选条件过滤数据,就很容易将正方形内的「女神」信息搜索出来。

Redis 实战篇:Geo 算法查找附近的人_第1张图片

 多出来的一些区域咋办?

多出来的这部分区域内的用户,到圆点的距离一定比圆的半径要大,那么我们就计算用户中心点与正方形内所有用户的距离,筛选出所有距离小于等于半径的用户,圆形区域内的所用户即符合要求的附近的人

为了满足高性能的矩形区域算法,数据表需要在经纬度坐标加上复合索引 (longitude, latitude),这样可以最大优化查询性能。

三、实战

根据经纬度和距离获取外接矩形最大、最小经纬度以及根据经纬度计算距离使用了一个第三方类库:


     com.spatial4j
     spatial4j
     0.5

获取到外接矩形后,以矩形的最大最小经、纬度值搜索正方形区域内的用户,再剔除超过指定距离的用户,就是最终的附近的人

/**
 * 获取附近 x 米的人
 *
 * @param distance 搜索距离范围 单位km
 * @param userLng  当前用户的经度
 * @param userLat  当前用户的纬度
 */
public String nearBySearch(double distance, double userLng, double userLat) {
  //1.获取外接正方形
  Rectangle rectangle = getRectangle(distance, userLng, userLat);
  //2.获取位置在正方形内的所有用户
  List users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());
  //3.剔除半径超过指定距离的多余用户
  users = users.stream()
    .filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance)
    .collect(Collectors.toList());
  return JSON.toJSONString(users);
}

// 获取外接矩形
private Rectangle getRectangle(double distance, double userLng, double userLat) {
  return spatialContext.getDistCalc()
    .calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat), 
                         distance * DistanceUtils.KM_TO_DEG, spatialContext, null);
}

     /***
     * 球面中,两点间的距离
     * @param longitude 经度1
     * @param latitude  纬度1
     * @param userLng   经度2
     * @param userLat   纬度2
     * @return 返回距离,单位km
     */
    private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {
        return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),
                spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;
    }

 由于用户间距离的排序是在业务代码中实现的,可以看到SQL语句也非常的简单。

SELECT * FROM nearby_user
WHERE 1=1
AND (longitude BETWEEN #{minlng} AND #{maxlng})
AND (latitude BETWEEN #{minlat} AND #{maxlat})

但是数据库查询性能毕竟有限,如果「附近的人」查询请求非常多,在高并发场合,这可能并不是一个很好的方案。

Redis GEO 实现

GEO 类型是将经纬度的经过 GeoHash 编码的合并值作为 Sorted Set 元素的 score 权重,Redis 的 GEO 有哪些指令呢?

我们需要把登陆 app 的女生 ID 和对应的经纬度存到 Sorted Set 里面。

更多 GEO 类型指令可参考:https://redis.io/commands#geo

GEOADD

Redis 提供了 GEOADD key longitude latitude member 命令,将一组经纬度信息和对应的「女神 ID」记录到 GEO 类型的集合中,如下:一次记录多个用户(苍井空、波多野结衣)的经纬度信息。

GEOADD girl:localtion 13.361389 38.115556 "苍井空" 15.087269 37.502669 "波多野结衣"

GEORADIUS

我登陆了 app,获取自己的经纬度信息,如何查找以这个经纬度为中心的一定范围内的其他用用户呢?

Redis GEO类型提供了 GEORADIUS指令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素。

假设自己的经纬度是(15.087269 37.502669),需要获取附近 10 km 的「女神」并返回给 LBS 应用:

GEORADIUS girl:locations 15.087269 37.502669 km ASC COUNT 10

ASC可以实现让「女神」信息按照这个距离自己的经纬度由近到远排序。

COUNT选项表示指定返回的「女神」数量,防止附近太多「女神」,节省带宽资源。

用户下线后,如删除下线的「女神」经纬度呢?

这个问题问得好,GEO 类型是基于 Sorted Set 实现的,所以可以借用 ZREM 命令实现对地理位置信息的删除。

比如删除「苍井空」的位置信息:ZREM girl:localtion "苍井空"

你可能感兴趣的:(redis,算法)