使用 Redis 实现查找附近用户功能

228 阅读3分钟

这是 Java 和 Go 语言的 Redis 地理位置实现示例。直接上代码。

Java:

  1. 注入 StringRedisTemplate
private static final String USER_LOCATION_KEY = "user_location";
private final StringRedisTemplate stringRedisTemplate;
  1. 存储五个用户的地理位置
/**
 * 存储用户地理坐标信息
 *
 * @param userId 用户 ID
 * @param longitude 经度(-180 - 180)
 * @param latitude 纬度(-90 - 90)
 * <p></p>
 * 根据坐标反查地址:http://api.map.baidu.com/lbsapi/getpoint/index.html
 *
 * <p></p>
 * 一般情况下,小数点后保留 5 到 6 位足以精确到街道级别。例如,保留到小数点后 6 位,精度约为 0.1 米。<p><p>
 *
 * 模拟数据<p>
 * localhost/geo/saveUserLocation/1/116.000000/40.123456
 * localhost/geo/saveUserLocation/2/116.000100/40.123456
 * localhost/geo/saveUserLocation/3/116.000210/40.123456
 * localhost/geo/saveUserLocation/4/116.000330/40.123456
 * localhost/geo/saveUserLocation/5/116.000460/40.123456
 */
@GetMapping("saveUserLocation/{userId}/{longitude}/{latitude}")
public void saveUserLocation(@PathVariable String userId, @PathVariable double longitude, @PathVariable double latitude) {
    stringRedisTemplate.opsForGeo().add(USER_LOCATION_KEY, new Point(longitude, latitude), userId); // 底层用zset实现
}

可以看到,Redis 通过使用 ZSET 存储经纬度信息,经纬度数据通过 GeoHash 进行编码,再通过奇偶互插的方式组成一个 score。

  1. 根据用户ID,获取离自己最近的用户
 /**
 * 注意:引入redisson会让Distance的Metric属性被覆盖,导致方法失效
 * 获取距离自己最近的人,基于Redis GEORADIUS
 * <p>
 * 业务场景:查找附近的东西/实时共享位置(轮询获取)/同城
 */
@GetMapping("findNearestUser/{userId}")
public ResponseEntity<?> findNearestUser(@PathVariable String userId) {
    // 获取请求用户的地理位置坐标
    List<Point> points = stringRedisTemplate.opsForGeo().position(USER_LOCATION_KEY, userId);
    if (CollectionUtils.isEmpty(points)) {
        return null;
    }
    Point point = points.get(0);
    // 查找离该用户最近的limit位用户
    GeoResults<RedisGeoCommands.GeoLocation<String>> radius = stringRedisTemplate
            .opsForGeo()
            .radius(USER_LOCATION_KEY,
                    new Circle(point, new Distance(100)),
                    RedisGeoCommands
                            .GeoRadiusCommandArgs
                            .newGeoRadiusArgs()
                            .includeCoordinates() // 如果不指定此参数,查询结果将不包含用户的坐标信息。
                            .includeDistance() // 如果不指定此参数,查询结果将不包含距离信息。
                            .sortAscending() // 进行升序排序,即按距离从近到远的顺序排列
                            .limit(10));
    return ok(radius);
}

输入 userId 为4的人进行查询,查出的结果如下,可以看到离4最近的人按照从近到远依次排列

    // 20230419105235
    // http://localhost/geo/findNearestUser/4

    {
      "averageDistance": {
        "value": 13.77858,
        "metric": "METERS"
      },
      "content": [
        {
          "content": {
            "name": "4",
            "point": {
              "x": 116.00032836198807,
              "y": 40.12345603963592
            }
          },
          "distance": {
            "value": 0.0,
            "metric": "METERS"
          }
        },
        {
          "content": {
            "name": "3",
            "point": {
              "x": 116.00021034479141,
              "y": 40.12345603963592
            }
          },
          "distance": {
            "value": 10.0374,
            "metric": "METERS"
          }
        },
        {
          "content": {
            "name": "5",
            "point": {
              "x": 116.00046247243881,
              "y": 40.12345603963592
            }
          },
          "distance": {
            "value": 11.4061,
            "metric": "METERS"
          }
        },
        {
          "content": {
            "name": "2",
            "point": {
              "x": 116.00009769201279,
              "y": 40.12345603963592
            }
          },
          "distance": {
            "value": 19.6185,
            "metric": "METERS"
          }
        },
        {
          "content": {
            "name": "1",
            "point": {
              "x": 116.00000113248825,
              "y": 40.12345603963592
            }
          },
          "distance": {
            "value": 27.8309,
            "metric": "METERS"
          }
        }
      ]
    }
  1. 其他方法
/**
* 获取两个用户之间的距离
*
* @param userId1 用户 ID 1
* @param userId2 用户 ID 2
* @return 两个用户之间的距离,单位为米
*/
@GetMapping("getUserDistance/{userId1}/{userId2}")
    public ResponseEntity<Double> getUserDistance(@PathVariable String userId1, @PathVariable String userId2) {
    // 计算两个用户之间的距离
    Distance distance = stringRedisTemplate.opsForGeo().distance(USER_LOCATION_KEY, userId1, userId2); // Metric默认米

    if (distance == null) {
        return status(HttpStatus.BAD_REQUEST).body(null);
    }

    // 返回距离(单位为米)
    return ok(distance.getValue());
}
/**
 * 获取用户的地理坐标信息
 *
 * @param userId 用户 ID
 * @return 用户的地理坐标信息
 */
@GetMapping("getUserLocation/{userId}")
public Point getUserLocation(@PathVariable String userId) {
    List<Point> points = stringRedisTemplate.opsForGeo().position(USER_LOCATION_KEY, userId); // 可以传多个userId
    if (CollectionUtils.isEmpty(points)) {
        return null;
    }
    Point point = points.get(0);
    return new Point(round(point.getX(), 6), round(point.getY(), 6));
}

/**
 * 四舍五入
 *
 * @param value 值
 * @param places 小数点后几位
 */
private static double round(double value, int places) {
    return BigDecimal.valueOf(value).setScale(places, RoundingMode.HALF_UP).doubleValue();
}

Go:

package api

import (
    "context"
    "github.com/redis/go-redis/v9"
    "math"
)

type GeoApi struct {
    redisClient *redis.Client
}

const (
    UserLocationKey = "user_location"
)

// NewGeoApi 创建 GeoApi 实例
func NewGeoApi(redisClient *redis.Client) *GeoApi {
    return &GeoApi{
        redisClient: redisClient,
    }
}

// SaveUserLocation 存自己位置
func (geoApi *GeoApi) SaveUserLocation(ctx context.Context, userId string, longitude, latitude float64) {
    geoApi.redisClient.GeoAdd(ctx, UserLocationKey, &redis.GeoLocation{
        Longitude: longitude,
        Latitude:  latitude,
        Name:      userId,
    })
}

// GetUserLocation 拿自己位置
func (geoApi *GeoApi) GetUserLocation(ctx context.Context, userId string) (longitude, latitude float64) {
    locations, _ := geoApi.redisClient.GeoPos(ctx, UserLocationKey, userId).Result()
    return Round(locations[0].Longitude, 6), Round(locations[0].Latitude, 6)
}

// FindNearestUser 找我最近的人
func (geoApi *GeoApi) FindNearestUser(ctx context.Context, userId string) []redis.GeoLocation {
    // 自己的坐标
    userLocation, _ := geoApi.redisClient.GeoPos(ctx, UserLocationKey, userId).Result()
    // 距离自己最近的user
    nearestUsers, _ := geoApi.redisClient.GeoRadius(ctx,
                                                    UserLocationKey,
                                                    userLocation[0].Longitude,
                                                    userLocation[0].Latitude,
                                                    &redis.GeoRadiusQuery{
            Radius:    100,
            Unit:      "m",
            WithCoord: true,
            WithDist:  true,
            Sort:      "ASC",
            Count:     3,
        }).Result()
    return nearestUsers
}

// GetUserDistance 两个人的距离
func (geoApi *GeoApi) GetUserDistance(ctx context.Context, userId1, userId2 string) float64 {
    distance, _ := geoApi.redisClient.GeoDist(ctx, UserLocationKey, userId1, userId2, "m").Result()
    return Round(distance, 6)
}

// Round 对 float64 值保留指定小数位数
func Round(value float64, places int) float64 {
    multiplier := math.Pow(10, float64(places))
    return math.Round(value*multiplier) / multiplier
}