PHP附近的人怎么算

wen PHP项目 6

** PHP实现“附近的人”功能全解析:从GeoHash到高性能实战指南

PHP附近的人怎么算


📚 目录导读

  1. 引言:为什么“附近的人”是LBS应用的灵魂
  2. 核心算法之争:GeoHash vs 经纬度范围直接计算
    • 1 直观但低效的“硬算”方案
    • 2 高效且优雅的GeoHash算法原理
  3. PHP代码实战:从零构建GeoHash类
    • 1 经纬度编码与解码
    • 2 计算九宫格邻居(关键步骤)
  4. 查询逻辑与SQL优化策略
    • 1 利用Redis有序集合(ZSET)加速
    • 2 MySQL空间索引(SPATIAL)方案对比
  5. 距离精确计算:Haversine公式与优化
  6. 经典问答FAQ(解决开发者90%的困惑)
  7. 选型建议与性能红线

引言:为什么“附近的人”是LBS应用的灵魂

在社交、O2O、打车等场景中,“附近的人”或“附近的店铺”是最高频的入口,对于PHP开发者而言,处理海量用户坐标并快速返回有序列表,不仅仅是调用一个API那么简单。算法的核心在于“如何用最小的计算开销,排除掉99%不相关的数据”,很多初学者直接遍历全表计算距离,当用户量过万时,数据库CPU瞬间飙高,请求超时。

本文将基于业界主流的GeoHash算法,结合PHP和Redis,为你呈现一套经得起高并发考验的解决方案,并对比多种实现方式的优缺点。

核心算法之争:GeoHash vs 经纬度范围直接计算

1 直观但低效的“硬算”方案

最原始的方法是:SELECT * FROM users WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?,这需要先根据当前坐标反推一个矩形边界,虽然能用索引,但无法解决“距离排序”问题,且矩形对角线与等距圆存在误差。

2 高效且优雅的GeoHash算法原理

GeoHash将二维的经纬度转换成一维的Base32字符串,其核心思想是:

  • 将地球纬度区间[-90,90]二分,经度区间[-180,180]二分。
  • 通过不断对半分割,将坐标落点编码为二进制序列。
  • 最后将经纬度二进制交替合并,转成Base32字符串。

关键优势字符串前缀匹配即代表空间邻近性wx4g0wx4g1 这两个前缀为 wx4g 的编码,代表它们在同一个网格内,这意味着我们只需在数据库中查询前缀相同的记录即可,无需计算距离。

PHP代码实战:从零构建GeoHash类

1 经纬度编码与解码

以下是一个极简的PHP实现(非扩展):

<?php
class GeoHash {
    // 将经纬度转换为二进制字符串
    private function encodeBinary($range, $value, $precision = 30) {
        $binary = '';
        $low = $range[0]; $high = $range[1];
        for ($i = 0; $i < $precision; $i++) {
            $mid = ($low + $high) / 2;
            if ($value > $mid) {
                $binary .= '1';
                $low = $mid;
            } else {
                $binary .= '0';
                $high = $mid;
            }
        }
        return $binary;
    }
    // 主编码函数
    public function encode($lat, $lng) {
        $latBin = $this->encodeBinary([-90, 90], $lat);
        $lngBin = $this->encodeBinary([-180, 180], $lng);
        // 交替合并
        $combined = '';
        for ($i = 0; $i < strlen($latBin); $i++) {
            $combined .= $lngBin[$i] . $latBin[$i];
        }
        // 每5位转为一个Base32字符
        $base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
        $hash = '';
        for ($i = 0; $i < strlen($combined); $i += 5) {
            $index = bindec(substr($combined, $i, 5));
            $hash .= $base32[$index];
        }
        return $hash; // 返回如 "wx4g0fe"
    }
}
?>

2 计算九宫格邻居(关键步骤)

为什么需要九宫格? 因为目标用户可能恰好位于当前网格的边缘,如果只查当前网格的编码前缀,会漏掉隔壁网格紧挨着的用户。解决方案:计算出当前编码的8个邻居网格编码,然后取并集查询。

举个例子:用户A的编码是 wx4g0,周围一圈网格分别是 wx4g2, wx4g3, wx4g1 等,在SQL中需构造 LIKE 'wx4g0%' OR LIKE 'wx4g1%' ...

高效技巧:在Redis中,甚至不需要计算九宫格,直接用ZRANGEBYSCORE按经纬度索引,但GeoHash+MySQL是入门首选。

查询逻辑与SQL优化策略

1 利用Redis有序集合(ZSET)加速(推荐)

在Redis中,KEY为每个GeoHash前缀,VALUE为用户的ID,SCORE为经纬度转换的特定整数。

  • 步骤A:将用户ID写入以其GeoHash前缀为分组的ZSET中。
  • 步骤B:计算当前用户的九宫格前缀。
  • 步骤C:使用ZUNIONSTORE合并九宫格对应的集合,并去除重复。
  • 步骤D:对结果集执行ZRANGEBYSCORE按距离排序。

性能对比:Redis方案比MySQL方案快10倍以上,因为避免了磁盘I/O。

2 MySQL空间索引(SPATIAL)方案对比

MySQL 5.7+支持POINT类型和ST_Distance_Sphere函数,虽然可以精确计算距离,但排序时全表扫描的代价极高,最优策略是:先用GeoHash粗筛(缩小范围),再用SQL精确计算距离并排序

-- 粗筛
SELECT * FROM users
WHERE geohash LIKE 'wx4g0%'
   OR geohash LIKE 'wx4g1%'
   OR geohash LIKE 'wx4g2%';
-- 再使用 HAVING distance < 5000 排序

距离精确计算:Haversine公式与优化

虽然GeoHash能定位,但距离计算仍需精确公式,Haversine公式考虑了地球曲率:

function haversine($lat1, $lng1, $lat2, $lng2) {
    $R = 6371000; // 地球半径(米)
    $lat1Rad = deg2rad($lat1);
    $lat2Rad = deg2rad($lat2);
    $deltaLat = deg2rad($lat2 - $lat1);
    $deltaLng = deg2rad($lng2 - $lng1);
    $a = sin($deltaLat/2)**2 + cos($lat1Rad) * cos($lat2Rad) * sin($deltaLng/2)**2;
    $c = 2 * atan2(sqrt($a), sqrt(1-$a));
    return $R * $c;
}

优化技巧:在PHP端只计算粗筛后的几百条数据的距离,而不是所有用户。

经典问答FAQ(解决开发者90%的困惑)

Q1:GeoHash编码长度一般取几位的? A:6位编码精度约为1.2公里,7位约为150米(适合小区级别),8位约为19米(适合室内定位)。推荐将编码长度增加1位存储为索引列,但查询时使用去掉末位的6位进行范围搜索,以免网格太碎导致跨边界漏查。

Q2:如果用户量上亿,Redis方案还能扛住吗? A:可以,可以将GeoHash作为Redis Cluster的分片键,但注意迁移成本——热网格的数据量可能极大,建议热点城市单独建立索引,或者使用Google S2算法(比GeoHash更均匀,但PHP生态较弱)。

Q3:我用ST_Distance_Sphere直接查询为什么慢? A:因为MySQL不会对ST_Distance_Sphere的结果使用二级索引。必须先用MBRContains(最小边界矩形)或GeoHash前缀过滤掉大部分行,再在结果集上执行距离计算。

Q4:怎么处理用户当前坐标的偏移? A:如果经纬度从客户端传入,需在服务端做坐标系转换(GCJ-02转WGS-84),否则,距离计算会偏差很大,国内地图SDK默认返回火星坐标系。

Q5:排序时,能不能直接用GeoHash字符串前缀排序? A绝对不行!Base32字符串的字典序并不完全等于距离远近顺序,必须计算实际距离后排序。

选型建议与性能红线

  • 初级规模(<10万用户):MySQL + GeoHash前缀索引 + PHP数组排序,简单可靠。
  • 中级规模(10万-100万):Redis ZSET + 定时同步MySQL,建议将用户坐标异步更新到Redis。
  • 高级规模(>100万):引入地理位置专用搜索引擎(如Elasticsearch的Geo Distance Query)或TiDB,PHP仅作网关。

最后提醒:计算“附近的人”时,不要忘记缓存结果5分钟——用户移动频繁但不会瞬间跳跃,可极大降低数据库压力,切记,不要在数据库里执行类似 SQRT(POWER(..., 2) + POWER(..., 2)) 这种没有索引的表达式计算。


希望这篇指南能帮你彻底搞定PHP下的“附近的人”功能,如有疑问,欢迎在评论区交流。

抱歉,评论功能暂时关闭!