欢迎来到“代码巫师”地理定位高并发实战讲座
各位绅士、淑女,以及那些整天在屏幕前敲代码敲得手指抽筋的“极客”们,大家好!
我是你们的老朋友,一个既喜欢用PHP写逻辑,又喜欢用数学公式折磨自己的技术专家。
今天,我们要聊的话题非常硬核,也非常“接地气”。大家打开手机,打开外卖APP,或者打开地图APP,你会看到一个神奇的功能:“附近的人”、“距离你5公里的咖啡店”、“附近的加油站”。
这背后究竟是什么魔法?难道是上帝拿着放大镜在给你找东西吗?当然不是!这是计算机科学的胜利,是数学的胜利,是数据库索引的胜利,当然,也是我们PHP程序员智慧的结晶。
但是,这里有一个巨大的坑。如果你的数据库里有几百万条数据,你要找离我1公里内的人,你怎么做?你难道要写个循环,把每一行数据都拿出来,用计算器算一遍距离,然后存到数组里,最后再排序?那样的话,你的服务器大概会在半小时后吐血而亡,而你的用户早就饿着肚子去隔壁买煎饼果子了。
所以,今天这场讲座,我们就来深度剖析:如何用PHP,玩转高性能地理位置搜索与距离排序。
准备好了吗?让我们把地图铺开,开始这场“寻宝”之旅。
第一章:距离计算的“老黄牛”——Haversine 公式
首先,我们得聊聊那个让我们又爱又恨的数学公式。
如果你在面试中被问到“怎么算地球两点距离”,你肯定脱口而出:Haversine 公式。没错,这是标准答案。它考虑了地球是圆的(虽然是个扁球体,但我们为了生活简单点,通常当它是圆的)。
公式大概长这样(别怕,我不考你背诵):
$$a = sin^2(Delta phi / 2) + cos phi_1 cdot cos phi_2 cdot sin^2(Delta lambda / 2)$$
$$c = 2 cdot text{atan2}(sqrt{a}, sqrt{1 – a})$$
$$d = R cdot c$$
其中 $phi$ 是纬度,$lambda$ 是经度,$R$ 是地球半径(约6371公里)。
在PHP里,写个函数把它算出来,那是基本功。但这仅仅是基本功。因为数学计算是很昂贵的。
/**
* 使用 Haversine 公式计算两点间距离 (米)
*
* @param float $lat1 起点纬度
* @param float $lon1 起点经度
* @param float $lat2 终点纬度
* @param float $lon2 终点经度
* @return float 距离
*/
function calculateDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadius = 6371000; // 地球半径,单位:米
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
// 测试一下
$distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737); // 北京到上海
echo "距离: " . round($distance, 2) . " 米";
这段代码写得漂不漂亮?漂亮!但它有个致命缺点:慢。
如果你的数据库里有1万条数据,这1万次计算可能只需要0.1秒,感觉不到。但如果数据量到了100万,那就是100万次循环计算三角函数。这时候,PHP引擎都在角落里瑟瑟发抖了。
所以,直接在数据库里写 ORDER BY distance,那简直是“自杀式袭击”。
第二章:空间索引的“护目镜”——MySQL Spatial Index
我们不得不提一下数据库里的“空间索引”。这就像是给数据戴上了一副护目镜,让数据库能瞬间看清哪个点在哪个点的“隔壁”。
在MySQL 5.6及以上版本,或者PostgreSQL(带PostGIS插件),都支持这种功能。
原理是: 数据库把经纬度转换成一种特殊的几何形状(比如Point),然后建立一种“树状结构”的索引,让你能迅速锁定一个区域。
PHP怎么玩?
// 1. 创建表,使用 spatial 类型
// CREATE TABLE locations (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(100),
// geo GEOMETRY NOT NULL SRID 4326
// );
// 2. 插入数据
// INSERT INTO locations (name, geo) VALUES ('北京', ST_GeomFromText('POINT(116.404 39.915)'));
// 3. PHP 查询
$stmt = $pdo->prepare("SELECT name, ST_Distance_Sphere(geo, ST_GeomFromText(:point)) as distance FROM locations WHERE ST_Distance_Sphere(geo, ST_GeomFromText(:point)) <= :radius ORDER BY distance ASC LIMIT 10");
$stmt->execute([
'point' => 'POINT(116.404 39.915)',
'radius' => 1000
]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
老专家点评:
这种方法的优点是通用,你不需要换数据库,也不需要复杂的代码。但是,在数据量极大(比如千万级)的时候,空间索引的查询性能会下降,尤其是在没有针对地理查询进行特殊硬件优化的情况下,它依然可能像蜗牛一样慢。
而且,如果你的业务逻辑比较复杂,需要结合用户的其他属性(比如“年龄小于25且性别为女且距离小于5公里”)进行查询,纯SQL的空间查询会变得非常复杂,甚至不可维护。
所以,纯SQL方案,我们只能给个“良”的评价。
第三章:GeoHash 的“马赛克魔法”
现在,让我们进入主角环节。今天的高性能核心——GeoHash。
你有没有注意过,地图上的定位框,有时候是一串字母和数字,比如 wx4erg。这就是 GeoHash。
它的核心思想是:把二维的经纬度坐标,编码成一段字符串。
想象一下,你有一张巨大的世界地图。你把它切成一个个小格子,就像俄罗斯方块一样。每一个格子都有个名字,比如 wx4 代表中国北京附近的一个大区域,wx4e 代表北京的一个小区,wx4er 代表你家里。
为什么它快?
因为字符串有“前缀匹配”的特性。
wx4er...包含在wx4e...里面。- 如果你找“附近的人”,你只需要找到一个包含你当前位置的网格,然后把这个网格周围8个格子里的数据都取出来,算一下距离就行了。
这比遍历整个数据库快一万倍!
3.1 原生PHP实现 GeoHash(不依赖库)
为了展示技术深度,我们手写一个简易版的GeoHash编码器。
GeoHash利用了二进制的“位编码”。经度用偶数位,纬度用奇数位,交叉排列。
class GeoHashEncoder {
private $base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
/**
* 编码
*/
public function encode($lat, $lon, $precision = 12) {
$latRange = [-90, 90];
$lonRange = [-180, 180];
$binary = "";
$evenBit = true;
for ($i = 0; $i < $precision; $i++) {
if ($evenBit) {
$mid = ($lonRange[0] + $lonRange[1]) / 2;
if ($lon > $mid) {
$binary .= "1";
$lonRange[0] = $mid;
} else {
$binary .= "0";
$lonRange[1] = $mid;
}
} else {
$mid = ($latRange[0] + $latRange[1]) / 2;
if ($lat > $mid) {
$binary .= "1";
$latRange[0] = $mid;
} else {
$binary .= "0";
$latRange[1] = $mid;
}
}
$evenBit = !$evenBit;
}
// 二进制转 Base32
$base32 = "";
for ($i = 0; $i < strlen($binary); $i += 5) {
$chunk = substr($binary, $i, 5);
$index = bindec($chunk);
$base32 .= $this->base32[$index];
}
return $base32;
}
}
$encoder = new GeoHashEncoder();
echo "北京坐标 GeoHash: " . $encoder->encode(39.9042, 116.4074, 8); // wx4ngn2v
3.2 GeoHash 的范围查询
光有编码还不够,我们得能根据一个GeoHash找范围。
假设你有个用户,他的GeoHash是 wx4ngn2v。
你需要找他的邻居。
邻居包括:
- 前一个字符
- 后一个字符
- 字符交换(GeoHash有个特性,如果某一位是0,把它变成1,邻居就会变;如果是1,变0)
这就有点绕了。实际上,有一个更简单的办法:前缀匹配。
如果你有个数据库,存储了所有的GeoHash。你可以建一个普通的B-Tree索引。
CREATE INDEX idx_geohash ON locations(geohash);
然后查询逻辑变成这样:
SELECT * FROM locations
WHERE geohash LIKE 'wx4ngn2v%'
OR geohash LIKE 'wx4ngn2w%'
OR geohash LIKE 'wx4ngn2x%'
-- ... 需要枚举周围8个格子
ORDER BY ...
虽然这种方法比全表扫描快,但你需要枚举邻居。如果精度很高(比如12位),邻居的数量是可控的(通常几十个)。
但是,PHP的循环处理这几十个邻居,再结合数据库查询,依然存在网络IO的开销。
第四章:Redis Geo —— 地球上的“特种部队”
各位,如果要让性能提升到极致,我们得把数据库甩到脑后,或者至少把最热的数据搬到内存里。
Redis 2.8版本以后,专门推出了 Geo 模块。这简直是地理搜索界的“核武器”。
Redis把经纬度存在一个ZSet(有序集合)里,Score是GeoHash的数值(二进制数值,不是字符串)。
为什么它快?
- 内存存储:数据都在内存里,没有磁盘IO。
- 二进制比较:ZSet是有序的,范围查询极快。
- 内置算法:Redis底层用到了极高效的算法,直接告诉你哪个点最近。
4.1 PHP 操作 Redis Geo
我们需要安装 predis/predis 库。在PHP里,连接Redis就像喝水一样简单。
require 'vendor/autoload.php';
use PredisClient;
// 连接 Redis (默认端口 6379)
$redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
]);
/**
* 模拟添加大量用户数据
*/
function seedData($redis, $count = 100000) {
$redis->del('users:locations'); // 清空数据
for ($i = 0; $i < $count; $i++) {
// 随机生成经纬度 (比如在某个区域内)
$lat = rand(116, 117) + rand(0, 9999) / 10000;
$lon = rand(39, 40) + rand(0, 9999) / 10000;
$username = "user_" . $i;
// GEOADD key longitude latitude member [longitude latitude member ...]
$redis->geoadd('users:locations', $lon, $lat, $username);
}
echo "已添加 {$count} 条数据n";
}
// 执行一下
// seedData($redis, 50000);
4.2 核心查询:距离排序
现在,我要找离用户 user_0 最近的人。
/**
* 查找附近的人
*
* @param Redis $redis
* @param string $member 当前用户
* @param float $radius 搜索半径 (米)
* @param string $unit 单位 (m, km, ft, mi)
* @param int $count 返回数量
*/
function findNearbyUsers($redis, $member, $radius = 1000, $unit = 'm', $count = 10) {
// GEOSEARCH key longitude latitude BYRADIUS radius unit [WITHCOORD] [WITHDIST] [COUNT count] [ASC|DESC] [ANY]
// 或者旧版本的 GEOPOS, GEOHASH, GEOSEARCH
$results = $redis->georadius(
'users:locations',
$lon,
$lat,
$radius,
$unit,
[
'WITHDIST' => true, // 包含距离
'WITHCOORD' => true, // 包含坐标
'COUNT' => $count,
'ORDER' => 'ASC' // 按距离升序排列
]
);
return $results;
}
老专家点评:
注意那个 WITHDIST 参数。Redis会自动帮你算好距离,并且排好序!这行代码返回的数据结构是:
[
['user_1', 500.5, ['39.9', '116.4']], // [member, distance, [lat, lon]]
['user_2', 800.1, ['39.91', '116.41']],
]
这就是你要的“高性能附近搜索”。
4.3 性能对比测试
让我们写个简单的基准测试来感受一下Redis的速度。
function benchmark($redis) {
$start = microtime(true);
// 查找附近 5000 米,返回 100 个人
$res = $redis->georadius('users:locations', 116.4, 39.9, 5000, 'm', [
'WITHDIST' => true,
'COUNT' => 100
]);
$end = microtime(true);
echo "查询耗时: " . round(($end - $start) * 1000, 2) . " 毫秒n";
echo "找到 " . count($res) . " 个结果n";
// 打印前3个结果
foreach (array_slice($res, 0, 3) as $item) {
echo "距离: {$item[1]} 米, 用户: {$item[0]}n";
}
}
// benchmark($redis); // 在有数据的情况下运行
如果你的Redis在本地,这个查询通常在 1ms 到 5ms 之间完成。无论是50条数据还是50万条数据,速度基本一致。
这就是内存索引的威力!
第五章:终极优化——网格索引
虽然Redis很快,但它不是万能的。如果数据量到了几千万甚至上亿,Redis的内存占用也会爆炸。而且,如果你需要做复杂的业务关联(比如:查出附近的人,然后查他们的订单,再查他们的等级,最后过滤),Redis的数据结构(String或ZSet)存不下那么多关联信息。
这时候,我们需要一个数据库层面的优化方案,也就是网格索引。
原理:
我们不存储GeoHash字符串,而是计算一个 网格ID。
假设我们取经纬度的前5位小数作为精度。
比如 39.90420 -> 39904。这代表一个很小的格子。
class GridIndexer {
// 假设我们用经纬度各取4位,组合成一个整数ID
// 格子大小约 11米 x 11米 (因为 0.0001度 约等于 11米)
private static $precision = 4;
public static function getGridId($lat, $lon) {
// 将浮点数转换为整数
$latGrid = (int)($lat * pow(10, self::$precision));
$lonGrid = (int)($lon * pow(10, self::$precision));
// 组合成一个大整数,或者用字符串拼接
// 为了性能,通常用整数,但要注意溢出
return $latGrid * 1000000 + $lonGrid;
}
}
查询逻辑:
- 计算当前坐标的
grid_id。 - 在数据库中查询
grid_id相同,或者grid_id +/- 1的所有记录。grid_id + 1(例如:39904 + 1 = 39905,代表下一个格子)grid_id - 1(例如:39904 – 1 = 39903)grid_id + 1000000(代表水平方向相邻)grid_id - 1000000(代表水平方向相邻)grid_id + 999999(斜向)grid_id + 1000001(斜向)grid_id - 999999(斜向)grid_id - 1000001(斜向)
这就产生了邻居查询。
PHP 实现:
function searchByGrid($pdo, $lat, $lon, $radiusMeters = 1000) {
// 1. 计算当前网格
$currentGrid = GridIndexer::getGridId($lat, $lon);
// 2. 计算需要跨越多少个格子 (半径 / 格子大小)
// 假设精度4位,格子大小约11米
$gridSize = 11;
$span = ceil($radiusMeters / $gridSize);
// 3. 构建SQL查询
$sql = "SELECT *,
(6371000 * acos(cos(radians(:lat)) * cos(radians(lat)) * cos(radians(lon) - radians(:lon)) + sin(radians(:lat)) * sin(radians(lat)))) AS distance
FROM users
WHERE grid_id IN (";
$params = [':lat' => $lat, ':lon' => $lon];
$placeholders = [];
for ($i = -1; $i <= 1; $i++) {
for ($j = -1; $j <= 1; $j++) {
// 这里的计算逻辑取决于grid_id是如何组合的
// 简单起见,我们假设grid_id = lat_grid * 1000000 + lon_grid
$targetGrid = $currentGrid + ($i * 1000000) + $j;
$placeholders[] = $targetGrid;
}
}
$sql .= implode(',', $placeholders) . ")
HAVING distance <= :radius
ORDER BY distance ASC";
$params[':radius'] = $radiusMeters;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
老专家点评:
这种方法(通常称为“桶索引” Bucket Index 或 Grid Index)是处理超大规模数据(如地图点、滴滴司机、外卖骑手)的最佳实践。
它结合了数据库的强大关联能力和索引的快速查找能力。
优缺点总结:
- 优点:内存占用可控,支持复杂的SQL关联查询(Join),通用性极强。
- 缺点:查询时需要计算一定数量的网格(通常是几十到几百个),然后过滤掉在半径外的点。这在PHP中计算几百次距离是很快的,但在数据库里,这个SQL可能有点长。
第六章:实战综合案例——构建“超级外卖”API
好了,理论讲得够多了,口水也干了。让我们来写一个完整的PHP类,模拟一个外卖APP的API。
这个API将具备以下能力:
- 注册骑手(插入数据)。
- 订单派送(找到最近的骑手)。
- 用户下单(找到附近的商家)。
我们将使用 MySQL + 空间索引 来存数据,使用 PHP 业务逻辑 来做查询。为什么不用Redis?因为演示代码中部署Redis环境太麻烦,而且MySQL的空间索引配合正确的索引,对于中小型并发(每秒几十个请求)已经绰绰有余了。我们追求的是“高性能”,而不是“极致的每秒10万次请求”。
6.1 数据库准备
CREATE TABLE `riders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`latitude` double DEFAULT NULL COMMENT '纬度',
`longitude` double DEFAULT NULL COMMENT '经度',
`geo` point NOT NULL COMMENT '空间点',
PRIMARY KEY (`id`),
SPATIAL KEY `idx_geo` (`geo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入几条测试数据
-- 注意:MySQL空间点格式 'POINT(lng lat)'
INSERT INTO `riders` (`name`, `latitude`, `longitude`, `geo`) VALUES
('张三', 39.9042, 116.4074, ST_GeomFromText('POINT(116.4074 39.9042)')),
('李四', 39.9150, 116.4040, ST_GeomFromText('POINT(116.4040 39.9150)')),
('王五', 39.9200, 116.4100, ST_GeomFromText('POINT(116.4100 39.9200)')),
('赵六', 39.8900, 116.3800, ST_GeomFromText('POINT(116.3800 39.8900)'));
6.2 PHP 服务类实现
class RiderService {
private $pdo;
public function __construct($dsn, $username, $password) {
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* 派单:找到距离用户最近的骑手
* @param float $userLat 用户纬度
* @param float $userLng 用户经度
* @param float $radius 半径(米)
* @param int $limit 限制数量
* @return array
*/
public function findNearestRider($userLat, $userLng, $radius = 2000, $limit = 1) {
$sql = "SELECT
id,
name,
ST_Distance_Sphere(geo, ST_GeomFromText(:geo)) AS distance,
ST_X(geo) AS lng,
ST_Y(geo) AS lat
FROM riders
WHERE ST_Distance_Sphere(geo, ST_GeomFromText(:geo)) <= :radius
ORDER BY distance ASC
LIMIT :limit";
// 注意:ST_X 和 ST_Y 用于提取点的X(经度)和Y(纬度)
$stmt = $this->pdo->prepare($sql);
// ST_GeomFromText 需要格式 'POINT(lng lat)'
$geoText = "POINT({$userLng} {$userLat})";
$stmt->bindParam(':geo', $geoText);
$stmt->bindValue(':radius', $radius, PDO::PARAM_INT);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 附近商家搜索
* @param float $lat
* @param float $lng
* @param float $radius
* @return array
*/
public function findNearbyShops($lat, $lng, $radius) {
$sql = "SELECT
id,
name,
address,
ST_Distance_Sphere(geo, ST_GeomFromText(:geo)) AS distance
FROM shops
WHERE ST_Distance_Sphere(geo, ST_GeomFromText(:geo)) <= :radius
ORDER BY distance ASC
LIMIT 20";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':geo' => "POINT({$lng} {$lat})",
':radius' => $radius
]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 高级:结合业务逻辑筛选
* 找到距离500米内,且评分大于4.5,且状态为“空闲”的骑手
*/
public function findOptimalRider($lat, $lng, $radius = 1000) {
$sql = "SELECT
r.id,
r.name,
r.rating,
ST_Distance_Sphere(r.geo, ST_GeomFromText(:geo)) AS distance
FROM riders r
WHERE ST_Distance_Sphere(r.geo, ST_GeomFromText(:geo)) <= :radius
AND r.status = 'free'
AND r.rating > 4.5
ORDER BY distance ASC, r.rating DESC
LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':geo' => "POINT({$lng} {$lat})",
':radius' => $radius
]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
// --- 使用示例 ---
try {
$service = new RiderService("mysql:host=localhost;dbname=test", "root", "password");
// 模拟用户位置:天安门
$userLat = 39.9042;
$userLng = 116.4074;
// 1. 找最近的骑手
$riders = $service->findNearestRider($userLat, $userLng, 3000, 5);
echo ">>> 找到最近的5个骑手:n";
foreach ($riders as $r) {
echo "- {$r['name']} (距离: " . round($r['distance'], 2) . "米)n";
}
echo "n>>> 找到最优骑手(评分高+距离近):n";
$optimal = $service->findOptimalRider($userLat, $userLng, 3000);
if ($optimal) {
echo "- {$optimal['name']} (距离: " . round($optimal['distance'], 2) . "米, 评分: {$optimal['rating']})n";
} else {
echo "没有找到合适的骑手。n";
}
} catch (PDOException $e) {
echo "Error: " . $e->getMessage();
}
运行结果分析:
这段代码利用了MySQL的空间索引。你会看到查询时间通常在 10ms – 50ms 之间。对于一次HTTP请求来说,这个延迟是完全可以接受的。而且,你可以随意修改SQL,加入 AND status = 1 或者 ORDER BY rating DESC,不需要像GeoHash那样去写复杂的边界计算代码。
第七章:避坑指南与性能陷阱
作为一名资深专家,我必须告诉你们,地理搜索里全是坑。如果你掉进去了,代码写得再漂亮也没用。
1. 经度与纬度的顺序
这是初学者最容易犯的错误。在数据库里存 lat, lon,但在传给 POINT() 函数时,必须是 lon, lat。
- SQL:
POINT(经度 纬度) - 别搞反了,不然你的点会出现在地球的另一端。
2. 球体与平面
Haversine公式是基于球体的。如果你搜索的范围非常巨大(比如跨越半个地球),或者你非常极其在意精度,球体公式没问题。
但是,如果你的搜索范围很小(比如只在一个小区内),我们可以使用平面近似公式,速度会快一丢丢。
公式:$d = sqrt{(x2-x1)^2 + (y2-y1)^2}$ (乘上缩放比例即可)。
3. 零点问题
经度在北极点是 0,在北极点附近,经度没有意义(所有经度都汇聚一点)。
如果你的搜索点在北极点附近,或者跨越了180度经线(国际日期变更线),直接计算距离可能会出Bug。MySQL的 ST_Distance_Sphere 处理得比较好,但要心里有数。
4. 索引失效
如果你没有给 geo 字段加上 SPATIAL KEY,上面的查询就会退化为全表扫描。
记得检查 SHOW INDEX FROM table;。
5. 数据一致性问题
如果骑手正在移动,怎么处理?
在Redis方案中,你可以设置一个TTL,或者实时更新ZSet。
在MySQL方案中,通常通过应用层逻辑,比如骑手每5分钟上报一次位置(或者实时上报到Redis),然后定时同步到MySQL(异步)。千万别在每次移动都直接UPDATE数据库,那是性能杀手。
第八章:Redis vs MySQL —— 到底选谁?
这是最后的决策时刻。
| 维度 | MySQL (Spatial Index) | Redis (Geo Module) |
|---|---|---|
| 数据量 | 适合百万级及以下(磁盘IO是瓶颈) | 适合千万级以上(内存无敌) |
| 数据关联 | 极强(可以直接Join订单表、用户表) | 弱(需要将关联数据放在Redis Hash或另建库) |
| 查询速度 | 10ms – 50ms | 1ms – 5ms |
| 实现复杂度 | 低(标准SQL) | 低(Redis命令) |
| 资源占用 | 中(需要磁盘存储) | 高(内存昂贵) |
我的建议:
- 起步阶段/中型项目:直接上 MySQL Spatial Index。配合上面的PHP类,代码简单,维护容易,性能足够。
- 大型项目/超实时:双写策略。Redis存热点数据(最近移动的),MySQL存全量数据。
- 客户端请求 -> 查Redis -> 如果有结果直接返回 -> 如果没有,查MySQL -> 查到后写Redis -> 返回。
结语:做一个高效的“探险家”
各位,今天我们从Haversine公式讲到了Redis的ZSet,从简单的循环讲到了空间索引。
高性能地理搜索的秘诀不是在于你会不会那个复杂的数学公式,而在于你懂不懂数据结构,懂不懂空间划分。
记住:
- 数据量小,用 GeoHash 或 MySQL Spatial。
- 数据量大,用 Redis Geo。
- 数据量爆炸且需复杂关联,用 网格索引。
写代码就像探险。不要拿着手电筒(简单循环)漫无目的地乱走,要准备好地图(索引)和望远镜(优化算法)。
希望今天的讲座能帮你在下次写“附近的人”功能时,不再写出那个会让服务器CPU飙红的“超慢”代码。
如果你在实战中遇到了什么坑,或者你有更骚的优化思路,欢迎在评论区留言。我是你们的老朋友,我们下次再见!记得给代码加注释,给服务器降降温。