Redis实战 - 11 Redis GEO 实现附近的人功能

news/2024/7/23 22:08:48 标签: redis, 数据库, 缓存

各种社交软件里面都有附件的人的需求,在该应用中,我们查询附近1公里的食客,同时只需查询出20个即可。

文章目录

    • 1. Redis GEO常用命令
    • 2. 上传用户地理位置
      • 1. RedisKeyConstant
      • 2. 控制层 NearMeController
      • 3. 业务层 NearMeService
      • 4. 项目测试
      • 5. jmeter 构造数据
    • 3. 查找附近的人
      • 1. 视图 NearMeDinerVO
      • 2. 控制层 NearMeController
      • 3. 业务层 NearMeService
      • 4. 项目测试

1. Redis GEO常用命令

命令功能参数
GEOADD添加地理位置GEOADD key longitude latitude member [longitude latitude member …]
GEODIST两点间的距离GEODIST key member1 member2 [unit]
GEOHASH返回标准的Geohash值GEOHASH key member [member …]
GEOPOS返回key中给定元素的位置信息(经纬度)GEOPOS key member [member …]
GEOREDIUS返回以某点为圆心,距离为半径的其他位置元素GEOREDIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
GEORADIUSBYMEMBER跟GEOREDIUS一样,只不过圆心是给定的member元素GEORADIUSBYMEMBER key longitude latitude radius m | km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]

GEOADD key longitude latitude member [longitude latitude member …]

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。

# 添加单个位置
127.0.0.1:6379> GEOADD diner:location 121.446617 31.205593 'zhangsan'
(integer) 1

# 添加多个位置信息
127.0.0.1:6379> GEOADD diner:location 121.4465774 31.20485103  'lisi' 121.44534  31.2031 'wangwu'  121.4510648 31.2090667 'zhangliu'
(integer) 3

GEODIST key member1 member2 [unit]

返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。 其中unit为单位 m|km|ft(英尺)|mi(英里)。

# 计算两点间的距离,返回距离的单位是米(m)
127.0.0.1:6379> GEODIST diner:location zhangsan lisi m
"82.4241"

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。范围可以使用以下其中一个单位:m 表示单位为米。km 表示单位为千米。

在给定以下可选项时, 命令会返回额外的信息:

WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。

WITHCOORD: 将位置元素的经度和维度也一并返回。

WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。

DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。

# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其位置
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHCOORD
1) 1) "wangwu"
   2) 1) "121.44534140825271606"
      2) "31.20310057881493293"
2) 1) "lisi"
   2) 1) "121.44657522439956665"
      2) "31.20485207113603821"
3) 1) "zhangsan"
   2) 1) "121.44661813974380493"
      2) "31.20559220971455971"
4) 1) "zhangliu"
   2) 1) "121.45106524229049683"
      2) "31.20906731242401833"

# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米)
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST
1) 1) "wangwu"
   2) "302.6202"
2) 1) "lisi"
   2) "82.5066"
3) 1) "zhangsan"
   2) "0.1396"
4) 1) "zhangliu"
   2) "573.0651"

# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米) 由近及远
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST ASC
1) 1) "zhangsan"
   2) "0.1396"
2) 1) "lisi"
   2) "82.5066"
3) 1) "wangwu"
   2) "302.6202"
4) 1) "zhangliu"
   2) "573.0651"


# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH
1) 1) "wangwu"
   2) (integer) 4054756135204337
2) 1) "lisi"
   2) (integer) 4054756138536712
3) 1) "zhangsan"
   2) (integer) 4054756138736536
4) 1) "zhangliu"
   2) (integer) 4054756186304127


# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值去2个
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH COUNT 2
1) 1) "zhangsan"
   2) (integer) 4054756138736536
2) 1) "lisi"
   2) (integer) 4054756138536712

GEOPOS key member [member …]

key里返回所有给定位置元素的位置(经度和纬度)。

GEOHASH key member [member …]

返回一个或多个位置元素的 Geohash 表示。保存到 Redis 中是用 Geohash 位置52点整数编码。

GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。字符串越长,表示的范围越精确。

09185339-add66a56b3da417ab00370e354c74667.png

# 计算某个位置的GeoHash值
127.0.0.1:0>GEOHASH diner:location zhangsan
 1)  "wtw3e8f9z20"

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心。

127.0.0.1:6379> geoadd haha 12.1 13.1 zhangsan 12.2 13.2 lisi
(integer) 2
127.0.0.1:6379> geodist haha zhangsan lisi
"15524.7516"
127.0.0.1:6379> georadius haha 13 14 200 km withdist
1) 1) "zhangsan"
   2) "139.6108"
2) 1) "lisi"
   2) "124.0861"
127.0.0.1:6379> georadius haha 13 14 200 km withcoord
1) 1) "zhangsan"
   2) 1) "12.10000008344650269"
      2) "13.10000039220049217"
2) 1) "lisi"
   2) 1) "12.19999819993972778"
      2) "13.20000021137872892"
127.0.0.1:6379> georadius haha 13 14 200 km withdist withcoord
1) 1) "zhangsan"
   2) "139.6108"
   3) 1) "12.10000008344650269"
      2) "13.10000039220049217"
2) 1) "lisi"
   2) "124.0861"
   3) 1) "12.19999819993972778"
      2) "13.20000021137872892"
127.0.0.1:6379> geopos haha zhangsan lisi
1) 1) "12.10000008344650269"
   2) "13.10000039220049217"
2) 1) "12.19999819993972778"
   2) "13.20000021137872892"
127.0.0.1:6379> geohash haha zhangsan lisi
1) "s62de2fcn30"
2) "s62ejdgz8d0"
127.0.0.1:6379> georadiusbymember haha zhangsan 100 km
1) "zhangsan"
2) "lisi"
127.0.0.1:6379>

2. 上传用户地理位置

在ms-diners服务中编写功能

1. RedisKeyConstant

@Getter
public enum RedisKeyConstant {

    /**
     * redis 的 key
     */
    diner_location("diner:location", "diner地理位置Key"),
    ;

    private String key;
    private String desc;

    RedisKeyConstant(String key, String desc) {
        this.key = key;
        this.desc = desc;
    }
}

GEOADD key longitude latitude member [longitude latitude member …]

2. 控制层 NearMeController

传入登录用户的lon(经度)和lat(纬度)信息,一般根据实际情况,客户端要定时去获取用户的地理位置进行上传(5s中一般刷新一次)

@RestController
@RequestMapping("nearme")
public class NearMeController {

    @Resource
    private HttpServletRequest request;
    @Resource
    private NearMeService nearMeService;

    /**
     * 更新食客坐标
     *
     * @param access_token
     * @param lon
     * @param lat
     * @return
     */
    @PostMapping
    public ResultInfo updateDinerLocation(String access_token,
                                          @RequestParam Float lon,
                                          @RequestParam Float lat) {
        nearMeService.updateDinerLocation(access_token, lon, lat);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "更新成功");
    }
}

3. 业务层 NearMeService

保存的key为:diner:location,member 为 dinerId

@Service
public class NearMeService {

    @Resource
    private DinersService dinersService;
    
    @Value("${service.name.ms-oauth-server}")
    private String oauthServerName;
    
    @Resource
    private RestTemplate restTemplate;
    
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 更新食客坐标
     *
     * @param accessToken 登录用户 token
     * @param lon         经度
     * @param lat         纬度
     */
    public void updateDinerLocation(String accessToken, Float lon, Float lat) {
        // 参数校验
        AssertUtil.isTrue(lon == null, "获取经度失败");
        AssertUtil.isTrue(lat == null, "获取纬度失败");
        // 获取登录用户信息
        SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);
        // 获取 key diner:location
        String key = RedisKeyConstant.diner_location.getKey();
        // 将用户地理位置信息存入 Redis
        RedisGeoCommands.GeoLocation geoLocation = new RedisGeoCommands
              .GeoLocation(signInDinerInfo.getId(), new Point(lon, lat));
        redisTemplate.opsForGeo().add(key, geoLocation);
    }
}

4. 项目测试

http://localhost/diners/nearme?access_token=76fc8f1c-1633-4dde-995b-7627d34616d8&lon=121.446617&lat=31.205593

在这里插入图片描述

5. jmeter 构造数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 查找附近的人

传入登录用户token,同时传入查询范围(默认3000m)以及当前用户的lon(经),lat(纬)度,为什么要传入用户此时的经纬度呢,这样查出来的结果更加准确。有可能用户处于移动状态。

1. 视图 NearMeDinerVO

@Getter
@Setter
@ApiModel(description = "关注食客信息")
public class ShortDinerInfo implements Serializable {
    
    @ApiModelProperty("主键")
    public Integer id;
    @ApiModelProperty("昵称")
    private String nickname;
    @ApiModelProperty("头像")
    private String avatarUrl;
    
}
@ApiModel(description = "附近的人")
@Getter
@Setter
public class NearMeDinerVO extends ShortDinerInfo {

    @ApiModelProperty(value = "距离", example = "98m")
    private String distance;
}

2. 控制层 NearMeController

@RestController
@RequestMapping("nearme")
public class NearMeController {

    @Resource
    private HttpServletRequest request;
    @Resource
    private NearMeService nearMeService;

    /**
     * 获取附近的人
     *
     * @param access_token 登录凭证
     * @param radius 半径
     * @param lon 登录用户的经度
     * @param lat 登录用户的纬度
     */
    @GetMapping
    public ResultInfo nearMe(String access_token,
                             Integer radius,
                             Float lon, Float lat) {
        List<NearMeDinerVO> nearMe = nearMeService.findNearMe(access_token, radius, lon, lat);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), nearMe);
    }
}

3. 业务层 NearMeService

  • 获取登录用户id
  • 获取查询半径,以米为单位,默认3000m
  • 获取用户的经纬度,如果客户端没上传经纬度,那么从Redis中读取经纬度
  • 格式化查询的半径,使用RedisTemplate的Distance对象
  • 查询限制条件:限制20,返回包含距离,按由近及远排序
  • 格式化结果,将其封装到Map中,Key为dinerId,Value构建返回的VO,同时格式化distance属性,方便客户端展示
  • 查询附近的人的信息,并添加到对应的VO中
  • 返回结果
@Service
public class NearMeService {

    @Resource
    private DinersService dinersService;
    @Value("${service.name.ms-oauth-server}")
    private String oauthServerName;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 获取附近的人
     *
     * @param accessToken 用户登录 token
     * @param radius      半径,默认 1000m
     * @param lon         经度
     * @param lat         纬度
     * @return
     */
    public List<NearMeDinerVO> findNearMe(String accessToken,
                                          Integer radius,
                                          Float lon, Float lat) {
        // 获取登录用户信息
        SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);
        // 食客 ID
        Integer dinerId = signInDinerInfo.getId();
        // 处理半径,默认 3000m
        if (radius == null) {
            radius = 3000;
        }
        // 获取 key
        String key = RedisKeyConstant.diner_location.getKey();
        // 获取用户经纬度
        Point point = null;
        if (lon == null || lat == null) {
            // 如果经纬度没传,那么从 Redis 中获取
            List<Point> points = redisTemplate.opsForGeo().position(key, dinerId);
            AssertUtil.isTrue(points == null || points.isEmpty(), "获取经纬度失败");
            point = points.get(0);
        } else {
            point = new Point(lon, lat);
        }
        // 初始化距离对象,单位 m
        Distance distance = new Distance(radius, RedisGeoCommands.DistanceUnit.METERS);
        // 初始化 Geo 命令参数对象
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs();
        // 附近的人限制 20,包含距离,按由近到远排序
        args.limit(20).includeDistance().sortAscending();
        // 以用户经纬度为圆心,范围 3000m
        Circle circle = new Circle(point, distance);
        // 获取附近的人 GeoLocation 信息
        GeoResults<RedisGeoCommands.GeoLocation> geoResult = redisTemplate.opsForGeo().radius(key, circle, args);
        // 构建有序 Map
        Map<Integer, NearMeDinerVO> nearMeDinerVOMap = Maps.newLinkedHashMap();
        // 完善用户头像昵称信息
        geoResult.forEach(result -> {
            RedisGeoCommands.GeoLocation<Integer> geoLocation = result.getContent();
            // 初始化 Vo 对象
            NearMeDinerVO nearMeDinerVO = new NearMeDinerVO();
            nearMeDinerVO.setId(geoLocation.getName());
            // 获取距离
            Double dist = result.getDistance().getValue();
            // 四舍五入精确到小数点后 1 位,方便客户端显示
            String distanceStr = NumberUtil.round(dist, 1).toString() + "m";
            nearMeDinerVO.setDistance(distanceStr);
            nearMeDinerVOMap.put(geoLocation.getName(), nearMeDinerVO);
        });
        // 获取附近的人的信息(根据 Diner 服务接口获取)
        Integer[] dinerIds = nearMeDinerVOMap.keySet().toArray(new Integer[]{});
        List<ShortDinerInfo> shortDinerInfos = dinersService.findByIds(StrUtil.join(",", dinerIds));
        // 完善昵称头像信息
        shortDinerInfos.forEach(shortDinerInfo -> {
            NearMeDinerVO nearMeDinerVO = nearMeDinerVOMap.get(shortDinerInfo.getId());
            nearMeDinerVO.setNickname(shortDinerInfo.getNickname());
            nearMeDinerVO.setAvatarUrl(shortDinerInfo.getAvatarUrl());
        });
        return Lists.newArrayList(nearMeDinerVOMap.values());
    }
}

4. 项目测试

在这里插入图片描述

{
    "code": 1,
    "message": "Successful.",
    "path": "/nearme",
    "data": [
        {
            "id": 26,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 27,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 28,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 29,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 30,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 5,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 9,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "0.0m"
        },
        {
            "id": 6,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "82.4m"
        },
        {
            "id": 7,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "302.6m"
        },
        {
            "id": 21,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "504.7m"
        },
        {
            "id": 22,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "504.7m"
        },
        {
            "id": 23,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "504.7m"
        },
        {
            "id": 24,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "504.7m"
        },
        {
            "id": 25,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "504.7m"
        },
        {
            "id": 10,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        },
        {
            "id": 11,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        },
        {
            "id": 12,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        },
        {
            "id": 13,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        },
        {
            "id": 14,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        },
        {
            "id": 15,
            "nickname": "test",
            "avatarUrl": null,
            "distance": "1413.0m"
        }
    ]
}

http://www.niftyadmin.cn/n/309.html

相关文章

golang的defer的理解- defer的函数一定会执行吗?

文章目录golang的defer什么是defer理解deferdefer什么时间执行&#xff08;defer、 return、返回值 三者的执行顺序&#xff09;defer输出的值&#xff0c;就是定义时的值。而不是defer真正执行时的变量值&#xff08;注意引用情况&#xff09;多个defer&#xff0c;执行顺序de…

毕业设计 基于51单片机无线蓝牙APP控LED灯亮灭亮度设计

基于51单片机无线蓝牙APP控LED灯亮灭亮度设计1、项目简介1.1 系统构成1.2 系统功能2、部分电路设计2.1 LED信号指示灯电路设计2.2 蓝牙模块3、部分代码展示3.1 串口初始化3.1 定时器初始化1、项目简介 选题指导&#xff0c;项目分享: https://gitee.com/lighter-z/embedded-ba…

【从零开始游戏开发】EmmyLua插件注解功能

你知道的越多&#xff0c;你不知道的越多 &#x1f1e8;&#x1f1f3;&#x1f1e8;&#x1f1f3;&#x1f1e8;&#x1f1f3; 点赞再看&#xff0c;养成习惯&#xff0c;别忘了一键三连哦 &#x1f44d;&#x1f44d;&#x1f44d; 文章持续更新中 &#x1f4dd;&#x1f4dd;…

【1024 | 程序员节】浅谈前端开发中常用的设计模式——适配器模式、工厂模式、单例模式等

前言 博主主页&#x1f449;&#x1f3fb;蜡笔雏田学代码 专栏链接&#x1f449;&#x1f3fb;【前端面试专栏】 今天学习前端面试题相关的知识&#xff01; 感兴趣的小伙伴一起来看看吧~&#x1f91e; 文章目录设计模式设计模式分类工厂模式什么是工厂模式工厂模式好处单例模式…

【 C++11 】lambda表达式

目录 1、lambda表达式的引入 2、lambda表达式 lambda表达式的语法 lambda表达式捕捉列表说明 使用lambda表达式排序自定义类型 lambda表达式的底层原理 1、lambda表达式的引入 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法&…

简明微积分(1)

在进一步学习微积分和线性代数之前&#xff0c;我们需要插入部分微积分的学习。 有点模糊&#xff0c;买了实体书&#xff0c;还没到。 重点在于后面这个证明&#xff1a; &#xff08;可以感受到&#xff0c;微积分和线性代数有点不同&#xff0c;有很多技巧性的东西&#x…

Window Terminal的安装、常见命令

WIndow Terminal原先是Window内部的一个项目&#xff0c;在2017年时&#xff0c;微软将其在GitHub开源&#xff0c;地址为&#xff1a; https://github.com/microsoft/terminal&#xff0c;用于替代Window老旧的DOS黑框框窗口。它支持Tab标签页、Emoj表情文字、检验文件的MD5值…

今天面了个阿里拿27k出来的小哥,让我见识到了什么是天花板

2022年堪称大学生就业最难的一年&#xff0c;应届毕业生人数是1076万。失业率超50%&#xff01; 但是我观察到一个数据&#xff0c;那就是已经就业的毕业生中&#xff0c;计算机通信等行业最受毕业生欢迎&#xff01; 计算机IT行业薪资高&#xff0c;平均薪资是文科其他岗位的3…