一、为什么你的"附近的人"功能总超时?
上周帮朋友排查一个LBS交友App的性能问题:用户量刚破千万,"附近5公里"查询就从200ms飙升到3秒,数据库直接被拖垮。后来发现他们还在用最原始的SQL查询:
SELECT * FROM users
WHERE latitude BETWEEN 30.5-0.01 AND 30.5+0.01
AND longitude BETWEEN 120.3-0.01 AND 120.3+0.01;
这种"画正方形"的笨办法,在百万级用户时就会扫描全表。今天就从后端视角聊聊,怎么设计一个支撑亿级用户的地理空间算法架构。
二、3种算法选型:从"快递分拣"到"城市规划"
1. GeoHash:把地球装进字符串的黑科技
核心思想:像切蛋糕一样把地球切成网格,每个网格用字符串编码(如wtw3e8
)。两个用户编码前缀越像,距离越近。
举个栗子:上海人民广场的GeoHash是wtw3e8
,外滩是wtw3eg
——前5位相同,说明在同一区域。
代码实战(Java实现编码):
public String encode(double lat, double lon) {
StringBuilder hash = new StringBuilder();
double[] latRange = {-90.0, 90.0};
double[] lonRange = {-180.0, 180.0};
for (int i = 0; i < 12; i++) { // 12位精度约1米
// 经度编码
double midLon = (lonRange[0] + lonRange[1]) / 2;
if (lon >= midLon) {
hash.append('1');
lonRange[0] = midLon;
} else {
hash.append('0');
lonRange[1] = midLon;
}
// 纬度编码(省略类似逻辑)
}
return base32Encode(hash.toString()); // 二进制转Base32字符串
}
坑点:高纬度地区网格变形(如挪威),需结合距离二次过滤。
2. Redis GEO:一行命令搞定附近的人
Redis 3.2+内置GEO模块,底层用Z-order曲线索引,支持毫秒级查询:
# 添加用户位置
GEOADD user_loc 121.4737 31.2304 "user_10086"
# 查找5公里内用户,返回距离
GEORADIUS user_loc 121.4737 31.2304 5 km WITHDIST
性能测试:1000万用户数据,查询耗时稳定在2ms内,比MongoDB快3倍。
适用场景:中小规模App(千万级用户),运维成本低。
3. 动态网格+内存计算:Liao系统的亿级方案
当用户破10亿,Redis也扛不住?Liao系统的骚操作:
- 网格分区:赤道10公里/格,北极100公里/格,用户位置存内存GridMap
- 查询流程:
- 用户A位于GridID=10086
- 加载GridID=10086及周边8个网格用户(共9个)
- 内存中计算距离并排序,返回Top20
- 内存占用:10亿用户×(ID+经纬度)=12GB,单台服务器搞定
三、性能优化:从3秒到3毫秒的秘密
-
索引设计:
- MySQL用SPATIAL索引(仅MyISAM支持)
- MongoDB建2dsphere索引:
db.users.createIndex({loc: "2dsphere"})
-
隐私合规:
- 动态虚拟ID:用户真实位置→临时坐标(如偏移500米),每小时刷新
- 差分隐私:添加拉普拉斯噪声,满足《网络数据安全管理条例》
-
避坑指南:
- 别用float存经纬度!精度不够导致用户"穿越"到隔壁城市
- 高并发场景预计算网格用户,避免实时计算
四、总结:技术选型决策树
用户量 < 100万 → Redis GEO
100万 ~ 1亿 → GeoHash+Redis缓存
> 1亿 → 动态网格+内存计算
最后:LBS算法的本质是空间换时间——用合理的索引策略减少计算量。关注「服务端技术精选」,每周分享后端干货,让我们一起在技术的道路上越走越远!