关于redis:GeoHash原理及redis-geo相关操作

9次阅读

共计 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,依据经纬度获取左近的人:

  1. 给定经纬度,计算 geohash
  2. 依据半径范畴选取最小的区块,例如 600m 左近,能够应用 6 位的 geohash 作为最小区块
  3. 因为本身可能在最小区块内的任意地位,因而须要一并获取最小区块的四周 8 个邻近区块
  4. 数据库中筛选 geohash 的 6 位前缀在这 9 个中的所有用户,而后计算间隔,排除间隔外的用户

redis 的 geo 命令

6 个命令:

  • GEOADD 增加经纬度坐标到汇合中
  • GEODIST 获取汇合中两个成员的间隔
  • GEOHASH 获取成员的 geohash
  • GEOPOS 获取汇合中成员的经纬度坐标
  • GEORADIUS 依据经纬度获取给定半径内的成员列表
  • GEORADIUSBYMEMBER 依据成员获取给定半径内的成员列表

geoadd 命令:

GEOADD key longitude latitude member
  1. 操作参数是经纬度,经度范畴:-180 to 180 degrees. 纬度范畴:-85.05112878 to 85.05112878 degrees.
  2. 理论存储的数据类型是 zset,第四个参数 member 是 zset 的 value,score 是依据经纬度计算出 geohash
  3. geohash 的精度:52bit 的长整型,计算间隔应用的公式是:Haversine
  4. 理论在 redis 中的数据如下图,其中 score 是 52bit 的长整型
  5. 因为理论存储的是 geohash 值,所以应用 geopos 获取的经纬度与理论保留值有肯定误差
  6. 删除应用 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
正文完
 0