百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Java使用Redis的GeoHash数据结构

mhr18 2025-03-13 18:07 25 浏览 0 评论

GeoHash介绍

GeoHash目前比较主流实现位置服务的技术,Geohash算法将经纬度二维数据编码为一个字符串,本质是一个将降维的过程。

举个简单的例子,在地球上为了表示一个地标点,人们通过经度和纬度的交叉点来确定,但是这个地标点的表示必须是二维的。用二维数据存储的情况下,如果搜索某个地标点A周边5公里的酒店,如果将每个点到A的距离计算一遍,计算量非常大。

简单优化如果把地球划分两块,先判断经纬度范围,就可以减少一半的计算量,以此类推,当划分块越小计算量就越少。

geohash

进一步优化如果把地球划分成很多区块(因为地球并不是正圆,所以划分的区块是东西窄南北宽的长方形),一个小块的所有经纬坐标都视为小块的中心点的坐标(编码为字符串方便索引),显然A点属于某个小区块内的点,搜索A点5公里酒店时,只需计算A点到周边8个小块中心点的距离,如果某个小块不满足,继续取不满足区块周边的小区块直到满足条件,将全部小区块放到集合中去重,然后取出所有符合区块内的所有酒店计算一遍距离,剔除不满足条件的酒店,这样一来就将复杂的二维降低到一维,达到快速搜索的目的。

GeoHash如何编码等过于详细的内容就不多说了,大家可以网上搜一下。

Redis的GeoHash底层存储

Redis实现GeoHash底层使用了SortedSet存储。

遇到的问题

当需要存储的元素比较多时,不能存储到一个Rediskey中,这样会因大Key的查询导致网络堵塞,但是对于Redis的GeoHash不会有这个问题,查询时会根据经纬度计算出来符合条件的元素返回,我就遇到了下面的报警,感觉是误报,既然报警就要解决,请继续往下看。

Redis报警

解决思路

对于大Key,首先想到的方法是进行拆分,如何拆分呢?

刚开始是想着根据经纬度拆分,rediskey的设计是key_lon_lat,经度和维度取整,做也勉强可以,仔细想想还是不太合适。既然有GeoHash了,为什么不用GeoHash计算出来的编码作为Rediskey呢?为了区分集群中其他类型的Rediskey,再加个前缀表示地理位置的就完全解决拆分问题了。

注意事项

  • rediskey设计:使用String.format("geohash_%s", geoHash.toBase32()),下图直观看下geohash的输出结果。下图直观看下geoHash.toBase32()输出结果:

  • 精度选择:如果geohash的位数是9位数的时候,大概为附近2米。如下图:

精度对比图

代码片断

import ch.hsr.geohash.GeoHash;
import ch.hsr.geohash.WGS84Point;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.kn.util.CoordinateUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.junit.Test;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Description: Redis GeoHash测试
 */
public class RedisGeoHashTest {
    @Getter
    @Setter
    @Data
    @AllArgsConstructor
    class H {
        private String id;
        private Double longitude;
        private Double latitude;
    }

    @Test
    public void redisGeoHash() {
        Jedis jedis = new Jedis("127.0.0.1");
        //确定精度
        int numberOfCharacters = 5;
        //Mock数据并完成数据存储
        List list = Lists.newArrayList(new H("1", 121.514523, 31.10110537)
                , new H("2", 121.624523, 31.20110537)
                , new H("3", 121.734523, 31.30110537));
        for (H h : list) {
            GeoHash geoHash = GeoHash.withCharacterPrecision(h.getLatitude(), h.getLongitude(), numberOfCharacters);
            String rediskey = String.format("geohash_%s", geoHash.toBase32());
            jedis.geoadd(rediskey, h.getLongitude(), h.getLatitude(), h.id);
            jedis.setex(String.format("h_%s", h.getId()), 86400, h.toString());
        }

        //优化写入:大批量数据写入可以用分批方式
        //Map memberCoordinateMap = Maps.newHashMap();
        //jedis.geoadd("rediskey",memberCoordinateMap);
        //jedis.mset("rediskey",String[])
    }

    /**
     * 搜索5千米内符合条件的数据
     */
    @Test
    public void queryList() {
        Jedis jedis = new Jedis("127.0.0.1");
        double radius = 5000;
        double latitude = 31.22416;
        double longitude = 121.366384;
        //计算坐标点所在区块编码,并获取符合条件的周边区块
        GeoHash geoHash = GeoHash.withCharacterPrecision(latitude, longitude, 5);
        Set pointSet = Sets.newHashSet();
        pointSet.addAll(Arrays.stream(geoHash.getAdjacent()).collect(Collectors.toSet()));
        pointSet.addAll(getAdjacent(geoHash, radius, longitude, latitude));
        System.out.println(pointSet.size());
        List list = Lists.newArrayList();
        for (GeoHash gh : pointSet) {
            list.addAll(jedis.georadius(String.format("geohash_%s", gh.toBase32()), longitude, latitude, radius, GeoUnit.M));
        }
        Set hList = list.stream().map(e -> e.getMemberByString()).collect(Collectors.toSet());
        //获取H集合
        List keys = hList.stream().map(e -> String.format("h_%s", e)).collect(Collectors.toList());
        List rtnList = jedis.mget(keys.toArray(new String[0]));
        rtnList.forEach(System.out::println);
    }

    /**
     * 返回指定经纬度到周边区块中心点小于指定距离的区块集合
     *
     * @param geoHash
     * @param radius
     * @param longitude
     * @param latitude
     * @return
     */
    public Set getAdjacent(GeoHash geoHash, double radius, double longitude, double latitude) {
        WGS84Point point = geoHash.getOriginatingPoint();
        double v = CoordinateUtil.calDisByLonAndLatSimple(longitude, latitude, point.getLongitude(), point.getLatitude());
        if (v < radius) {
            return Sets.newHashSet(geoHash);
        }
        Set set = Sets.newHashSet();
        GeoHash[] adjacent = geoHash.getAdjacent();
        for (GeoHash gh : adjacent) {
            set.addAll(getAdjacent(gh, radius, longitude, latitude));
        }
        return set;
    }
}

需要引入如下pom:

        
            redis.clients
            jedis
            3.3.0
         
			
            ch.hsr
            geohash
            1.4.0
        
        
            org.projectlombok
            lombok
            1.18.22
        

觉得有用就收藏分享,关注我更多有价值的文章会第一时间推荐给你!

相关推荐

使用 Docker 部署 Java 项目(通俗易懂)

前言:搜索镜像的网站(推荐):DockerDocs1、下载与配置Docker1.1docker下载(这里使用的是Ubuntu,Centos命令可能有不同)以下命令,默认不是root用户操作,...

Spring Boot 3.3.5 + CRaC:从冷启动到秒级响应的架构实践与踩坑实录

去年,我们团队负责的电商订单系统因扩容需求需在10分钟内启动200个Pod实例。当运维组按下扩容按钮时,传统SpringBoot应用的冷启动耗时(平均8.7秒)直接导致流量洪峰期出现30%的请求超时...

《github精选系列》——SpringBoot 全家桶

1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...

Nacos简介—1.Nacos使用简介

大纲1.Nacos的在服务注册中心+配置中心中的应用2.Nacos2.x最新版本下载与目录结构3.Nacos2.x的数据库存储与日志存储4.Nacos2.x服务端的startup.sh启动脚...

spring-ai ollama小试牛刀

序本文主要展示下spring-aiollama的使用示例pom.xml<dependency><groupId>org.springframework.ai<...

SpringCloud系列——10Spring Cloud Gateway网关

学习目标Gateway是什么?它有什么作用?Gateway中的断言使用Gateway中的过滤器使用Gateway中的路由使用第1章网关1.1网关的概念简单来说,网关就是一个网络连接到另外一个网络的...

Spring Boot 自动装配原理剖析

前言在这瞬息万变的技术领域,比了解技术的使用方法更重要的是了解其原理及应用背景。以往我们使用SpringMVC来构建一个项目需要很多基础操作:添加很多jar,配置web.xml,配置Spr...

疯了!Spring 再官宣惊天大漏洞

Spring官宣高危漏洞大家好,我是栈长。前几天爆出来的Spring漏洞,刚修复完又来?今天愚人节来了,这是和大家开玩笑吗?不是的,我也是猝不及防!这个玩笑也开的太大了!!你之前看到的这个漏洞已...

「架构师必备」基于SpringCloud的SaaS型微服务脚手架

简介基于SpringCloud(Hoxton.SR1)+SpringBoot(2.2.4.RELEASE)的SaaS型微服务脚手架,具备用户管理、资源权限管理、网关统一鉴权、Xss防跨站攻击、...

SpringCloud分布式框架&amp;分布式事务&amp;分布式锁

总结本文承接上一篇SpringCloud分布式框架实践之后,进一步实践分布式事务与分布式锁,其中分布式事务主要是基于Seata的AT模式进行强一致性,基于RocketMQ事务消息进行最终一致性,分布式...

SpringBoot全家桶:23篇博客加23个可运行项目让你对它了如指掌

SpringBoot现在已经成为Java开发领域的一颗璀璨明珠,它本身是包容万象的,可以跟各种技术集成。本项目对目前Web开发中常用的各个技术,通过和SpringBoot的集成,并且对各种技术通...

开发好物推荐12之分布式锁redisson-sb

前言springboot开发现在基本都是分布式环境,分布式环境下分布式锁的使用必不可少,主流分布式锁主要包括数据库锁,redis锁,还有zookepper实现的分布式锁,其中最实用的还是Redis分...

拥抱Kubernetes,再见了Spring Cloud

相信很多开发者在熟悉微服务工作后,才发现:以为用SpringCloud已经成功打造了微服务架构帝国,殊不知引入了k8s后,却和CloudNative的生态发展脱轨。从2013年的...

Zabbix/J监控框架和Spring框架的整合方法

Zabbix/J是一个Java版本的系统监控框架,它可以完美地兼容于Zabbix监控系统,使得开发、运维等技术人员能够对整个业务系统的基础设施、应用软件/中间件和业务逻辑进行全方位的分层监控。Spri...

SpringBoot+JWT+Shiro+Mybatis实现Restful快速开发后端脚手架

作者:lywJee来源:cnblogs.com/lywJ/p/11252064.html一、背景前后端分离已经成为互联网项目开发标准,它会为以后的大型分布式架构打下基础。SpringBoot使编码配置...

取消回复欢迎 发表评论: