如何使用Redis实现附近商家查询

news2024/11/24 0:54:31

 

导读

在日常生活中,我们经常能看见查询附近商家的功能。

常见的场景有,比如你在点外卖的时候,就可能需要按照距离查询附近几百米或者几公里的商家。

本文将介绍如何使用Redis实现按照距离查询附近商户的功能,并以SpringBoot项目作为举例。

想知道这样的功能是如何实现的吗?接着往下看吧!

Redis地理位置功能

Redis是一种高性能的键值存储数据库,具有快速读写能力和丰富的数据结构支持。在Redis 3.2版本之后,它引入了地理位置(Geospatial)功能,使其可以轻松处理与地理位置相关的数据。

地理位置功能的核心数据结构是有序集合(Sorted Set),它将元素与分数(score)关联起来。在地理位置功能中,分数表示地理位置的经度和纬度,而元素则是一个标识符,比如商户的ID。

我们只需要在数据库中存储商家的经纬度,以商家id作为key,经纬度作为value存入redis中,就可以通过redis命令来获得以某一个点为圆心一定范围内的商家,以及他们之间的距离。

 

常用命令

1. GEOADD:将地理位置添加到有序集合中
   使用GEOADD命令,可以将一个或多个地理位置添加到有序集合中。语法如下:

GEOADD key longitude latitude member [longitude latitude member ...]

示例:
   GEOADD stores 116.404 39.915 "storeA"
   GEOADD stores 116.418 39.917 "storeB"

2. GEODIST:计算两个位置之间的距离

  GEODIST命令用于计算两个位置之间的距离,可以指定单位(米、千米、英里、英尺等)。


   GEODIST key member1 member2 [unit]

   示例:

   GEODIST stores storeA storeB km

3. GEORADIUS:按照距离查询位置范围内的元素
   GEORADIUS命令用于在指定的地理位置范围内查询元素。它可以按照经纬度坐标和半径来查询,还可以限制返回的结果数量。

 GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key]

   示例:
 
   GEORADIUS stores 116.408 39.916 1 km WITHDIST COUNT 5
  

4. GEOHASH:获取位置的geohash值
   GEOHASH命令用于获取指定位置的geohash值,geohash是一种将地理位置编码成字符串的方法,可以用于快速近似的位置计算。

 GEOHASH key member [member ...]

   示例:

   GEOHASH stores storeA storeB

5. GEOPOS:获取一个或多个位置的经纬度坐标
   GEOPOS命令用于获取一个或多个位置的经纬度坐标。


   GEOPOS key member [member ...]

   示例:

   GEOPOS stores storeA storeB
   

6. GEORADIUSBYMEMBER:根据成员获取范围内的元素
   这个命令与GEORADIUS类似,但是它以一个已有的成员作为中心点进行查询。

 
   GEORADIUSBYMEMBER key member radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key]
  
   示例:
   
   GEORADIUSBYMEMBER stores storeA 1 km
   

地理位置功能不仅在查询附近商户等实际应用中非常有用,还可以应用于地理分析、位置推荐等领域。它通过利用Redis强大的有序集合数据结构,使得处理地理信息变得高效、灵活,并且易于集成到现有的应用中。无论是构建LBS应用还是处理位置相关数据,Redis的地理位置功能都能为开发者提供强大的支持。

Java代码实现

将数据库中的商家经纬度存入redis

数据库中有一张商家表,其中有经度,纬度这两个字段。我们可以通过单元测试批量将这些商家的经纬度数据存入redis。key为商家id,value为经纬度。

/**
     * 将数据库中的商户坐标添加到缓存
     */
    @Test
    void addShopGeo2Redis(){
        //获取商户集合
        List<Shop> list = shopService.list();
        //根据商户类型分类
        Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        for (Map.Entry<Long, List<Shop>> longListEntry : collect.entrySet()) {
            Long typeId = longListEntry.getKey();
            String key = "shop:geo:" + typeId;
            //获取商户经纬度
            List<Shop> shopList = longListEntry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shopList.size());
            for (Shop shop : shopList) {
//                stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString());
                //先收集完所有商户的地理位置,再一次性添加到redis
                locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(),shop.getY())));
            }
            stringRedisTemplate.opsForGeo().add(key,locations);
        }
    }

接口类:queryShopByType(typeId,current,x,y)

定义一个根据商家类型查询所有商家的接口,如果前端传来的参数中携带该用户的经纬度,则代表需要根据距离查询附近商家。

  /**
     * 根据商铺类型分页查询商铺信息
     * @param typeId 商铺类型
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/type")
    public Result queryShopByType(
            @RequestParam("typeId") Integer typeId,
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "x", required = false) Double x,
            @RequestParam(value = "y", required = false) Double y
    ) {

        return shopService.queryShopByType(typeId, current, x, y);
    }

服务类:queryShopByType(typeId,current,x,y)

1.首先判断是否经纬度参数x和y是否为空

2.计算分页参数(redis无法分页,需要手动分页)

3.查询redis

4.获取商户id集合

5.根据商户id查询数据库

6.返回

  @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断是否需要根据坐标查询
        if(x == null || y == null){
            //直接数据库查询
            Page<Shop> page = query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.ok(page.getRecords());
        }
        //2.计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        //3.查询redis,按照距离排序,分页。结果:shopId,distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
                );

        //4.解析出id
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if(list.size() <= from){
            //没有下一页
            return Result.ok(Collections.emptyList());
        }
        //4.1截取from——end部分
        List<Long> ids = new ArrayList<>(list.size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);
        });
        //5.根据id查询shop
        String idStr = StrUtil.join(",",ids);
        List<Shop> shops = query().in("id",ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops){
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }

        //6.返回
        return Result.ok(shops);
    }
}

注意点

1.redis查询的结果是从第1条到第end条,不能直接返回第begin条到第end条。

那么如何跳过begin前面的记录呢?

可以使用stream()流的skip()方法,skip()方法中指定参数begin,就会跳过前面的begin条记录。

2.通过redis获取的ids集合,再使用mybatis-plus使用query().in()进行查询时,会破坏数据顺序,如何解决?

手动指定顺序。在后面加上last("ORDER BY FIELD(id," + idStr + ")").list()。而idStr = StrUtil.join(",",ids);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/893342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

偷偷告诉你,线上便宜的卡你就得忍受这些毛病!

很多人都觉得线上的流量卡资费便宜&#xff0c;都想随手申请一张&#xff0c;在这里&#xff0c;小编偷偷的告诉大家一下&#xff0c;线上的流量卡资费虽然便宜&#xff0c;但是缺点也有不少&#xff0c;看完你能接受吗&#xff1f; ​ 我们先说一说线上流量卡的优势&#xff1…

如何自定义IIS的页面

到C:\inetpub\wwwroot 文件夹下面有一个 iisstart.htm文件 修改它内容如下 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns"http://www.w3.org/1999/xhtml&…

没有放入回收站的文件如何找回?这个方法可以帮你

在我们平常使用电脑时&#xff0c;经常需要删除一些文件或文件夹。通常情况下&#xff0c;这些文件会被移动到回收站&#xff0c;以便我们在需要时可以轻松恢复。然而&#xff0c;有时候我们希望直接永久删除文件&#xff0c;而不经过回收站。不过&#xff0c;在使用这些方法之…

九耶丨阁瑞钛伦特-Spring boot与Spring cloud 之间的关系

Spring Boot和Spring Cloud是两个相互关联的项目&#xff0c;它们可以一起使用来构建微服务架构。 Spring Boot是一个用于简化Spring应用程序开发的框架&#xff0c;它提供了自动配置、快速开发的特性&#xff0c;使得开发人员可以更加轻松地创建独立的、生产级别的Spring应用程…

Azure Bastion的简单使用

什么是Azure Bastion Azure Bastion 是一个提供安全远程连接到 Azure 虚拟机&#xff08;VM&#xff09;的服务。传统上&#xff0c;访问 VM 需要使用公共 IP 或者设立 VPN 连接&#xff0c;这可能存在一些安全风险。Azure Bastion 提供了一种更安全的方式&#xff0c;它是一个…

SB-B300、CKN1262、CKN55160楔块式单向离合器

SB-A130 S8-8130 SB-A150 SB-B150 SB-B180 SB-B180 SB-B200 SB-B200 SB-B250 SB-B250 SB-B300 SB-B300 CKN1262 CKN1568 CKN2075 CKN2590 CKN30100 CKN35110 CKN40125 CKN45130 CKN40150 CKN55160 CKN60170 CKN70190 CKN80210 CKN90230 CKN系列为楔块…

jmeter实用随机字符串常用变量 生成指定位数汇总

前言 在日常接口测试、压力测试时&#xff0c;我们需要生成大量的随机变量。例如&#xff1a;姓名、手机号、随机字符串等。这时候使用Jmeter内置的一些方法的随机生成变量&#xff0c;便成了解决问题的一种方式。 一、姓名随机生成 1.引入代码 ${__RandomString(1,赵钱孙李…

【Java转Go】快速上手学习笔记(三)之基础篇二

【Java转Go】快速上手学习笔记&#xff08;二&#xff09;之基础篇一 了解了基本语法、基本数据类型这些使用&#xff0c;接下来我们来讲数组、切片、值传递、引用传递、指针类型、函数、map、结构体。 目录 数组和切片值传递、引用传递指针类型defer延迟执行函数map结构体匿名…

历时数月钻研推流/对比各种流媒体服务程序/PK总结

1 前言 大量测试下来&#xff0c;网页显示视频流实时性从高到低依次是 webrtc > ws-flv > flv > hls。播放器打开rtsp/rtmp视频流实时性由具体的播放器控制&#xff0c;比如缓存大小和缓存时间&#xff0c;是否音视频同步等。由于flv拉流同源地址最大支持6路同时播放…

docker-php扩展

生成扩展骨架 环境&#xff1a;docker-compose、php74 1.本地要有一份 php-src git clone https://github.com/php/php-src.git cd php-src git checkout PHP-7.4.52.\www\php-src\ext可以看到有一个 ext_skel.php 文件 3.通过ext_skel.php脚本创建了一个hello扩展&#xf…

不用+号算加法(位运算实现)

最近在LeetCode上刷题看到一道非常有意思的题&#xff0c;如何不用号算加法&#xff1f;我觉得挺有意思的故而分享给大家。 在不能使用 号的情况下其实很容易想到运用位运算去解决问题&#xff0c;也就是用二进制去表示十进制加法的逻辑。所以我们可以先拆分十进制加法来帮助理…

「2024」预备研究生mem-三角形内心立体几何相接与相切

一、三角形内心&立体几何相接与相切 现场推算&#xff1a; 二、练习题

花费400元,我DIY了一台全志A133平板电脑

项目作者&#xff1a;flyn 简介&#xff1a;DIY爱好者&#xff0c;在立创开源平台开源了个人的DIY项目4G手机MiniPhone以及焊接工具焊台、恒温加热台和多功能控制台。 这是一款基于全志A133处理器DIY的平板电脑&#xff0c;可运行android和linux系统。平板搭载一块7寸1024X600…

sip网络号角喇叭 sip音柱 POE供电广播音箱 ip网络防水对讲终端 sip网络功放

SV-7042TP网络号角喇叭 一、描述 SV-7042TP是我司的一款SIP网络号角喇叭&#xff0c;具有10/100M以太网接口&#xff0c;内置有一个高品质扬声器&#xff0c;将网络音源通过自带的功放和喇叭输出播放&#xff0c;可达到功率30W。SV-7042TP作为SIP系统的播放终端&#xff0c;可…

Docker:Windows container和Linux container

点击"Switch to Windows containers"菜单时&#xff1a; 提示 然后 实际上是运行&#xff1a;com.docker.admin.exe start-service

连接pgsql数据库 sslmode sslrootcert sslkey sslcert 参数的作用

sslmode 参数的作用 sslmode 参数用于指定数据库连接时使用的 SSL 加密模式。SSL&#xff08;Secure Sockets Layer&#xff09;是一种加密协议&#xff0c;用于保护数据在客户端和服务器之间的传输过程&#xff0c;以增加数据传输的安全性。sslmode 参数可以设置不同的值&…

美团滑块模拟登录

本教程仅限于学术探讨&#xff0c;也没有专门针对某个网站而编写&#xff0c;禁止用于非法用途、商业活动、恶意滥用技术等&#xff0c;否则后果自负。观看则同意此约定。如有侵权&#xff0c;请告知删除&#xff0c;谢谢&#xff01; aHR0cHM6Ly9wYXNzcG9ydC5tZWl0dWFuLmNvbS9…

获奖方案|麒麟信安,深入电力调控国产化的最后一公里

从城市到乡村&#xff0c;从工厂车间到田间地头&#xff0c;无形的电力在有形的电网中流淌&#xff0c;支撑着企业生产、人民生活和城市运行。电力是国民经济运行的基础能源&#xff0c;其稳定运行关乎国计民生。 伴随着电力设备大规模接入&#xff0c;电力系统面临巨大的挑战&…

半导体自动化专用静电消除器主要由哪些部分组成

半导体自动化专用静电消除器是一种用于消除半导体生产过程中的静电问题的设备。由于半导体制造过程中对静电的敏感性&#xff0c;静电可能会对半导体器件的质量和可靠性产生很大的影响&#xff0c;甚至造成元件损坏。因此&#xff0c;半导体生产中采用专用的静电消除器是非常重…

QT如何打包

目录 1.windeployqt工具 2.工具位置 3.使用方法 4.注意事项 Qt Creator 默认以动态链接的方式生成可执行文件&#xff0c;该文件无法独立运行&#xff0c;必须为其提供所需的动态链接库。也就是说&#xff0c;只分享 Qt Creator 生成的可执行文件是不行的&#xff0c;必须将…