Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现
摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了。
具体算法略过... 有兴趣自行google就好。
在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。
Redis 的 Geo 指令基本使用
Redis 提供的 Geo 指令只有 6 个,读者们瞬间就可以掌握。
增加geoadd
jedis.geoadd(COMPANY, 116.48105, 39.996794, "juejin");
jedis.geoadd(COMPANY, 116.514203, 39.905409, "ireader");
jedis.geoadd(COMPANY, 116.489033, 40.007669, "meituan");
Map map = CollectionUtil.newHashMap(2);
map.put("jd",new GeoCoordinate(116.562108, 39.787602));
map.put("xiaomi",new GeoCoordinate(116.334255 ,40.027400));
jedis.geoadd(COMPANY, map);
距离geodist
geodist 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。
Double geodist = jedis.geodist(COMPANY, "juejin", "ireader", GeoUnit.KM);
Console.log("掘金与掌阅间距离"+geodist + "km");
geodist = jedis.geodist(COMPANY, "juejin", "meituan", GeoUnit.KM);
Console.log("掘金与美团间距离"+geodist + "km");
geodist = jedis.geodist(COMPANY, "juejin", "jd", GeoUnit.KM);
Console.log("掘金与京东间距离"+geodist + "km");
geodist = jedis.geodist(COMPANY, "juejin", "xiaomi", GeoUnit.KM);
Console.log("掘金与小米间距离"+geodist + "km");
geodist = jedis.geodist(COMPANY, "juejin", "juejin", GeoUnit.KM);
Console.log("掘金与掘金间距离"+geodist + "km");
掘金与掌阅间距离10.5501km
掘金与美团间距离1.3878km
掘金与京东间距离24.2739km
掘金与小米间距离12.9606km
掘金与掘金间距离0.0km
获取元素位置geopos
geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。
List geopos = jedis.geopos(COMPANY, "juejin","ireader");
Console.log("掘金与掌阅的位置:"+ JSONUtil.toJsonPrettyStr(geopos));
// ============================
掘金与掌阅的位置:[
{
"latitude": 39.9967934885826,
"longitude": 116.4810499548912
},
{
"latitude": 39.905409186624944,
"longitude": 116.51420205831528
}
]
获取元素的 hash 值
geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可以使用这个编码值去 http://geohash.org/${hash}中进行直接定位,它是 geohash 的标准编码值。
List geohash = jedis.geohash(COMPANY, "juejin", "ireader");
Console.log("掘金与掌阅的hash值:"+ JSONUtil.toJsonPrettyStr(geohash));
// ==============
掘金与掌阅的hash值:[
"wx4gd94yjn0",
"wx4g52e1ce0"
]
让我们打开地址 http://geohash.org/wx4g52e1ce0,观察地图指向的位置是否正确。
附近的公司georadiusbymember
georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂。
GeoRadiusParam param = GeoRadiusParam.geoRadiusParam().count(3).sortAscending();
List nearbyIreader = jedis.georadiusByMember(COMPANY, "ireader", 20, GeoUnit.KM, param);
Console.log("掌阅附近的公司顺序:");
for (int i = 0; i < nearbyIreader.size(); i++) {
Console.log(i+") "+nearbyIreader.get(i).getMemberByString());
}
param = GeoRadiusParam.geoRadiusParam().count(3).sortDescending();
nearbyIreader = jedis.georadiusByMember(COMPANY, "ireader", 20, GeoUnit.KM, param);
Console.log("掌阅附近的公司倒序:");
for (int i = 0; i < nearbyIreader.size(); i++) {
Console.log(i+") "+nearbyIreader.get(i).getMemberByString());
}
// withdist 显示距离,withCoord 显示坐标
param = GeoRadiusParam.geoRadiusParam().count(3).withCoord().withDist();
nearbyIreader = jedis.georadiusByMember(COMPANY, "ireader", 20, GeoUnit.KM, param);
Console.log("掌阅附近的公司:");
for (int i = 0; i < nearbyIreader.size(); i++) {
Console.log(i+") "+nearbyIreader.get(i).getMemberByString());
Console.log("坐标:"+JSONUtil.toJsonPrettyStr(nearbyIreader.get(i).getCoordinate()));
Console.log("距离:"+nearbyIreader.get(i).getDistance());
}
返回值如下:
掌阅附近的公司顺序:
0) ireader
1) juejin
2) meituan
掌阅附近的公司倒序:
0) jd
1) meituan
2) juejin
掌阅附近的公司:
0) ireader
坐标:{
"latitude": 39.905409186624944,
"longitude": 116.51420205831528
}
距离:0.0
1) juejin
坐标:{
"latitude": 39.9967934885826,
"longitude": 116.4810499548912
}
距离:10.5501
2) meituan
坐标:{
"latitude": 40.00766997707732,
"longitude": 116.48903220891953
}
距离:11.5748
georadius
根据坐标值来查询附近的元素
param = GeoRadiusParam.geoRadiusParam().count(3).withDist().sortAscending();
jedis.georadius(COMPANY, 116.514202 ,39.905409 ,20,GeoUnit.KM,param);
Console.log("根据坐标查找附近的公司:");
for (int i = 0; i < nearbyIreader.size(); i++) {
Console.log(i+") "+nearbyIreader.get(i).getMemberByString());
}
根据坐标查找附近的公司:
- ireader
- juejin
- meituan
注意事项
在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。
本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)