【工作记录】springboot中基于redis实现地理位置相关需求@20240822

news2024/9/28 3:19:24

背景

近期收到一个需求,有个事件管理系统,存储了用户上报上来的事件信息,其中包含了事件发生的经纬度,还有另外一个系统中保存了一系列的摄像头数据,也包含经纬度信息。

需求是这样的,用户点击某个事件的时候弹出一个详情对话框,用户可以点击对话框中的查看周边视频按钮来查看该事件发生地附近一公里以内的摄像头数据

这里就涉及到一个问题,如何在诸多摄像头中找到事件发生地周边一公里范围内的摄像头数据

分析实现方案

看到这个需求,第一个想法就是使用mysql或者redis这一类数据库工具提供的地理空间支持API来实现。以下是各个实现方案对比:

方案名称方案描述优点缺点适用场景
纯Java实现使用Haversine公式计算两个经纬度之间的距离,然后遍历所有经纬度数据,计算目标经纬度与每个数据点之间的距离,判断是否在指定范围内。****易于实现:只需要基本的Java编程知识。
无需额外依赖:不需要使用数据库或其他外部服务。
性能较低:需要对每个数据点进行计算,随着数据量增加,性能会显著下降。
不适合大数据集:对于大规模数据集,这种方法可能不可行。
数据量较小的情况。
不需要实时处理的情况
使用Redis GEO功能使用Redis的GEO功能,将经纬度数据存储在Redis中,并利用Redis提供的GEORADIUS命令来查询指定范围内的数据。高性能:Redis提供了非常快的查询速度。
易于扩展:Redis可以水平扩展,支持集群部署。
需要额外的Redis服务:需要部署和维护Redis服务。
学习成本:需要了解Redis的使用方法。
需要高性能查询的场景。
数据量较大,需要快速响应的情况
使用MySQL的GIS功能使用MySQL的GIS功能,将经纬度数据存储为POINT类型,并使用ST_Distance_Sphere等函数来查询指定范围内的数据。成熟稳定:MySQL是一个成熟的数据库系统,支持GIS功能。
易于集成:如果已经在使用MySQL,集成起来比较容易。
性能相对较低:相比Redis,MySQL的查询速度较慢。
需要额外配置:需要配置MySQL支持GIS功能。
已经在使用MySQL的项目。
数据量适中,对性能要求不是特别高的情况。
使用PostgreSQL的PostGIS扩展使用PostgreSQL的PostGIS扩展,将经纬度数据存储为地理坐标,并利用PostGIS提供的函数来查询指定范围内的数据。强大的GIS功能:PostGIS提供了丰富的GIS功能。
高性能:PostgreSQL是一个高性能的数据库系统。
学习曲线较高:PostGIS的学习曲线比其他方案更陡峭。
部署和维护成本:需要部署和维护PostgreSQL服务。
需要高级GIS功能的项目。
数据量较大,对性能和功能都有较高要求的情况。
使用Elasticsearch使用Elasticsearch的地理坐标支持,将经纬度数据索引到Elasticsearch中,并使用geo_distance查询来获取指定范围内的数据。高性能:Elasticsearch擅长处理大规模数据集。
易于扩展:Elasticsearch支持水平扩展。
部署和维护成本:需要部署和维护Elasticsearch服务。
学习成本:需要学习Elasticsearch的使用方法。
需要处理大规模数据集的项目。
对性能和可扩展性有较高要求的情况。

总结如下:

  • 纯Java实现适用于数据量较小且不需要实时处理的情况。
  • Redis GEO功能适用于需要高性能查询的场景,特别是数据量较大的情况。
  • MySQL GIS功能适用于已经在使用MySQL且数据量适中的项目。
  • PostgreSQL PostGIS扩展适用于需要高级GIS功能的项目,数据量较大且对性能和功能都有较高要求的情况。
  • Elasticsearch适用于需要处理大规模数据集且对性能和可扩展性有较高要求的情况。

根据你的具体需求和现有技术栈选择合适的方案。

本文我们选择基于RedisGeo实现该需求。

开始

环境准备

  1. Java开发环境和开发工具,如IDEA、MAVEN等
  2. 支持RedisGeo的redis环境

引入依赖

pom.xml文件参考如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zjtx.tech-demo</groupId>
    <artifactId>redis-geo-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.8</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>redis-geo-demo</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

添加配置

application.yml配置redis相关信息:

spring:
  redis:
    host: 172.16.10.204
    port: 6379
    database: 8

数据准备

摄像头信息实体类:

package com.zjtx.tech.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CameraInfo {

    private Integer id;

    private String name;

    private double longitude;

    private double latitude;

    private String address;

}

返回结果类:

package com.zjtx.tech.demo.utils;

import lombok.Data;

@Data
public class Result {

    private int code = 200;

    private String msg = "ok";

    private Object data;


    public Result() {

    }

    public static Result ok(Object data) {
        Result result = new Result();
        result.setData(data);
        return result;
    }

}

为了演示方便,这里使用java生成了一系列的经纬度点位(实际中应当是从某个数据存储中读取),代码参考如下:

package com.zjtx.tech.demo.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class GeoPointGenerator {

    // 地球平均半径,单位:千米
    private static final double EARTH_RADIUS_KM = 6371;
    private static final Random RANDOM = new Random();

    /**
     * 生成指定数量的经纬度信息,满足特定的距离条件。
     *
     * @param centerLongitude 中心点的经度
     * @param centerLatitude  中心点的纬度
     * @return 生成的经纬度列表
     */
    public static List<Double[]> generateGeoPoints(double centerLongitude, double centerLatitude) {
        List<Double[]> geoPoints = new ArrayList<>();
        // 不足1km, 不足2km, 不足3km, 超过3km 的数量
        int[] counts = {2, 3, 4, 3};
        // 距离上限
        double[] distancesKm = {1, 2, 3, 10};

        for (int i = 0; i < counts.length; i++) {
            for (int j = 0; j < counts[i]; j++) {
                double distanceKm = getRandomDistance(distancesKm[i]);
                double angle = getRandomAngle();
                Double[] point = getGeoPoint(centerLongitude, centerLatitude, distanceKm, angle);
                while (point[0] < 0 || point[1] < 0) {
                    // 如果生成的经纬度有负值,则重新生成
                    distanceKm = getRandomDistance(distancesKm[i]);
                    angle = getRandomAngle();
                    point = getGeoPoint(centerLongitude, centerLatitude, distanceKm, angle);
                }
                geoPoints.add(point);
            }
        }

        return geoPoints;
    }

    private static double getRandomDistance(double maxDistanceKm) {
        return RANDOM.nextDouble() * maxDistanceKm;
    }

    private static double getRandomAngle() {
        return RANDOM.nextDouble() * 360;
    }

    private static Double[] getGeoPoint(double centerLongitude, double centerLatitude, double distanceKm, double angleDegrees) {
        // 将角度转换为弧度
        double angle = Math.toRadians(angleDegrees);
        // 计算半径
        double radius = distanceKm / EARTH_RADIUS_KM;
        double lat1 = Math.toRadians(centerLatitude);
        double lon1 = Math.toRadians(centerLongitude);
        double lat2 = Math.asin(Math.sin(lat1) * Math.cos(radius) + Math.cos(lat1) * Math.sin(radius) * Math.cos(angle));
        double lon2 = lon1 + Math.atan2(Math.sin(angle) * Math.sin(radius) * Math.cos(lat1), Math.cos(radius) - Math.sin(lat1) * Math.sin(lat2));
        return new Double[]{Math.toDegrees(lon2), Math.toDegrees(lat2)};
    }

}

编写service类

package com.zjtx.tech.demo.service;

import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.domain.geo.Metrics;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class RedisGeoService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 向指定的Geo集合中添加成员,附带其地理坐标
     *
     * @param key       Geo集合的键,标识一个Geo集合
     * @param member    需要添加的成员
     * @param longitude 经度,表示成员的地理坐标位置
     * @param latitude  纬度,表示成员的地理坐标位置
     */
    public void addMember(String key, String member, double longitude, double latitude) {
        redisTemplate.opsForGeo().add(key, new Point(longitude, latitude), member);
    }

    /**
     * 根据指定的圆范围查询Geo集合中的成员
     * 此方法用于查询给定Geo集合中,在指定圆范围内的所有成员,圆的中心由经纬度指定,半径为5公里
     *
     * @param key       Geo集合的键,标识一个Geo集合
     * @param longitude 圆心的经度
     * @param latitude  圆心的纬度
     */
    public void radius(String key, double longitude, double latitude, int radius) {
        Point center = new Point(longitude, latitude);
        Circle circle = new Circle(center, new Distance(radius, Metrics.KILOMETERS));
        redisTemplate.opsForGeo().radius(key, circle);
    }

    /**
     * 根据指定的圆范围查询Geo集合中的成员,并按距离排序
     * <p>
     * 此方法用于查询给定Geo集合中,在指定圆范围内的所有成员,圆的中心由经纬度指定,半径为5公里。
     * 查询结果将按照距离从近到远排序
     *
     * @param key       Geo集合的键,标识一个Geo集合
     * @param longitude 圆心的经度
     * @param latitude  圆心的纬度
     * @return 按距离排序的查询结果
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> radiusCondition(String key, double longitude, double latitude, int radius, long count) {
        Point center = new Point(longitude, latitude);
        Circle circle = new Circle(center, new Distance(radius, Metrics.KILOMETERS));
        //这里实现了根据距离升序排序并且限制返回数量的功能,可根据实际情况灵活调整
        RedisGeoCommands.GeoRadiusCommandArgs args =
                RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                        .includeDistance().includeCoordinates().sortAscending().limit(count);
        return redisTemplate.opsForGeo().radius(key, circle, args);
    }

}

附上redis原生支持的命令:

命令命令说明参数说明使用示例
GEOADD用于向一个已存在的 Geo 集合键中添加一个或多个带有地理坐标的成员。GEOADD key longitude latitude member [longitude latitude member …]GEOADD geo:locations 116.3975 39.9042 Beijing 121.4737 31.2304 Shanghai
GEOPOS获取一个或多个给定成员的位置(经度和纬度)。GEOPOS key member [member …]GEOPOS geo:locations Beijing
GEODIST计算两个给定成员之间的距离。GEODIST key member1 member2 unitGEODIST geo:locations Beijing Shanghai km
GEOHASH返回一个或多个给定成员的 Geo Hash 表示。GEOHASH key member [member …]GEOHASH geo:locations Beijing
GEORADIUS以给定的经纬度为中心,返回键中指定区域内满足给定的最大距离的所有成员。GEODIST key member1 member2 unitGEORADIUS geo:locations 116.3975 39.9042 100 km WITHDIST WITHCOORD
GEORADIUSBYMEMBER与 GEORADIUS 类似,但是是以给定的成员为中心,而不是经纬度。GEORADIUSBYMEMBER key member radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC DESC] [COUNT count] [STORE key] [STOREDIST key]GEORADIUSBYMEMBER geo:locations Beijing 100 km WITHDIST WITHCOORD

这些命令在 Redis 3.2 版本开始被正式引入到 Redis 中,提供了强大的地理空间数据存储和检索功能。

编写controller类

package com.zjtx.tech.demo.controller;

import com.zjtx.tech.demo.entity.CameraInfo;
import com.zjtx.tech.demo.service.RedisGeoService;
import com.zjtx.tech.demo.utils.GeoPointGenerator;
import com.zjtx.tech.demo.utils.Result;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("camera")
public class RedisGeoDemoController {

    @Resource
    private RedisGeoService redisGeoService;

    public static final List<CameraInfo> CAMERA_INFO_LIST = new ArrayList<>();

    static {
        double centerLongitude = 116.3975;
        double centerLatitude = 39.9042;
        List<Double[]> geoPoints = GeoPointGenerator.generateGeoPoints(centerLongitude, centerLatitude);
        for (int i = 0; i < geoPoints.size(); i++) {
            CameraInfo cameraInfo = new CameraInfo();
            cameraInfo.setId(i);
            cameraInfo.setName("camera" + i);
            cameraInfo.setLongitude(geoPoints.get(i)[0]);
            cameraInfo.setLatitude(geoPoints.get(i)[1]);
            cameraInfo.setAddress("address" + i);
            CAMERA_INFO_LIST.add(cameraInfo);
        }
    }

    @GetMapping("list")
    public Result getCameraList(double longitude, double latitude) {
        CAMERA_INFO_LIST.forEach(cameraInfo -> {
            redisGeoService.addMember("cameras", cameraInfo.getName(), cameraInfo.getLongitude(), cameraInfo.getLatitude());
        });
        //查询传入的经纬度附近1公里以内的3个摄像头数据
        GeoResults<RedisGeoCommands.GeoLocation<String>> res = redisGeoService.radiusCondition("cameras", longitude, latitude, 1, 3);
        res.getContent().forEach(result -> System.out.println(result.getContent().getName()));
        return Result.ok(res);
    }

}

测试

启动项目并通过浏览器访问接口测试地址
在这里插入图片描述

总结

本文依赖于redis对geo的支持相关API,结合springboot实现了指定范围内经纬度的搜索。

示例相对简单,如有更复杂需求可在此基础上实现,有任何问题欢迎留言讨论。

除了文中提到的方案外还有其他方案,如有需要欢迎留言交流。

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

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

相关文章

OpenSBI的PMP

概述 在RISC-V体系架构中&#xff0c;PMP是用于保护物理内存访问权限的机制。PMP机制允许为不同的物理内存区域指定访问权限&#xff08;读、写、执行&#xff09;。这种机制使得运行在RISC-V处理器上的软件只能访问被明确授权的物理地址空间&#xff0c;从而提高了系统的安全…

5.登录功能的开发

登录功能的开发 一、前端1.1首页跳转到登录页面1.2登录界面处理 二、后端2.1创建User实体类2.2创建UserDto类2.3创建UserServlet类 三、效果演示四、轻提示组件 一、前端 1.1首页跳转到登录页面 登录页面是一个单独的页面&#xff0c;我们需要从项目的首页跳转到登录页面。具…

OpenAI Whisper Cannot Import Numpy

题意&#xff1a;“OpenAI Whisper 无法导入 Numpy” 问题背景&#xff1a; I am trying to run the OpenAI Whisper model but running into the following error when trying to run my script: “我正在尝试运行 OpenAI Whisper 模型&#xff0c;但在运行脚本时遇到了以下…

【全网行为管理解决方案】上网行为系统有哪些?

全网行为管理系统是一种用于监控、管理和优化企业内部网络中所有用户活动及网络流量的技术解决方案。 这类系统可以帮助企业提高网络安全、优化网络性能&#xff0c;并确保网络使用符合公司政策及法规要求。以下是几种常用的上网行为管理系统&#xff1a; 一、安企神 特点&am…

防范小程序隐私合规风险,筑牢用户信任防线

随着国内APP软件生态的成熟&#xff0c;依托于头部APP的小程序逐渐成为零售、娱乐、出行等行业必选的获客渠道之一。较低的开发成本和成熟的用户营销功能&#xff0c;令小程序的数量在过去几年呈指数级增长。截止2023年&#xff0c;头部APP内集成的小程序总量已超千万。然而&am…

【业余玩儿AI】【文档问答】实操记录0822

电梯 前文回顾继续踩坑实录从头来过,docker部署Ollama我一定是被偷听了,大数据之神把我拉出泥潭时间回到白天,模型初窥时间回到开心的链接上Ollama那一刻 按捺住乱撞的小鹿,我去准备下文的剧情了 前文回顾 这里书接上文,上文说到: 目标是文档问答,先是本地部署了Marker,然后又…

计算机二级题--指针 章节

1.概念 1.函数名代表函数的入口地址 2.交换地址 1.*s,说明s是一个指针变量 2.s&k;说明让s指向k地址 3.所以*sk实际上与上面那句是等价的,因此m一直都没有什么变化依然是3 4.k是全局变量所以是5 3&#xff0c;7&#xff1b;改变s指向之后&#xff0c;又将值赋给了s指向的…

WS2812B硬件电路设计总结

一、WS2812b的电压是多少&#xff1f; WS2812B的电压通常在3.5到5.3V之间。 WS2812B是一种流行的可编程LED&#xff0c;也称为NeoPixel。它集成了RGB LED和控制电路&#xff0c;可以通过单个数据线进行串联连接。这种LED的输入电源电压范围为3.5到5.3V&#xff0c;这意味着它…

WPF中的XAML是如何转换成对象的?

起因 最近有遇到有小伙伴在实现TreeView不同层级使用不同数据模板时&#xff0c;遇到了一些问题。 经过查阅资料&#xff0c;我提供了两种解决方案。 第一种是使用TemplateSelector&#xff0c;这种方式可以根据ViewModel设置不同的数据模板。 第二种是根据数据动态创建数据…

中兴 随身WIFI 5产品参数

产品参数 无线参数无线速率2.4GHz, 300Mbps天线类型内置Wi-Fi天线软件功能手机App中兴ZTE Link APP Pro更多功能移动网络&#xff08;4G/3G&#xff09;接入、Wi-Fi接入、Wi-Fi加密认证、WebUI、PIN保护、FOTA升级等硬件规格接口Micro USB/标准SIM卡&#xff08;2FF&#xff0…

软件测试 缺陷报告处理流程

系统软件 操作系统 软件缺陷 缺陷报告 当测试人员发现了一个缺陷&#xff0c;需要填写一份 缺陷报告 来记录这个缺陷&#xff0c;并通过这个缺陷报告告知开发人员所发生的问题————缺陷报告是测试人员和开发人员交流沟通的重要工具。 缺陷报告的组成 1、缺陷ID 缺陷编号&…

JuiceFS 在多云架构中加速大模型推理

在大模型的开发与应用中&#xff0c;数据预处理、模型开发、训练和推理构成四个关键环节。本文将重点探讨推理环节。在之前的博客中&#xff0c;社区用户 BentoML 和贝壳的案例提到了使用 JuiceFS 社区版来提高模型加载的效率。本文将结合我们的实际经验&#xff0c;详细介绍企…

Linux——网络(2)

一、通信 --- 不同主机上进程间的通信 1、IP和端口号 IP&#xff1a;标识网络中的一台主机 本质上 32位的整型数据 端口号: 标识某个进程 本质上 16位的整型数据 2、udp和tcp udp的特点: 1.无连接 2.不可靠 tcp的特点&#xff1a; 1.面…

【赵渝强老师】执行Oracle的冷备份与冷恢复

冷备份与冷恢复是指发生在数据库已经正常关闭的情况下进行的备份和恢复。由于此时数据库已经关闭&#xff0c;通过冷备份可以将数据库的关键性文件拷贝到另外存储位置。冷备份因为只是拷贝文件&#xff0c;因此备份的速度非常快。在执行恢复时&#xff0c;只需将文件再拷贝回去…

命令模式:如何利用命令模式实现手游后端架构?

成长路上不孤单&#x1f60a;【14后boy&#xff0c;C爱好者&#xff0c;持续分享所学&#xff0c;如有需要欢迎收藏转发&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#xff01;&#xff01;&#xff01;接上篇博文&#xf…

拍抖音在哪里去水印,三招教你快速掌握去水印技巧

在抖音上&#xff0c;我们经常会看到一些精彩的内容&#xff0c;想要保存下来&#xff0c;但往往视频上会有水印。本文将分享五个免费且高效的去除抖音视频水印的技巧&#xff0c;帮助你轻松保存无水印的视频。 技巧一&#xff1a;奈斯水印助手(小程序) 奈斯水印助手是一款专…

为技术博客添加评论功能:Gitalk 教程与实战

为技术博客添加评论功能&#xff1a;Gitalk 教程与实战 简介安装使用创建 Github Application方式1方式2 主页传送门&#xff1a;&#x1f4c0; 传送 简介 Gitalk是一个基于 GitHub Issue 和 Preact 开发的评论插件。   Gitalk是一个现代、无后端、基于GitHub Issue的评论系…

基于单片机的程控电源显示控制电路设计

摘要 : 介绍了基于单片机程控电源显示控制电路的硬件设计和软件实现 &#xff0c; 该设计可以实现程控电源的输出显示和手动控制功能。 实践验证 &#xff0c; 该设计具有很好的使用效果和工程价值 。 关键词 : 程控电源 ; 显示控制 ; 单片机 0 引言 程控电源广泛地应用在…

python怎么写乘法表

代码如下&#xff1a; 代码详解&#xff08;为了让自己理解&#xff09;&#xff1a; for i in range(1,10):# print(i,end )for j in range(1,i1):print(%s*%s%s %(i,j,i*j),end )print() 1. for i in range(1,10) 这是一个for循环语句&#xff0c;range&#xff08;&…

无线数传模块是啥东西?

一 、 产品概述 无线数传模块是用来替代传统数据采集、通讯、控制布线的占用工业级模块。 无线数传模块一款工作在免费频段、5000m传输距离模块发射功率158mW、具有高稳定性、低功耗、高性价比、工业级特点。 模块具有多种传输距离规格可供选择&#xff0c;根据应用场景需要&am…