场景解决方案:附近的人(GeoHash的应用)

前言

  附近的人,这四个字的需求就大有文章可做了。很二逼的做法是,存每个人的经度纬度,然后遍历数据库所有数据,foreach循环,两点距离坐标公式。量少的时候,这个没啥问题。量大了,扫描全表 + 经纬度距离运算分分钟拖垮数据库。那么是否有方案可以解决这个痛点呢,今年就来说下Geohash

实现思路

  想要不拖垮数据,要做到能走索引。就是跟你无关的点,不要扫描。减少扫描行数来实现减轻数据库的压力。那么减少扫描行数肯定要想到索引。可是经纬度有两个字段,且查询条件无论怎么写都没办法走索引。那么唯一能想到的就是二维变一维。 geohash基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码,这种方式简单粗暴,可以满足对小规模的数据进行经纬度的检索。两个点的距离越近,他们的编码前缀部分就相同,前缀部分相同越多,代表距离越近。然后我们做数据库扫描的时候 可以 WHERE geohash Like 'code%'这样就起到了走索引从而优化了执行效率。

代码思路(PHP)

<?php
class Geohash
{
    const  LATITUDE = 1;
    const LONGITUDE = 2;
    const BASE32 = array(
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm',
        'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
        'y', 'z');

    public static function encode($latitude = 0, $longitude = 0, $level = 11)
    {
        $latitude_str  = self::separate($latitude, '', 1, self::get_precision_level_num($level, self::LATITUDE), self::get_interval_value(self::LATITUDE));
        $longitude_str = self::separate($longitude, '', 1, self::get_precision_level_num($level, self::LONGITUDE), self::get_interval_value(self::LONGITUDE));
        return self::geohash_encode(self::combination($latitude_str, $longitude_str));
    }

    public static function decode($geohash)
    {
        $data = array();
        $combination_str = self::geohash_decode($geohash);
        $separate_str = self::de_combination($combination_str);
        $data[self::LATITUDE] = self::de_separate($separate_str[self::LATITUDE],1,self::get_interval_value(self::LATITUDE));
        $data[self::LONGITUDE] = self::de_separate($separate_str[self::LONGITUDE],1,self::get_interval_value(self::LONGITUDE));

        return $data;
    }


    /**
     * 编码
     */

    /**
     * @param float $num经度或纬度
     * @param string $str递归字符串
     * @param int $i 递归次数
     * @param int $max_separate_num递归总次数
     * @param array $data 区间值
     * @return string
     */
    public static function separate($num, $str = '', $i = 1, $max_separate_num = 20, $data = array('min' => -90, 'max' => 90))
    {
        $count   = ($data['max'] - $data['min']) / 2;
        $limit_0 = array(
            'min' => $data['min'],
            'max' => $data['min'] + $count
        );
        $limit_1 = array(
            'min' => $data['min'] + $count,
            'max' => $data['max']
        );
        $str     .= $num > $limit_1['min'] ? 1 : 0;
        if ($i >= $max_separate_num) {
            return $str;
        } else {
            return self::separate($num, $str, $i + 1, $max_separate_num, $num > $limit_1['min'] ? $limit_1 : $limit_0);
        }
    }

    /**
     * @param $latitude_str 纬度
     * @param $longitude_str 经度
     */
    public static function combination($latitude_str, $longitude_str)
    {
        $str = '';
        for ($i = 0; $i < strlen($longitude_str); $i++) {//根据精度表,可发现维度>=精度
            $str .= $longitude_str{$i};
            if(isset($latitude_str{$i})){
                $str .=  $latitude_str{$i};
            }
        }
        return $str;
    }

    public static function geohash_encode($str)
    {
        $str_arr    = str_split($str, 5);//按5位分割字符串
        $encode_str = '';
        foreach ($str_arr as $va) {
            $decimal    = bindec($va);
            $encode_str .= self::BASE32[$decimal];
        }
        return $encode_str;
    }
    /**
     * 编码
     */

    /**
     * 解码
     */
    public static  function geohash_decode($str)
    {
        //根据一位字符串进行切割
        $str_arr    = str_split($str, 1);
        $decode_str = '';
        $base32     = array_flip(self::BASE32);
        foreach ($str_arr as $va) {
            $decode_str .= str_pad(decbin($base32[$va]),5,'0',STR_PAD_LEFT);
        }
        return (string)$decode_str;

    }

    /**
     * 解码二进制组合
     * @param $str
     * @return array
     */
    public static function de_combination($str)
    {
        $latitude_str  = '';
        $longitude_str = '';
        //根据两位字符串切割
        $str_arr = str_split($str, 2);
        foreach ($str_arr as $va) {
            $longitude_str .= $va[0];
            if(isset($va[1])){//根据精度表,可发现维度>=精度
                $latitude_str  .= $va[1];
            }
        }
        return array(
            self::LATITUDE=>$latitude_str,
            self::LONGITUDE=>$longitude_str,
        );
    }

    /**
     * 解码二分区间
     * @param $str
     * @param string $i//执行次数
     * @param array $data、、区间
     */
    public static function de_separate($str,$i=1,$data = array('min' => -90, 'max' => 90)){
        $count   = ($data['max'] - $data['min']) / 2;
        $limit_0 = array(
            'min' => $data['min'],
            'max' => $data['min'] + $count
        );
        $limit_1 = array(
            'min' => $data['min'] + $count,
            'max' => $data['max']
        );
        if($str[$i-1]==0){
            $data = $limit_0;
        }else{
            $data = $limit_1;
        }

        if ($i >= strlen($str)) {
            return $data;
        } else {
            return self::de_separate($str, $i + 1, $data);
        }
    }

    /**
     * 解码
     */

    /**
     * 根据精度获取二分次数
     * @param $level
     * @param $type
     */
    public static function get_precision_level_num($level, $type = self::LATITUDE)
    {
        $precision = array(
            1  => array(
                self::LATITUDE  => 2,
                self::LONGITUDE => 3,
            ),
            2  => array(
                self::LATITUDE  => 5,
                self::LONGITUDE => 5,
            ),
            3  => array(
                self::LATITUDE  => 7,
                self::LONGITUDE => 8,
            ),
            4  => array(
                self::LATITUDE  => 10,
                self::LONGITUDE => 10,
            ),
            5  => array(
                self::LATITUDE  => 12,
                self::LONGITUDE => 13,
            ),
            6  => array(
                self::LATITUDE  => 15,
                self::LONGITUDE => 15,
            ),
            7  => array(
                self::LATITUDE  => 17,
                self::LONGITUDE => 18,
            ),
            8  => array(
                self::LATITUDE  => 20,
                self::LONGITUDE => 20,
            ),
            9  => array(
                self::LATITUDE  => 22,
                self::LONGITUDE => 23,
            ),
            10 => array(
                self::LATITUDE  => 25,
                self::LONGITUDE => 25,
            ),
            11 => array(
                self::LATITUDE  => 27,
                self::LONGITUDE => 28,
            ),
            12 => array(
                self::LATITUDE  => 30,
                self::LONGITUDE => 30,
            ),
        );
        return $precision[$level][$type];

    }

    /**
     * 获取区间
     * @param $type
     * @return mixed
     */
    public static function get_interval_value($type = self::LATITUDE)
    {
        $interval = array(
            self::LATITUDE  => array(
                'min' => -90,
                'max' => 90
            ),
            self::LONGITUDE => array(
                'min' => -180,
                'max' => 180
            ),
        );
        return $interval[$type];
    }
}

精度值

如图,当前缀码相同为7相差76米左右,为8相差19米,为9的话可以近似理解为那个人就在你身边了。

clipboard.png


Lowky
服务端程序狗一枚

Go Go Go

4.5k 声望
70 粉丝
0 条评论
推荐阅读
运维笔记:防ssh恶意登录神器denyhosts
只要一台服务器暴露到公网,而且没有禁止ssh账号密码登录或者使用vpn 跳板机等防控行为,那么必定会受到一大波恶意登录尝试。这里推荐一款神器,denyhosts,专门用来应付这种情况。该

谢远东1阅读 2.3k

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.3k评论 4

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 1.8k评论 2

封面图
微信公众号开发:自动回复文本/图片/图文消息/关键词回复/上传素材/自定义菜单
对接流程1、申请微信公众号测试账号URL:[链接]2、登录,配置开发者服务器URL和Token开发者服务器配置代码:config.php {代码...} URL是config.php在你服务器的URLToken是上面代码自己设置的Token搞定之后,就能完...

TANKING2阅读 10k

Hyperf 3.0 发布,PHP 新时代
在过去的一年半时间里,Hyperf 2.2 共发布了 35 个小版本,使 Hyperf 达到了一个前所未有的高度,这里也获得了一些不错的数据反馈。

huangzhhui3阅读 1.1k

封面图
多个著名 Go 开源项目被放弃,做大开源不能用爱发电,更不能只靠自己!
大家好,我是煎鱼。相信关注我的许多同学都有接触 Go 语言的开发,甚至在企业中多有实践。那么你在日常开发中,势必会接触到 gorilla 组织下的各个 Go 开源项目。如下图:gorilla/mux:Star:17.9k。a powerful r...

煎鱼1阅读 2.3k

Go for 循环有时候真的很坑。。。
大家好,我是煎鱼。不知道有多少 Go 的面试题和泄露,都和 for 循环有关。今天我在周末认真一看,发现了 redefining for loop variable semantics 。著名的硬核大佬 Russ Cox 表示他一直在研究这个问题,并表示十...

煎鱼阅读 3.4k

Go Go Go

4.5k 声望
70 粉丝
宣传栏