很多社交APP都有附近的人和附近的店铺、骑手等功能,那么这个功能是如何实现的呢?其实是利用Redis的GEO命令。
一、为什么选Redis?
Redis提供了GEO系列命令,底层基于GeoHash+ZSet,有如下优势:
-
内存级别速度
-
支持距离、范围、排序
-
不用自己写复杂索引
特别适合的场景有:
-
附近的人
-
附近的店铺
-
附近的司机/骑手
二、Redis GEO核心命令
| 命令 | 作用 |
| GEOADD | 添加位置 |
| GEOPOS | 查询坐标 |
| GETDIST | 计算距离 |
| GEORADIUS | 按坐标查附近半径内的人 |
| GEORADIUSBYMEMBER | 按某人查附近 |
| GEOSEARCH | 功能和GEORADIUS一样,但是可以知道查询范围为长方形区域 |
新版的redis推荐用GETSEARCH,而不是GEORADIUS。
三、直接写代码吧
3.1 准备全局Redis客户端
internal/global/redis.go
/*
generated by comer,https://github.com/imoowi/comer
Copyright © 2023 jun<simpleyuan@gmail.com>
*/
package global
import (
"github.com/redis/go-redis/v9"
"github.com/spf13/cast"
"github.com/spf13/viper"
"go.uber.org/zap"
)
// 全局Redis客户端
var Redis *redis.Client
// 初始化redis
func initRedis() {
addr := viper.GetString("redis.addr")
if addr == "" {
panic("请在配置文件里配置【redis.addr")
}
pass := viper.GetString("redis.password")
if pass == "" {
panic("请在配置文件里配置【redis.password")
}
db := viper.GetString("redis.db")
if db == "" {
panic("请在配置文件里配置【redis.db")
}
Redis = redis.NewClient(&redis.Options{
Addr: addr,
Password: pass,
DB: cast.ToInt(db),
PoolSize: 10,
})
zap.L().Info("Redis连接地址:", zap.String("addr", addr))
}
3.2 把用户的经纬度加入Redis GEO
internal/services/nearby.service.go
const LocationKey = "user_locations"
func (s *NearbyService) AddNearby(c *gin.Context, n *models.Nearby) (uint, error) {
newId, err := s.Add(c, n)
if err != nil {
return 0, err
}
if newId > 0 {
//把经纬度加入到redis
global.Redis.GeoAdd(context.Background(), LocationKey, &redis.GeoLocation{
Name: cast.ToString(c.GetUint("userId")),
Latitude: n.Latitude,
Longitude: n.Longitude,
})
}
return newId, err
}
3.3 查找坐标范围内的人
func (s *NearbyService) GetNearby(c *gin.Context, filter *models.NearbyFilter) (nearbys []*models.Nearby, err error) {
//从redis中获取经纬度
query := &redis.GeoSearchLocationQuery{
GeoSearchQuery: redis.GeoSearchQuery{
Member: cast.ToString(c.GetUint("userId")),
Longitude: filter.CenterLon,
Latitude: filter.CenterLat,
Radius: filter.Radius, //查找的半径,单位是米
RadiusUnit: "km", //半径单位,m表示米,km表示千米,mi表示英里,ft表示英尺
Sort: "asc", //排序方式,asc表示由近到远,desc表示由远到近
Count: 100, //返回的最大数量
},
WithCoord: true, //返回经纬度
WithDist: true, //返回距离
WithHash: false, //返回位置的hash值
}
locations, err := global.Redis.GeoSearchLocation(context.Background(),
LocationKey,
query).Result()
if err != nil {
return nil, err
}
for _, location := range locations {
n := &models.Nearby{}
n.UserID = cast.ToUint(location.Name)
n.Latitude = location.Latitude
n.Longitude = location.Longitude
n.Distance = location.Dist
if location.Dist <= 0.5 {
//不返回精确距离
n.Distance = 0.5
}
//取出用户信息,方便展示用户的昵称和头像等基本信息
n.User, _ = models.GetWechatUserById(n.UserID, global.MysqlDb.Client)
nearbys = append(nearbys, n)
}
return
}
四、真实业务一定要加的过滤逻辑
Redis只负责“空间筛选”,但业务还需要二次过滤。
常见的过滤逻辑:
-
排除自己
-
不在线的用户
-
被拉黑/屏蔽的用户
-
性别/年龄筛选条件
-
最近活跃时间
五、注意事项
除了打车和外卖类需要显示精确距离的App以外。需要附近功能的设计软件一定要注意:做到500米以下的距离,尽量不要精确显示距离,要模糊显示<500m,否则很容易被人开盒。
六、为什么“附近的人”会存在开盒风险?
大多数社交软件为了保护隐私,通常只显示距离(距你500米),而不会在地图上标出对方的红点。但是,利用简单的数学原理--三点定位法,坏人可以轻松实现“开盒”。什么是三点定位法,有知道的请在留言区科普一下
七、如何防止用户被“开盒”?
作为开发者,如果你直接把Redis GEO返回的精确距离给前端,就是在给“开盒”提供便利。以下是几种常见的防开盒技术方案:
7.1 按距离分段模糊化
不返回1m、10m、499m这样精确数字:在后端对距离进行取整或分档。500米内统一返回 “<500m”,一公里内返回“1km内”。这样坏人的三点定位定出来的点就无法交汇成一个点。
7.2 坐标随机偏移
在将位置存入Redis前或者从Redis取出返回的时候,加入一个随机的微小偏移量,lat=lat + random(-0.001,0.001), 坏人看到的距离永远是波动的,三点定位法就会失效。
7.3 动态掩码
不计算实际距离,而是将地图划分为若干个GeoHash网格。告诉用户“这个人在【莲花池】区域”,而不是“她在你身后一米远”。要是精确显示了距离,简直就是灾难!
7.4 频率限制
针对同一用户,限制其短时间内查询“附近的人”的次数,或者检测其是否在短时间内进行了大幅度的“位置瞬移”。阻断自动化脚本通过虚拟定位频繁采集距离数据。
一句话总结:“附近的人”本质是用 Redis GEO 做空间粗筛、业务做精筛,而真正考验一个社交产品成熟度的,不是能不能算出距离,而是敢不敢把‘精确距离’藏起来。
加班费计算器小程序 :
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!