【案例实战】SpringBoot整合Redis的GEO实现查找附近门店功能

news2024/11/15 7:44:08

像我们平常美团点外卖的时候,都会看到一个商家距离我们多少米。还有类似QQ附近的人,我们能看到附近的人距离我们有多少米。

在这里插入图片描述

那么这些业务是怎么做的呢?是如何实现 基于位置的附近服务系统呢。

在去了解基于位置的附近服务之前,我们先来看一下什么是GIS技术。

GIS代表地理信息系统,是一种用于收集、存储、分析、管理和显示地理空间数据的技术。GIS利用计算机软件和硬件来创建、管理、分析和可视化地理信息,使用户能够更好地了解和解决地理空间问题。

简言之地图上的每一个位置都会一个经纬度坐标。根据这个坐标我们查出来附近的人,或者附近的门店之类的。

下面是基于百度的地图经纬度定位系统,大家可以自己体验下,你给它一个经纬度,他能给你定位到地图的某一个点。也就是当前经纬度的位置。

网址:http://jingweidu.757dy.com/

在这里插入图片描述

我们既然了解了经纬度这个概念,那么Redis 3.2版本之后新增的一个数据类型,是一种用于处理地理位置信息的数据结构。

GEO(地理位置):存储和查询地理位置数据,并快速地计算距离和位置集合的交集。

应用场景

  • 位置服务:可以使用GEO结构存储用户或商家的位置信息,并计算用户或商家之间的距离。
  • 商业分析:可以使用GEO结构在地图上可视化商家的分布状况,以进行市场分析和营销策略的制定。
  • 推荐系统:可以使用GEO结构计算用户与商家之间的距离,以实现根据附近商家推荐和推荐商家位置排名等功能。或者用于实现拼团和卡券等代表性做法,诸如可以实现商家对距离自己较近的用户进行自动优惠券发放等。

Redis的GEO数据结构常见的命令

  • geoadd:增加某个地理位置的坐标
    • 语法 GEOADD key longitude latitude member
  • geopos:获取某个地理位置的坐标
    • 语法 GEOPOS key [member [member ...]]
  • geodist:获取两个地理位置的距离
    • 语法 GEODIST key member1 member2 [M | KM | FT | MI]
    • 范围单位:m | km | ft | mi --> 米 | 千米 | 英尺 | 英里
  • georadius:根据给定地理位置坐标获取指定范围内的地理位置集合
    • 语法 GEORADIUS key longitude latitude radius <M | KM | FT | MI>
  • georadiusbymember:根据给定地理位置获取指定范围内的地理位置集合。
    • 语法 GEORADIUSBYMEMBER key member radius <M | KM | FT | MI>
  • geohash:获取某个地理位置的geohash值
    • 语法 GEOHASH key [member [member ...]]

SpringBoot整合案例实战

需求背景:一个用户想要找他的附近美食,使用Redis的GEO结构。

SpringBoot项目整合RedisTemplate这里就不过多介绍啦。

<!-- 创建SpringBoot项目加入redis的starter依赖 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

首先我们要先准备一些商家信息,我们先定义一个商家的实体类。

/**
 * 定义商家店铺实体类
 * @author lixiang
 * @date 2023/6/21 09:31
 */
@Data
public class Shop {

    /**
     * id
     */
    private String id;

    /**
     * 名称
     */
    private String name;

    /**
     * 精度
     */
    private BigDecimal accuracy;

    /**
     * 纬度
     */
    private BigDecimal latitude;

    /**
     * 店铺星级
     */
    private String star;

    /**
     * 评分
     */
    private BigDecimal score;

}

定义商店店铺测试数据的文本文件,通过读文件的方式放到内存中。

0f8207fd52344b348584f82d7ffef389 淮南牛肉汤 15.361239 20.115126 五星 9.7
c0304660e5be494eaff45ce26fcb9bf9 华莱士.鸡肉汉堡 13.361239 21.115126 四星 8.9
a998f4386fa34e16ba9ccf3f448bab7b 驴肉火烧 18.361239 24.115126 五星 9.5
76e50c6b464740bc888a226687961d0a 谷香煎饼 29.361239 24.115126 五星 9.0
1e84ace9b8c6492db416d6abd982e60d 老王鲜肉饼.砂锅 52.361239 40.115126 五星 9.0
3c9557c45a9f4e51ac3bd3ac39052622 麦多馅饼 51.361239 42.115126 三星 7.8
4a5771f48a4f4c61ba1c0b992989af86 张亮麻辣烫 78.361239 67.115126 五星 9.4
6c1b322c2f2546f4a286f745b3b800c3 农家大烤盘饭 80.361239 -67.115126 五星 9.0
2d577e6196414148a7809a469ded51c0 沙野轻食 -80.361239 -67.115126 三星 7.6
588fa28618b147fa87a904a541e7833b 卷饼王.炸串 70.361239 67.115126 五星 9.6
8247ba41fc2942f5b29e91cd42a7b422 凉皮先生.肉夹馍 29.361239 80.115126 五星 9.6
00de5559ecfc4c419b4e6adef8bffee6 火炉火韩式拌饭 12.361239 10.115126 五星 9.9
29d18fc219ed4ad09bdaf2fe806f796f 南城.黄焖鸡米饭 72.361239 50.115126 三星 7.9
770c8f0bbbb44f259d58d1e5b350fbd4 李大姐水饺 52.361239 42.115126 三星 7.9
b3d5dd8773e6475b9bb16ad25f876afb 田老师烤肉 52.469669 42.225196 三星 7.4
d112f7be99c24142b422633cdf15461b 老家炒饼 52.362239 42.145126 四星 8.4
8d59cae232da485d9cb77f6c6060c929 地摊烤冷面 52.398239 42.416526 四星 8.7
5ade01f108ba4ba884a8ec1d37bdf9bb 卤汁拌饭 83.361239 68.115126 四星 8.0
96f462d9a20f40419f18a0f4936ad099 人民公社大饭菜 20.361239 10.115126 四星 8.8
6b0b5955c6b8444ca49a5a2ba39ab49b 炸串王铁板烧 34.361239 20.115126 五星 9.8

我们定义两个方法,一个是获取列表,一个是根据ID获取商户信息。

/**
 * @author lixiang
 * @date 2023/6/21 09:53
 */
public class ShopData {

    public final static String SHOP_KEY = "shop:location";

    private final static List<Shop> SHOP_LIST;

    static {
        SHOP_LIST = new ArrayList<>();
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader("/Users/mac/IdeaProjects/spring-redis-demo/src/main/resources/shop.txt"));
            String line;
            do{
                line = reader.readLine();
                if (!StringUtils.isEmpty(line)){
                    String[] split = line.split(" ");
                    Shop shop = new Shop();
                    shop.setId(split[0]);
                    shop.setName(split[1]);
                    shop.setAccuracy(new BigDecimal(split[2]));
                    shop.setLatitude(new BigDecimal(split[3]));
                    shop.setStar(split[4]);
                    shop.setScore(new BigDecimal(split[5]));
                    SHOP_LIST.add(shop);
                }
            }while (line != null);
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据列表
     * @return
     */
    public static List<Shop> getData(){
        return SHOP_LIST;
    }

    /**
     * 获取数据map结构,根据ID获取商家信息
     * @return
     */
    public static Map<String,Shop> getDataMap(){
        return SHOP_LIST.stream().collect(Collectors.toMap(Shop::getId,obj->obj));
    }
}

测试数据是否进入到集合中。

    public static void main(String[] args) {
        List<Shop> data = ShopData.getData();
        for (Shop datum : data) {
            System.out.println(datum);
        }
    }

在这里插入图片描述

ok,没有问题。接下来我们开始写一个接口用于将地理位置信息同步到Redis中。封装GEO的操作组件。

/**
 * @author lixiang
 * @date 2023/6/21 11:32
 */
@Component
public class GeoComponent {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 添加成员
     * @param key
     * @param lon 经度
     * @param lat 纬度
     * @param member 成员
     * @return
     */
    public Long geoAdd(String key, double lon, double lat, String member){
        return redisTemplate.opsForGeo().add(key, new Point(lon, lat), member);
    }

    /**
     * 获取两个成员的距离
     * @param key
     * @param member1
     * @param member2
     * @return
     */
    public Distance geoDist(String key, String member1, String member2){
        return redisTemplate.opsForGeo().distance(key, member1, member2);
    }

    /**
     * 获取两个成员的距离
     * @param key
     * @param member1
     * @param member2
     * @param metric 度规(枚举)(km、m)
     * @return
     */
    public Distance geoDist(String key, String member1, String member2, Metrics metric){
        return redisTemplate.opsForGeo().distance(key, member1, member2, metric);
    }

    /**
     * 获取成员经纬度
     * @param key
     * @param members
     * @return
     */
    public List<Point> geoPos(String key, String... members){
        return redisTemplate.opsForGeo().position(key, members);
    }

    /**
     * 获取某个成员附近(距离范围内)的成员
     * @param key
     * @param member 成员
     * @param v 距离
     * @param metric  度规(枚举)(km、m)
     * @return
     */
    public List<String> geoRadiusByMember(String key, String member, double v, Metrics metric){
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(key, member, new Distance(v, metric));
        List<String> result = new ArrayList<>();
        for(GeoResult<RedisGeoCommands.GeoLocation<String>> geoResult :geoResults.getContent()){
            result.add(geoResult.getContent().getName());
        }
        return result;
    }

    /**
     * 获取某个成员附近(距离范围内)的成员
     * @param key
     * @param member 成员
     * @param v 距离
     * @param metric  度规(枚举)(km、m)
     * @param args
     * 示例:RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance().limit(1).sortAscending();
     * includeCoordinates:结果包含坐标,includeDistance:结果包含距离,limit:返回数量:sort...:排序
     * @return GeoResults
     * geoResult.getContent().getName() 元素名称
     * geoResult.getContent().getPoint() 元素坐标
     * geoResult.getDistance() 元素距离
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusByMember(String key, String member, double v, Metrics metric, RedisGeoCommands.GeoRadiusCommandArgs args){
        return redisTemplate.opsForGeo().radius(key, member, new Distance(v, metric), args);
    }
}

定义ShopService-syncShopLocationData()方法

		@Override
    public void syncShopLocationData() {
        List<Shop> data = ShopData.getData();
        //地理位置信息同步到Redis
        data.forEach(obj->{
            double accuracy = obj.getAccuracy().doubleValue();
            double latitude = obj.getLatitude().doubleValue();
            String id = obj.getId();
            geoComponent.geoAdd(SHOP_KEY,accuracy,latitude,id);
        });
    }
    @Autowired
    private ShopService shopService;
    private final static String SHOP_KEY = "shop:location";
    @GetMapping("/syncShopLocationToRedis")
    public void syncShopLocationToRedis(){
        shopService.syncShopLocationData();
    }

测试调用,数据已经写到Redis中。

在这里插入图片描述

ok,数据已经存储进来。接下来我们来实现一下按照用户的距离,搜索用户附近10km以内的商家,并按照着距离从小到大的进行排列商家的列表。

这里我们先在Service中定义一个根据ID查找商户的方法。有了我们之前的ShopData提供的方法,这里编写就很简单。

		@Override
    public Shop getShopById(String id) {
        return ShopData.getDataMap().get(id);
    }

定义controller方法,这里模拟里一个用户信息,给他一个经纬度,去查找附近的门店。

    @GetMapping("/getShopListByLocation")
    public List<ShopVO> getShopListByLocation(){
        //模拟用户信息
        Map<String,Object> user = new HashMap<>();
        user.put("accuracy",70.361239);
        user.put("latitude",67.115126);
        user.put("name","李祥");
        user.put("id", UUID.randomUUID().toString().replace("-",""));
        List<ShopVO> shopVO = shopService.getShopListByLocation(user);
        return shopVO;
    }

定义service层里面的逻辑。

		@Override
    public List<ShopVO> getShopListByLocation(Map<String, Object> user) {

        List<ShopVO> shopVOS = new ArrayList<>();

        // 获取用户的坐标位置
        double accuracy = (double) user.get("accuracy");
        double latitude = (double) user.get("latitude");
        String userId = String.valueOf(user.get("id"));

        // 将用户位置加入到Redis
        geoComponent.geoAdd(ShopData.SHOP_KEY, accuracy, latitude, userId);
        // 获取用户附近的门店
        List<String> shopIds = geoComponent.geoRadiusByMember(ShopData.SHOP_KEY, userId, 10, Metrics.KILOMETERS);
        for (String shopId : shopIds) {
            //如果是当前userId则直接跳出
            if (shopId.equals(userId)) {
                continue;
            }
            //获取shop信息
            Shop shop = this.getShopById(shopId);
            ShopVO shopVO = new ShopVO();
            BeanUtils.copyProperties(shop, shopVO);
            //获取两点的距离
            double distance = geoComponent.geoDist(ShopData.SHOP_KEY, userId, shopId, Metrics.KILOMETERS).getValue();
            //保留一位小数
            distance = new BigDecimal(distance).setScale(1, BigDecimal.ROUND_DOWN).doubleValue();
            shopVO.setDistance(distance);
            shopVOS.add(shopVO);
        }
        // 删除Redis中用户位置。
        geoComponent.geoDelete(ShopData.SHOP_KEY,userId);
        //排序 返回
        return shopVOS.stream().sorted(Comparator.comparingDouble(ShopVO::getDistance)).collect(Collectors.toList());    
    }

测试验证:

在这里插入图片描述

OK,那么关于Redis的GEO数据结构的实操,我们就讲到这里啦。觉得博主写的不错的记得给个赞哦!
在这里插入图片描述

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

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

相关文章

RSA算法

什么是RSA算法&#xff1f; 1976年以前&#xff0c;所有的加密方法都是同一种模式&#xff1a; 甲方选择某一种加密规则&#xff0c;对信息进行加密&#xff1b; 乙方使用同一种规则&#xff0c;对信息进行解密。 由于加密和解密使用同样规则&#xff08;简称"密钥"&…

C语言/C++ 之 打飞机游戏

【项目简介】 1、设计思想&#xff1a;本项目主要是为了实现打飞机游戏&#xff0c;主要包括5个函数模块&#xff0c;和1个主函数框架。分别是chu_shi_hua();、you_cao_zuo&#xff1b;、wu_cao_zuo();、show()&#xff1b;、main();等。项目完成过程中主要运用了C/C中的输入输…

蛋黄肉粽子

蛋黄肉粽子 一、蛋黄肉粽子 作为中国的传统美食之一&#xff0c;蛋黄肉粽子是端午节期间必不可少的特色食品。 我在今年的端午节期间品尝了这种美食&#xff0c;不仅让我感受到了它的美味&#xff0c;也让我领略到了它背后的历史和文化底蕴。 一、蛋黄肉粽子 首先&#xff0c;就…

第三方库介绍——nanomsg(高性能通信库)

一、nanomsg介绍 NanoMsg是一个Socket的通讯库&#xff0c;使用C语言编写实现的&#xff0c;这样就可以适用于多种操作系统&#xff0c;而且几乎不需要什么依赖&#xff0c;可扩展并且能易于使用。Nanomsg提供了几种常见的通信模式 &#xff08; 也称为“可扩展性协议” &#…

C++ 第一弹入门基础

目录 目录 1.关键字 2.命名空间 3.标准IO输入输出 4.缺省参数 5.函数重载 6.引用 7.内联函数 1.关键字 c98的关键字一共有63个 在之后用的时候都会再次详细介绍 2.命名空间 2.1什么是命名空间&#xff0c;为什么要有他&#xff1f; 在c中为了避免相同名字的变量&am…

torchvision.utils.save_image()保存tensor显示图片异常问题解决

用torchvision.utils.save_image()保存图片时出现异常 有些像素点会显示为全黑&#xff08;灰度图&#xff09;&#xff0c;如下图所示&#xff0c;第一张和第三张图 刚开始以为是图像数据分布范围的问题&#xff0c;在保存之前输出图像tensor的最大max和最小min值&#xff0c;…

【Python 基础篇】Python 模块与包

文章目录 引言一、模块与包概述二、模块的导入和使用三、包的组织和导入四、实际应用场景五、总结 引言 在Python编程中&#xff0c;模块和包是组织和复用代码的重要工具。随着项目规模的增长&#xff0c;将代码按照功能模块化并组织成包&#xff0c;可以提高代码的可读性、可…

吴恩达ChatGPT《Building Systems with the ChatGPT API》笔记

1. 课程介绍 使用ChatGPT搭建端到端的LLM系统 本课程将演示使用ChatGPT API搭建一个端到端的客户服务辅助系统&#xff0c;其将多个调用链接到语言模型&#xff0c;根据前一个调用的输出来决定使用不同的指令&#xff0c;有时也可以从外部来源查找信息。 课程链接&#xff1a…

设计模式之桥接模式笔记

设计模式之桥接模式笔记 说明Bridge(桥接)目录桥接模式示例类图视频文件接口avi视频文件类rmvb视频文件类抽象的操作系统类windows类mac类测试类 说明 记录下学习设计模式-桥接模式的写法。JDK使用版本为1.8版本。 Bridge(桥接) 意图:将抽象部分与其实现部分分离&#xff0c…

用微分方程描述的连续时间滤波器举例

用微分方程描述的连续时间滤波器举例 在许多应用中&#xff0c;频率选择性滤波器是用线性常系数微分或差分方程描述的线性时不变系统来实现的。这有许多理由&#xff0c;例如很多具有滤波作用的物理系统都是由微分或差分方程表征的。这方面的一个很好的例子就是在后续将研究的汽…

消息中间件相关知识简介

一、消息中间件相关知识 1、概述 消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能&#xff0c;成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件&#xff0c;如老牌的ActiveMQ、RabbitMQ&am…

Python 基础(十五):模块

❤️ 博客主页&#xff1a;水滴技术 &#x1f338; 订阅专栏&#xff1a;Python 入门核心技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; 文章目录 一、什么是模块二、自定义模块三、引用其它模块3.1、使用 import 引用模块3.2、使用 f…

揭秘Java 8的新特性:Stream API的使用和实践

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1. 集合处理数据的弊端 2. Stream流式思想概述…

VN8911/VN8912(A)/VN8914/VN8910(A)上安装驱动

问题&#xff1a; 如何在 VN8911、VN8912(A) 或 VN8914 设备上安装驱动程序&#xff1f; 背景&#xff1a; VN8911、VN8912(A) 或 VN8914 设备 [下面概括为 VN8900 系列&#xff0c;不包括 VN8910(A)] 与 CANoe 一起使用时作为分布式实时系统运行 [参见下面的 CANoe 11.0 和 V…

vue插槽概念解释

官方文档 https://cn.vuejs.org/guide/introduction.html 什么是插槽 在 Vue.js 中&#xff0c;插槽&#xff08;Slots&#xff09;是一项非常有效的功能&#xff0c;它允许我们在组件中预留一个占位符&#xff0c;供父组件插入自定义的内容。 具体来说&#xff0c;当一个组件…

Linux和Shell:开源力量与命令行之美

目录 一、概述二、Linux的简单介绍三、Shell的简单介绍四、Linux和Shell的应用领域五、Shell编程结语&#xff1a; 一、概述 Linux和Shell是开源世界中不可或缺的两个重要组成部分。Linux作为一种自由和开放的操作系统&#xff0c;以其稳定性、安全性和可定制性而备受推崇。而S…

【ProNoC】Chap.1 ProNoC生成2x2的mesh型的4核片上网络系统;实现NoC的RTL设计

【ProNoC】Chap.1 ProNoC生成2x2的mesh型的4核片上网络系统&#xff1b;实现NoC的RTL设计 0. NoC多核片上网络生成器ProNoCProNoC的功能实现 1. 生成一个叫做Mor1kx SoC的单个Tile&#xff08;包含NI网络接口&#xff09;1.1 打开ProNoC用于生成Tile的GUI界面1.2 为Tile添加时钟…

Python基础篇(四):基本数据类型的学习和示例

Python基础篇(三)&#xff1a;基本语句的示例和说明 基本数据类型的学习和操作 1. 数值类型1.1 整数&#xff08;int&#xff09;1.2 浮点数&#xff08;float&#xff09;1.3 复数&#xff08;complex&#xff09; 2. 字符串类型2.1 字符串的定义2.2 转义字符2.3 多行字符串2…

Vue-几种插槽(slot)的使用

插槽(slot) 插槽在vue中是一种很常见的写法&#xff0c;让父组件可以向子组件指定位置插入html结构&#xff0c;也是一种组件间通信的方式 一共有三种分类&#xff1a;默认插槽、具名插槽、作用域插槽&#xff0c;下面一一根据案例改造说明 1 基本案例 首先编写一个基本的案…

12.1 物业管理项目中的新知识点

1.RESTful 2.雪花ID 3.mybatis的动态sql 很多时候需要实现多条件查询&#xff0c;手动判断拼接sql有些麻烦 mybatis提供了一个动态sql实现多条件查询的方法 3.1 if元素 使用if元素可以根据条件来包含或排除某个SQL片段 <select id"search" resultType"Hous…