在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。
其实这个服务用es也可以做,但是学习以及维护成本太高了,因此我们使用Redis来做。
GEO原理
GEO使用的是Sorted Set集合类型,它底层并没有设计新的数据结构。GEO类型使用GeoHash编码方法实现了经纬度到Sorted Set中元素权重分数的转换,这其中的两个关键机制就是对二维地图做区间划分和对区间进行编码。一旦经纬度落在某个区间后,就用区间的编码来表示,并把编码值作为Sorted Set元素的权重分数。
这样一来,我们就可以把经纬度保存到Sorted Set中,利用Sorted Set提供的按权重进行有序范围查找的特性,实现LBS服务中频繁使用的搜索附近的需求。
常用命令
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
应用场景
滴滴叫车
这里以滴滴叫车的场景为例子,介绍一下具体如何使用GEO命令:GEOADD
和GEORADIUS
这两个命令。
假设车辆ID是33,经纬度位置是(116.04579,39.030452),我们可以利用好一个GEO集合保存所有车辆的经纬度,集合key是cars:locations。
执行下面的这个命令,就可以把ID号为33号的车辆当前经纬度位置存入GEO集合中:
GEOADD cars:locations 116.034579 39.030452 33
当用户想要寻找自己附近的网约车时,LBS应用就可以使用GEORADIUS梦里。
例如,LBS应用执行下面的命令时,Redis会根据输入的用户的经纬度信息(116.05479,39.030452),查找以这个经纬度为中心的5公里内的车辆信息,并返回给LBS应用。
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
代码实现
func (u *UserService) Location(ctx context.Context, request *LocationRequest) (*Response, error) {
longitude := request.Longitude
o, err := strconv.ParseFloat(longitude, 64)
if err != nil {
log.Println(err)
}
latitude := request.Latitude
a, err := strconv.ParseFloat(latitude, 64)
if err != nil {
log.Println(err)
}
userId := request.UserId
location := request.Location
s := strconv.FormatInt(userId, 10)
key := common.UserLocation + s
dao.Rdb.GeoAdd(dao.RCtx, key, &redis.GeoLocation{
Name: location,
Longitude: o,
Latitude: a,
})
return &Response{
Status: http.StatusOK,
Msg: "插入信息成功",
}, nil
}
func (u *UserService) FindFriend(ctx context.Context, request *FindFriendRequest) (*FindFriendResponse, error) {
longitude := request.Longitude
latitude := request.Latitude
o, err := strconv.ParseFloat(longitude, 64)
if err != nil {
log.Println(err)
}
a, err := strconv.ParseFloat(latitude, 64)
if err != nil {
log.Println(err)
}
key := common.UserLocation
result, err := dao.Rdb.GeoRadius(dao.RCtx, key, o, a, &redis.GeoRadiusQuery{
Radius: 5,
Unit: "km",
WithCoord: false, //传入WITHCOORD参数,则返回结果会带上匹配位置的经纬度
WithDist: true, //传入WITHDIST参数,则返回结果会带上匹配位置与给定地理位置的距离。
WithGeoHash: false, //传入WITHHASH参数,则返回结果会带上匹配位置的hash值
Sort: "ASC", //默认结果是未排序的,传入ASC为从近到远排序,传入DESC为从远到近排序。
}).Result()
if err != nil {
log.Println(err)
}
N := len(result)
name := make([]string, N)
dist := make([]float32, N)
for i := 0; i < N; i++ {
name[i] = result[i].Name
dist[i] = float32(result[i].Dist)
}
return &FindFriendResponse{
Name: name,
Dist: dist,
}, nil
}