Geo redis实现附近的车辆/人

Redis 1708 浏览

前段时间业务需要实现一个用户查看附近车辆的服务:用户根据自己的定位坐标(经度、纬度)来快速搜索到附近的车辆,可以根据半径进行搜索。

根据需求也制定了几个技术方案,思路是这样的:将用户的坐标跟数据库中各个车辆的最新坐标进行距离计算,找到指定距离内的车辆并按照距离从小到大排序返回给业务。这种方法需要找到每辆车的最新定位,数据库中车辆定位数庞大,查询时数据库压力较大,并且每辆车需要计算一次距离,整个流程的耗时耗费数据库资源可想而知。考虑第二种方案,因为现在业务所有车辆的最新定位都保存在缓存中(memcached),可以采用把所有车辆的定位先从缓存中拿出来,然后进行距离计算,这种方法同样存在问题,就是操作缓存过于频繁。

以上两种方案,都是通过计算距离来判断的,后来发现geohash算法在这方面很高效(geohash原理参加 http://blog.jobbole.com/80633/)。于是将该思路应用到以上两个方案中,无非是多保存一份每个定位点的geohash值,然后根据该hash值来进行排序找到附近的车(当然该hash值在数据库中做索引)。沿着这个思路的技术实现也考虑了一些,等技术实现(具体实现思路就不啰嗦了)基本考虑好的时候,突然发现redis 自带的geo api能够完美地解决该问题。于是,啥也别说,直接使用现成的就好啦。

闲聊结束,直接操作,按照步骤来就能轻松搞定:

Jedis包引入项目

项目代码使用java编写,操作redis使用的是Jedis库,所以第一步先把Jedis找到并导入项目中。使用maven构建工程的话,在pom.xml中加入如下:

<dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
</dependency>

Redis Geo Api使用

其实Redis Geo Api很多,本文只讲两个,因为实现这个功能就只需要要两个API(无非一个插入,一个搜索),非常简单。为了便于理解,将流程分为插入端以及搜索端。

注:两个API都存在于redis.clients.jedis.JedisCluster类中。

插入端(geoadd)

插入端很好理解,就是将车辆的经纬度插入redis(服务接收到各个车辆的定位数据,然后使用geoadd接口将数据存入redis)。当然这个跟普通的redis插入操作不同,这个不同体现在内部实现上不同(可以理解为内部实现了geo算法并且存储机制不同,以满足GEO功能)。

# key:任意值。使用者可以根据车辆属性将车进行分类,key作为类别名称。例如按照城市划分,这样在搜索时知道车辆属于哪个城市时只在这个key中进行搜索
# longitude: 经度
# latitude: 纬度
# member: 成员。这里就是车牌号
public Long geoadd(final String key, final double longitude, final double latitude,final String member)

搜索端(georadius)

搜索端同样好理解,就是使用方嘛。用户给点一个定位坐标和搜索半径,列出附近的车辆以及经纬度。

# key: 对应geoadd中的key
# longitude: 经度
# latitude: 纬度
# unit: 距离单位(GeoUnit.M:米,GeoUnit.KM:千米, GeoUnit.MI:英里, GeoUnit.FT:英尺)  
# param: 参数 参见 http://redisdoc.com/geo/georadius.html
public List<GeoRadiusResponse> georadius(String key, double longitude, double latitude,double radius, GeoUnit unit, GeoRadiusParam param)

可以看到,这个函数返回的是GeoRadiusResponse的集合,拿到这个集合整个工作就完成了。该集合即附近的车的集合,元素为车牌号以及对应的经纬度。

结束了,就这些,把这两个Jedis自带的函数看明白,然后使用在正确的地方就能实现附近的车辆服务了,很简单吧!


帮助理解,看一下GeoRadiusResponse这个类的结构:

public class GeoRadiusResponse {
  private byte[] member;   
  private double distance;
  private GeoCoordinate coordinate;
 
  public GeoRadiusResponse(byte[] member) {
    this.member = member;
  }
 
  public void setDistance(double distance) {
    this.distance = distance;
  }
 
  public void setCoordinate(GeoCoordinate coordinate) {
    this.coordinate = coordinate;
  }
 
  public byte[] getMember() {
    return member;
  }
 
  public String getMemberByString() {
    return SafeEncoder.encode(member);
  }
 
  public double getDistance() {
    return distance;
  }
 
  public GeoCoordinate getCoordinate() {
    return coordinate;
  }
}

这里的member就是这里的车牌号,distance为用户跟这个车的距离,coordinate为经纬度坐标。

顺便看下GeoCoordinate这个类的结构:

public class GeoCoordinate {
  private double longitude;
  private double latitude;
 
  public GeoCoordinate(double longitude, double latitude) {
    this.longitude = longitude;
    this.latitude = latitude;
  }
 
  public double getLongitude() {
    return longitude;
  }
 
  public double getLatitude() {
    return latitude;
  }
 
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof GeoCoordinate)) return false;
 
    GeoCoordinate that = (GeoCoordinate) o;
 
    if (Double.compare(that.longitude, longitude) != 0) return false;
    return Double.compare(that.latitude, latitude) == 0;
  }
 
  @Override
  public int hashCode() {
    // follows IntelliJ default hashCode implementation
    int result;
    long temp;
    temp = Double.doubleToLongBits(longitude);
    result = (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(latitude);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }
 
  @Override
  public String toString() {
    return "(" + longitude + "," + latitude + ")";
  }
}

关注微信公众号 "Kooola大数据" :) 聊人生 | 聊技术