GeoHash介绍
GeoHash目前比较主流实现位置服务的技术,Geohash算法将经纬度二维数据编码为一个字符串,本质是一个将降维的过程。
举个简单的例子,在地球上为了表示一个地标点,人们通过经度和纬度的交叉点来确定,但是这个地标点的表示必须是二维的。用二维数据存储的情况下,如果搜索某个地标点A周边5公里的酒店,如果将每个点到A的距离计算一遍,计算量非常大。
简单优化:如果把地球划分两块,先判断经纬度范围,就可以减少一半的计算量,以此类推,当划分块越小计算量就越少。
geohash
进一步优化:如果把地球划分成很多区块(因为地球并不是正圆,所以划分的区块是东西窄南北宽的长方形),一个小块的所有经纬坐标都视为小块的中心点的坐标(编码为字符串方便索引),显然A点属于某个小区块内的点,搜索A点5公里酒店时,只需计算A点到周边8个小块中心点的距离,如果某个小块不满足,继续取不满足区块周边的小区块直到满足条件,将全部小区块放到集合中去重,然后取出所有符合区块内的所有酒店计算一遍距离,剔除不满足条件的酒店,这样一来就将复杂的二维降低到一维,达到快速搜索的目的。
GeoHash如何编码等过于详细的内容就不多说了,大家可以网上搜一下。
Redis的GeoHash底层存储
Redis实现GeoHash底层使用了SortedSet存储。
遇到的问题
当需要存储的元素比较多时,不能存储到一个Rediskey中,这样会因大Key的查询导致网络堵塞,但是对于Redis的GeoHash不会有这个问题,查询时会根据经纬度计算出来符合条件的元素返回,我就遇到了下面的报警,感觉是误报,既然报警就要解决,请继续往下看。
Redis报警
解决思路
对于大Key,首先想到的方法是进行拆分,如何拆分呢?
刚开始是想着根据经纬度拆分,rediskey的设计是key_lon_lat,经度和维度取整,做也勉强可以,仔细想想还是不太合适。既然有GeoHash了,为什么不用GeoHash计算出来的编码作为Rediskey呢?为了区分集群中其他类型的Rediskey,再加个前缀表示地理位置的就完全解决拆分问题了。
注意事项
- rediskey设计:使用String.format("geohash_%s", geoHash.toBase32()),下图直观看下geohash的输出结果。下图直观看下geoHash.toBase32()输出结果:
- 精度选择:如果geohash的位数是9位数的时候,大概为附近2米。如下图:
精度对比图
代码片断
import ch.hsr.geohash.GeoHash;
import ch.hsr.geohash.WGS84Point;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.kn.util.CoordinateUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.junit.Test;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @Description: Redis GeoHash测试
*/
public class RedisGeoHashTest {
@Getter
@Setter
@Data
@AllArgsConstructor
class H {
private String id;
private Double longitude;
private Double latitude;
}
@Test
public void redisGeoHash() {
Jedis jedis = new Jedis("127.0.0.1");
//确定精度
int numberOfCharacters = 5;
//Mock数据并完成数据存储
List list = Lists.newArrayList(new H("1", 121.514523, 31.10110537)
, new H("2", 121.624523, 31.20110537)
, new H("3", 121.734523, 31.30110537));
for (H h : list) {
GeoHash geoHash = GeoHash.withCharacterPrecision(h.getLatitude(), h.getLongitude(), numberOfCharacters);
String rediskey = String.format("geohash_%s", geoHash.toBase32());
jedis.geoadd(rediskey, h.getLongitude(), h.getLatitude(), h.id);
jedis.setex(String.format("h_%s", h.getId()), 86400, h.toString());
}
//优化写入:大批量数据写入可以用分批方式
//Map memberCoordinateMap = Maps.newHashMap();
//jedis.geoadd("rediskey",memberCoordinateMap);
//jedis.mset("rediskey",String[])
}
/**
* 搜索5千米内符合条件的数据
*/
@Test
public void queryList() {
Jedis jedis = new Jedis("127.0.0.1");
double radius = 5000;
double latitude = 31.22416;
double longitude = 121.366384;
//计算坐标点所在区块编码,并获取符合条件的周边区块
GeoHash geoHash = GeoHash.withCharacterPrecision(latitude, longitude, 5);
Set pointSet = Sets.newHashSet();
pointSet.addAll(Arrays.stream(geoHash.getAdjacent()).collect(Collectors.toSet()));
pointSet.addAll(getAdjacent(geoHash, radius, longitude, latitude));
System.out.println(pointSet.size());
List list = Lists.newArrayList();
for (GeoHash gh : pointSet) {
list.addAll(jedis.georadius(String.format("geohash_%s", gh.toBase32()), longitude, latitude, radius, GeoUnit.M));
}
Set hList = list.stream().map(e -> e.getMemberByString()).collect(Collectors.toSet());
//获取H集合
List keys = hList.stream().map(e -> String.format("h_%s", e)).collect(Collectors.toList());
List rtnList = jedis.mget(keys.toArray(new String[0]));
rtnList.forEach(System.out::println);
}
/**
* 返回指定经纬度到周边区块中心点小于指定距离的区块集合
*
* @param geoHash
* @param radius
* @param longitude
* @param latitude
* @return
*/
public Set getAdjacent(GeoHash geoHash, double radius, double longitude, double latitude) {
WGS84Point point = geoHash.getOriginatingPoint();
double v = CoordinateUtil.calDisByLonAndLatSimple(longitude, latitude, point.getLongitude(), point.getLatitude());
if (v < radius) {
return Sets.newHashSet(geoHash);
}
Set set = Sets.newHashSet();
GeoHash[] adjacent = geoHash.getAdjacent();
for (GeoHash gh : adjacent) {
set.addAll(getAdjacent(gh, radius, longitude, latitude));
}
return set;
}
}
需要引入如下pom:
redis.clients
jedis
3.3.0
ch.hsr
geohash
1.4.0
org.projectlombok
lombok
1.18.22
觉得有用就收藏分享,关注我更多有价值的文章会第一时间推荐给你!