本文作者:Maxiye
本文链接:https://segmentfault.com/a/1190000038529554
geohash计算原理
经纬度地图:纵线是经度(-180~180)横线为纬度(-90~90)
GeoHash是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个二进制字符串,然后base32后成为一个短字符串。以( 123.15488794512 , 39.6584212421 )为例计算geohash:
1、第一步,经纬度分别计算出一个二进制数,通过二分法不断查找最小区间。
以经度 123.15488794512 为例,计算过程如下:
#经度转换结果
11010111100100111010
#维度转换结果
10111000011001110011
2、两个二进制组合,经度占偶数位,纬度占奇数位
11100 11101 10101 01001 01100 00111 11100 01101
#8个10进制数
28 29 21 9 12 7 28 13
wxp9d7we
3、每5位一组,进行base32编码
base32编码参照
public static function geoHash($lon, $lat, $precision = 10) {
$lonA = '';
$s = -180;$t = 180;
$totalBits = $precision * 5;
$bits = ceil($totalBits / 2);
for ($i = 0; $i < $bits; $i++) {
$mid = ($s + $t) / 2;
if ($lon >= $mid) {
$lonA .= 1;
$s = $mid;
} else {
$t = $mid;
$lonA .= 0;
}
}
$latA = '';
$s = -90;$t = 90;
$bits = floor($totalBits / 2);
for ($i = 0; $i < $bits; $i++) {
$mid = ($s + $t) / 2;
if ($lat >= $mid) {
$latA .= 1;
$s = $mid;
} else {
$t = $mid;
$latA .= 0;
}
}
$geoBinary = '';
for ($i = 0; $i < $bits; $i++) {
$geoBinary .= $lonA[$i] . $latA[$i];
}
return self::base32Encode($geoBinary, $totalBits);
}
public static function decodeGeoHash(string $geohash) {
$geoBinary = self::base32Decode($geohash);
$lonS = -180;$lonT = 180;
$latS = -90;$latT = 90;
for ($i = 0; $i < strlen($geoBinary); $i += 2) {
$lonCode = $geoBinary[$i];
$lonMid = ($lonS + $lonT) / 2;
if ($lonCode) {
$lonS = $lonMid;
} else {
$lonT = $lonMid;
}
$latCode = $geoBinary[$i + 1];
$latMid = ($latS + $latT) / 2;
if ($latCode) {
$latS = $latMid;
} else {
$latT = $latMid;
}
}
$geo = [($lonS + $lonT) / 2, ($latS + $latT) / 2];
return $geo;
}
public static function base32Encode(string $geoBinary, $bits)
{
$encodeMap = '0123456789bcdefghjkmnpqrstuvwxyz';
$encode = '';
for ($i = 0; $i < $bits; $i += 5) {
$digit = intval(substr($geoBinary, $i, 5), 2);
$encode .= $encodeMap[$digit];
}
return $encode;
}
public static function base32Decode(string $geoHash)
{
$encodeMap = '0123456789bcdefghjkmnpqrstuvwxyz';
$decode = '';
for ($i = 0; $i < strlen($geoHash); $i++) {
$digit = strpos($encodeMap, $geoHash[$i]);
$binary = base_convert($digit, 10, 2);
$decode .= sprintf('%05d', $binary);
}
return $decode;
}
public function testGeoHash()
{
$geohash = self::geoHash(123.15488794512, 39.6584212421, 10);//wxp9d7wehc
$geo = self::decodeGeoHash($geohash);// (123.15488755703, 39.658420979977)
}
geohash的使用
geohash的位数是9位数的时候,误差约为4米;geohash的位数是10位数的时候,误差为0.6米
假设数据库中存储了所有用户的geohash,根据经纬度获取附近的人:
- 给定经纬度,计算geohash
- 根据半径范围选取最小的区块,例如600m附近,可以使用6位的geohash作为最小区块
- 由于自身可能在最小区块内的任意位置,因此需要一并获取最小区块的周围8个临近区块
- 数据库中筛选geohash的6位前缀在这9个中的所有用户,然后计算距离,排除距离外的用户
redis的geo命令
6个命令:
- GEOADD 添加经纬度坐标到集合中
- GEODIST 获取集合中两个成员的距离
- GEOHASH 获取成员的geohash
- GEOPOS 获取集合中成员的经纬度坐标
- GEORADIUS 根据经纬度获取给定半径内的成员列表
- GEORADIUSBYMEMBER 根据成员获取给定半径内的成员列表
geoadd命令:
GEOADD key longitude latitude member
- 操作参数是经纬度,经度范围:-180 to 180 degrees.纬度范围:-85.05112878 to 85.05112878 degrees.
- 实际存储的数据类型是zset,第四个参数member是zset的value,score是根据经纬度计算出geohash
- geohash的精度:52bit的长整型,计算距离使用的公式是:Haversine
- 实际在redis中的数据如下图,其中score是52bit的长整型
- 由于实际存储的是geohash值,所以使用geopos获取的经纬度与实际保存值有一定误差
- 删除使用zrem,重新geoadd会更新
附近的人查询命令:
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
#上边两个命令有store选项,会被集群当作写命令,只会在主节点执行,可以用只读形式
GEORADIUS_RO
GEORADIUSBYMEMBER_RO
redis内存占用情况测试
本文作者:Maxiye
本文链接:https://segmentfault.com/a/1190000038529554