共计 3827 个字符,预计需要花费 10 分钟才能阅读完成。
geohash 计算原理
经纬度地图:纵线是经度(-180~180)横线为纬度(-90~90)
GeoHash 是一种地址编码方法。他可能把二维的空间经纬度数据编码成一个二进制字符串,而后 base32 后成为一个短字符串。以 (123.15488794512
, 39.6584212421
) 为例计算 geohash:
1、第一步,经纬度别离计算出一个二进制数,通过二分法一直查找最小区间。
以经度 123.15488794512
为例,计算过程如下:
精度 | 左边界 | 平均值 | 右边界 | 后果 |
---|---|---|---|---|
1 | -180 | 0 | 180 | 1 |
2 | 0 | 90 | 180 | 1 |
3 | 90 | 135 | 180 | 0 |
4 | 90 | 112.5 | 135 | 1 |
5 | 112.5 | 123.75 | 135 | 0 |
6 | 112.5 | 118.125 | 123.75 | 1 |
7 | 118.125 | 120.9375 | 123.75 | 1 |
8 | 120.9375 | 122.3438 | 123.75 | 1 |
9 | 122.3438 | 123.0469 | 123.75 | 1 |
10 | 123.0469 | 123.3984 | 123.75 | 0 |
11 | 123.0469 | 123.2227 | 123.3984 | 0 |
12 | 123.0469 | 123.1348 | 123.2227 | 1 |
13 | 123.1348 | 123.1787 | 123.2227 | 0 |
14 | 123.1348 | 123.1567 | 123.1787 | 0 |
15 | 123.1348 | 123.1458 | 123.1567 | 1 |
16 | 123.1458 | 123.1512 | 123.1567 | 1 |
17 | 123.1512 | 123.154 | 123.1567 | 1 |
18 | 123.154 | 123.1554 | 123.1567 | 0 |
19 | 123.154 | 123.1547 | 123.1554 | 1 |
20 | 123.1547 | 123.155 | 123.1554 | 0 |
# 经度转换后果
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 长度 | Lat 位数 | Lon 位数 | Lat 误差 | Lon 误差 | Km 误差 |
---|---|---|---|---|---|
1 | 2 | 3 | ±23 | ±23 | ±2500 |
2 | 5 | 5 | ± 2.8 | ±5.6 | ±630 |
3 | 7 | 8 | ± 0.70 | ± 0.7 | ±78 |
4 | 10 | 10 | ± 0.087 | ± 0.18 | ±20 |
5 | 12 | 13 | ± 0.022 | ± 0.022 | ±2.4 |
6 | 15 | 15 | ± 0.0027 | ± 0.0055 | ±0.61 |
7 | 17 | 18 | ±0.00068 | ±0.00068 | ±0.076 |
8 | 20 | 20 | ±0.000086 | ±0.000172 | ±0.01911 |
9 | 22 | 23 | ±0.000021 | ±0.000021 | ±0.00478 |
10 | 25 | 25 | ±0.00000268 | ±0.00000536 | ±0.0005971 |
11 | 27 | 28 | ±0.00000067 | ±0.00000067 | ±0.0001492 |
12 | 30 | 30 | ±0.00000008 | ±0.00000017 | ±0.0000186 |
假如数据库中存储了所有用户的 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 内存占用状况测试
数据量 | 内存占用 |
---|---|
50w | 39.76M |
100w | 90.21M |
200w | 171.26M |
500w | 484.15M |
1000w | 907.26M |
正文完