java+redis 实现搜索附近人功能

最近在做一款交友软件的APP,现在有一个功能需要实现搜索附近的人。后来发现用redis 的GEO功能实现非常简。

先说一下设计思路,每个用户在登陆的时候都会添加一下经纬度,这个是APP端获取的,

然后设置一下这个经纬度到mysql数据库中,最后把经纬度同步到redis数据库中。我们先来

了解一下 redis GEO功能。

geoadd:增加某个地理位置的坐标。

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

将给定的空间元素(纬度、经度、名字)添加到指定的键里面。 这些数据会以有序集合的形式被储存在键里面, 从而使得像 GEORADIUS和 GEORADIUSBYMEMBER 这样的命令可以在之后通过位置查询取得这些元素。

GEOADD 命令以标准的 x,y 格式接受参数, 所以用户必须先输入经度, 然后再输入纬度。 GEOADD 能够记录的坐标是有限的: 非常接近两极的区域是无法被索引的。 精确的坐标限制由 EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义, 具体如下:

  • 有效的经度介于 -180 度至 180 度之间。
  • 有效的纬度介于 -85.05112878 度至 85.05112878 度之间。

当用户尝试输入一个超出范围的经度或者纬度时, GEOADD 命令将返回一个错误。

可用版本:
>= 3.2.0
时间复杂度:
每添加一个元素的复杂度为 O(log(N)) , 其中 N 为键里面包含的位置元素数量。
返回值:
新添加到键里面的空间元素数量,已经存在,再添加的话会更新当前数据,但是返回值不会包含。

redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2

redis> GEODIST Sicily Palermo Catania
"166274.15156960039"

redis> GEORADIUS Sicily 15 37 100 km
1) "Catania"

redis> GEORADIUS Sicily 15 37 200 km
1) "Palermo"
2) "Catania"
上面的是redis官方文档给出的操作。在实际运用中最好把key值作为用户id,这样比较容易检索。

geopos:获取某个地理位置的坐标。

GEOPOS key member [member ...]

从键里面返回所有给定位置元素的位置(经度和纬度)。

因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。

可用版本:
>= 3.2.0
时间复杂度:
获取每个位置元素的复杂度为 O(log(N)) , 其中 N 为键里面包含的位置元素数量。
返回值:
GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。
当给定的位置元素不存在时, 对应的数组项为空值。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 

redis> GEOPOS Sicily Palermo 
1) 1) "13.361389338970184"
   2) "38.115556395496299"
2) 1) "15.087267458438873"
   2) "37.50266842333162"
3) (nil)
geodist:获取两个地理位置的距离。

GEODIST key member1 member2 [unit]

返回两个给定位置之间的距离。

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

可用版本:
>= 3.2.0
复杂度:
O(log(N))
返回值:
计算出的距离会以双精度浮点数的形式被返回。 如果给定的位置元素不存在, 那么命令返回空值。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 

redis> GEODIST Sicily Palermo Catania
"166274.15156960039"

redis> GEODIST Sicily Palermo Catania km
"166.27415156960038"

redis> GEODIST Sicily Palermo Catania mi
"103.31822459492736"

redis> GEODIST Sicily Foo Bar
(nil)
georadius:根据给定地理位置坐标获取指定范围内的地理位置集合。

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

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD : 将位置元素的经度和维度也一并返回。
  • WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

  • ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。
  • DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。

在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。

可用版本:
>= 3.2.0
时间复杂度:
O(N+log(M)), 其中 N 为指定半径范围内的位置元素数量, 而 M 则是被返回位置元素的数量。
返回值:

GEORADIUS 命令返回一个数组, 具体来说:

  • 在没有给定任何 WITH 选项的情况下, 命令只会返回一个像 ["New York","Milan","Paris"] 这样的线性(linear)列表。
  • 在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。

在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:

  1. 以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
  2. geohash 整数。
  3. 由两个元素组成的坐标,分别为经度和纬度。

举个例子, GEORADIUS Sicily 15 37 200 km withcoord withdist 这样的命令返回的每个子数组都是类似以下格式的:

["Palermo","190.4424",["13.361389338970184","38.115556395496299"]]
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2

redis> GEORADIUS Sicily 15 37 200 km WITHDIST
1) 1) "Palermo"
   2) "190.4424"
2) 1) "Catania"
   2) "56.4413"

redis> GEORADIUS Sicily 15 37 200 km WITHCOORD
1) 1) "Palermo"
   2) 1) "13.361389338970184"
      2) "38.115556395496299"
2) 1) "Catania"
   2) 1) "15.087267458438873"
      2) "37.50266842333162"

redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
   2) "190.4424"
   3) 1) "13.361389338970184"
      2) "38.115556395496299"
2) 1) "Catania"
   2) "56.4413"
   3) 1) "15.087267458438873"
      2) "37.50266842333162"
这个方法用的最多一般也就是用这个方法,传入 经纬度,得到周围的人的id和距离。

georadiusbymember:根据给定地理位置获取指定范围内的地理位置集合。

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

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。

关于 GEORADIUSBYMEMBER 命令的更多信息, 请参考 GEORADIUS 命令的文档。

可用版本:
>= 3.2.0
时间复杂度:
O(log(N)+M), 其中 N 为指定范围之内的元素数量, 而 M 则是被返回的元素数量。
返回值:
一个数组, 数组中的每个项表示一个范围之内的位置元素。
redis> GEOADD Sicily 13.583333 37.316667 "Agrigento"
(integer) 1

redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2

redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km
1) "Agrigento"
2) "Palermo"
geohash:获取某个地理位置的geohash值。

GEOHASH key member [member ...]

返回一个或多个位置元素的 Geohash 表示。

可用版本:
>= 3.2.0
时间复杂度:
寻找每个位置元素的复杂度为 O(log(N)) , 其中 N 为给定键包含的位置元素数量。
返回值:
一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2

redis> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"
上面的都是一些理论知识,接下来用java代码实际操作一下

[java] view plain copy
  1. public class RedisUtil {  
  2.        private static JedisPool jedisPool = null;  
  3.         // Redis服务器IP  
  4.         private static String ADDR = "192.168.1.254";  
  5.         // Redis的端口号  
  6.         private static int PORT = 6380;  
  7.         // 访问密码  
  8.         private static String AUTH = "111111";  
  9.   
  10.         /** 
  11.          * 初始化Redis连接池 
  12.          */  
  13.         static {  
  14.             try {  
  15.                 JedisPoolConfig config = new JedisPoolConfig();  
  16.                 // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true  
  17.                 config.setBlockWhenExhausted(true);  
  18.                 // 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数)  
  19.                 config.setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy");  
  20.                 // 是否启用pool的jmx管理功能, 默认true  
  21.                 config.setJmxEnabled(true);  
  22.                 // 最大空闲连接数, 默认8个 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。  
  23.                 config.setMaxIdle(8);  
  24.                 // 最大连接数, 默认8个  
  25.                 config.setMaxTotal(200);  
  26.                 // 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;  
  27.                 config.setMaxWaitMillis(1000 * 100);  
  28.                 // 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;  
  29.                 config.setTestOnBorrow(true);  
  30.                 jedisPool = new JedisPool(config, ADDR, PORT, 3000, AUTH);  
  31.             } catch (Exception e) {  
  32.                 e.printStackTrace();  
  33.             }  
  34.         }  
  35.         /** 
  36.          * 获取Jedis实例 
  37.          *  
  38.          * @return 
  39.          */  
  40.         public synchronized static Jedis getJedis() {  
  41.             try {  
  42.                 if (jedisPool != null) {  
  43.                     Jedis resource = jedisPool.getResource();  
  44.                     return resource;  
  45.                 } else {  
  46.                     return null;  
  47.                 }  
  48.             } catch (Exception e) {  
  49.                 e.printStackTrace();  
  50.                 return null;  
  51.             }  
  52.         }  
  53.   
  54.         /** 
  55.          * 释放jedis资源 
  56.          *  
  57.          * @param jedis 
  58.          */  
  59.         public static void close(final Jedis jedis) {  
  60.             if (jedis != null) {  
  61.                 jedis.close();  
  62.             }  
  63.         }  
  64.   
  65.         public static void main(String[] args) {  
  66.             Jedis jedis = RedisUtil.getJedis();  
  67.               
  68.             //添加经纬度  
  69.             Coordinate coordinate=new Coordinate();  
  70.             coordinate.setLatitude(31.244803);  //维度  
  71.             coordinate.setLongitude(121.483671); //经度  
  72.             coordinate.setKey("1");  //可以作为用户表的id  
  73.               
  74.               
  75.             //添加经纬度  
  76.             Coordinate coordinate1=new Coordinate();  
  77.             coordinate1.setLatitude(31.245321);  //维度  
  78.             coordinate1.setLongitude(121.485015); //经度  
  79.             coordinate1.setKey("2");  //可以作为用户表的id  
  80.               
  81.             //添加经纬度  
  82.             Coordinate coordinate2=new Coordinate();  
  83.             coordinate2.setLatitude(31.245456);  //维度  
  84.             coordinate2.setLongitude(121.485285); //经度  
  85.             coordinate2.setKey("3");  //可以作为用户表的id  
  86.               
  87.             addReo(coordinate);  
  88.             addReo(coordinate1);  
  89.             addReo(coordinate2);  
  90.             RedisUtil.close(jedis);  
  91.         }  
  92.   
  93.         /** 
  94.          * 添加坐标 
  95.          * key 经度  维度  距离 
  96.          * return m 表示单位为米*/  
  97.         public static Long addReo(Coordinate coordinate) {  
  98.             Jedis jedis = null;  
  99.             try {  
  100.                 jedis = jedisPool.getResource();  
  101.                 //第一个参数可以理解为表名  
  102.                 return jedis.geoadd("test",coordinate.getLongitude(),coordinate.getLatitude(),coordinate.getKey());  
  103.             } catch (Exception e) {  
  104.                 System.out.println(e.getMessage());  
  105.             } finally {  
  106.                 if (null != jedis)  
  107.                     jedis.close();  
  108.             }  
  109.             return null;  
  110.         }  
  111.         /** 
  112.          * 查询附近人 
  113.          * key 经度  维度  距离 
  114.          * return GeoRadiusResponse*/  
  115.         public static List<GeoRadiusResponse> geoQuery(Coordinate coordinate) {  
  116.             Jedis jedis = null;  
  117.             try {  
  118.                 jedis = jedisPool.getResource();  
  119.                 //200F GeoUnit.KM表示km   
  120.                 return jedis.georadius("test",coordinate.getLongitude(),coordinate.getLatitude(),200F,GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist());  
  121.             } catch (Exception e) {  
  122.                 System.out.println(e.getMessage());  
  123.             } finally {  
  124.                 if (null != jedis)  
  125.                     jedis.close();  
  126.             }  
  127.             return null;  
  128.         }  
  129.   
  130. }  
执行完main方法之后我们可以看到添加了三条数据;



最后再写一个查询方法测试下:

[java] view plain copy
  1. public static void main(String[] args) {  
  2.         Jedis jedis = RedisUtil.getJedis();  
  3.              
  4.         //添加经纬度  
  5.         Coordinate coordinate=new Coordinate();  
  6.         coordinate.setLatitude(31.244803);  //维度  
  7.         coordinate.setLongitude(121.483671); //经度  
  8.         coordinate.setKey("1");  //用户表的id 以当前用户作为查询条件,查询他周围的人数   
  9.         List<GeoRadiusResponse> list=geoQuery(coordinate);  
  10.         for(GeoRadiusResponse geo:list){  
  11.             System.out.println(geo.getMemberByString()); //主键 有主键了个人信息就很简单了  
  12.             System.out.println(geo.getDistance());  //距离多少米  
  13.         }  
  14.           
  15.         RedisUtil.close(jedis);  
  16.     }  

输出语句

1
2.0E-4
2
0.1404
3
0.1698

这里主要要导入两个包

猜你喜欢

转载自blog.csdn.net/weixin_39816740/article/details/80911421