地理位置数据存储方案——Redis GEO

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 地理位置数据存储方案之redis-geo探索:基础介绍与源码解析。

一 题外话

   说起这个话题,就总会不由得想起刚毕业的时候,当时在导师的带领下,调研并使用了geo server和postgreSQL。geo server做图层和位置信息展示,而pg则用来存储地理位置数据。一转眼至今已有十年光景,真是让人感慨,十年,弹指一挥间。

二 GEO存储方案与空间索引

2.1 存储方案

   目前支持空间数据存储的方案很多,Esri公司的ArcSDE(Spatial Database Engine,空间数据库引擎),包括Oracle,SQL Server,IBM DB2都做了很好的支持,不过都是商业数据库,需要收费。开源领域,mysql、redis、elasticsearch、mongodb、postgreSQL等都做了相关支持。实现方案各自不同,使用上也有差异,简单理解,都是数据+索引结构组成的支撑,通过api来进行调用(废话)。

2.2 空间索引

   目前空间索引的实现有R树和其变种GIST树、四叉树、网格索引等。GeoHash也是空间索引的一种方式,并且特别适合点数据,而对线、面数据采用R树索引更有优势。

三 Redis GEO

3.1 命令

    Redis 3.2 版本新增了geo相关命令,用于存储和操作地理位置信息。提供的命令包括添加、计算位置之间距离、根据中心点坐标和距离范围来查询地理位置集合等,说明如下:

  • geoadd:添加地理位置的坐标。
  • geopos:获取地理位置的坐标。
  • geodist:计算两个位置之间的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:返回一个或多个位置对象的 geohash 值。

3.2 原理:redis源码解析

3.2.1 数据结构简述

   Redis geo并不是全新的数据结构,而是基于Sorted Set来实现的(这点我们会在后面进行说明)。说起sorted set,大家肯定了解zset,也是redis中常用的数据结构。

   我们看一下redis geo的源码,从中可以更好地理解数据结构和操作原理。redis源码可从https://github.com/redis/redis获取,我们切换到正在使用的3.2branch(也可以根据实际使用情况,切换到对应版本的分支)。3.2下的geo相关源码文件主要是src下的geo.h 和 geo.c,以及deps/geohash-int下的geohash.c,geohash.h,geohash_helper.h 和 geohash_helper.c。

3.2.2 geo.h

   geo.h是数据结构定义,里面包括了geoPoint 和 geoArray两个结构体,内容如下:

#ifndef __GEO_H__
#define __GEO_H__
#include "server.h"
/* Structures used inside geo.c in order to represent points and array of
 * points on the earth. */
typedef struct geoPoint {
    double longitude;
    double latitude;
    double dist;
    double score;
    char *member;
} geoPoint;
typedef struct geoArray {
    struct geoPoint *array;
    size_t buckets;
    size_t used;
} geoArray;
#endif

   可见geoPoint的字段包括 经度longitude、纬度latitude 这两个标识位置的基本字段,dist表示距离,member是成员(点)的名称/标识,以及score。score的含义是什么?聪明的小伙伴可能已经想到,应该是我们最开始提到的geohash值。其他的小伙伴不要着急,我们一起到geo.c中寻找答案。

3.2.3 geo.c

   geo.c是geo核心方法定义,内容不算很多,3.2版本的geo.c文件只有825行,所以阅读起来也并不复杂。

   这里定义了我们从redis客户端输入各redis命令的处理函数。geoaddCommand,georadiusCommand,geohashCommand,geoposCommand,geodistCommand等等,其中还有georadius的一系列包装函数,在void georadiusGeneric(client *c, int flags) 函数中定义了具体的处理逻辑:

   我们再详细看一下geoaddCommand(client *c)方法:

409-414行是校验逻辑,判断是否存在语法错误;

接下来是参数提取和处理。在419和420行,我们可以看到熟悉的命令:zadd;

接下来就更清晰了,注释中就已经明确写到:

   创建参数向量并调用zadd方法,来把所有的score,value队插入到zset中,这里score实际上是lat,long的编码版本。

什么编码?438-441行明确写出了答案,geohash(geohashEncodeWGS84,使用wgs84坐标系的geohash编码。wgs84坐标系即大地坐标系)。

3.3 操作实践

   上面我们分析了,redis geo虽然是通过geopos,geoadd等提供了操作命令,但底层实际上是基于zset来存储的,并且在geoadd命令中,也出现了转zadd操作的源码,那么我们是否可以直接使用zset的相关命令来操作redis geo的存储呢?

3.3.1 redis环境

   redis server版本3.2,本地单机部署,未设置密码。

3.3.2 命令行客户端连接

redis-cli -h mylocalhost  -p 8179 --raw

   注意:这里加上了--raw的参数。这是因为,当我们在redis中存储value包含中文时,如果不加上--raw,就会显示为unicode编码格式,如下:

   --raw参数,官网的解释中,包括以下两个作用:1.按数据原有格式打印数据,不展示额外的类型信息(例如整数value之前的 (integer) 2);2. 显示中文。

3.3.3 geo-zset操作验证

   先通过geoadd添加一条记录:

   geopos查看成员位置:

   重点来了,接下来我们通过zrange 来查询集合元素:

   显然是可以的。也就是说,user_local就是一个zset。

   接下来,我们看一下刚刚添加进来的test_1这个成员的score:

   score值为 4174690127103984,回顾之前我们看过的geoadd源码,也就是说test_1的经纬度,对应的geohash值就是4174690127103984。

四 springframework与redis geo

   springframework中已经加入了对redis geo的支持,相关的类都在org.springframework.data.geo包下。而对redis的命令行交互,也提供了org.springframework.data.redis相关的类来支持相关开发。

   为了在项目中方便使用,整理工具代码如下,主要封装了:

1、添加元素到redisgeo;

2、计算某指定集合下,给定中心和查询范围,获取区域内成员的方法;

3、计算两个成员的距离

4、查询某指定成员(数组)的位置信息

相关方法,如有需要可供参考:

package tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RedisGeoTool {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    /**
     * 添加节点及位置信息
     * @param geoKey 位置集合
     * @param pointName 位置点标识
     * @param longitude 经度
     * @param latitude 纬度
     */
    public void geoAdd(String geoKey, String pointName, double longitude, double latitude){
        Point point = new Point(longitude, latitude);
        redisTemplate.opsForGeo().add(geoKey, point, pointName);
    }
    /**
     *
     * @param longitude
     * @param latitude
     * @param radius
     * @param geoKey
     * @param metricUnit 距离单位,例如 Metrics.KILOMETERS
     * @param metricUnit
     * @return
     */
    public List<GeoResult<RedisGeoCommands.GeoLocation<String>>> findRadius(String geoKey
            , double longitude, double latitude, double radius, Metrics metricUnit, int limit){
        // 设置检索范围
        Point point = new Point(longitude, latitude);
        Circle circle = new Circle(point, new Distance(radius, metricUnit));
        // 定义返回结果参数,如果不指定默认只返回content即保存的member信息
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs().includeDistance().includeCoordinates()
                .sortAscending()
                .limit(limit);
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().radius(geoKey, circle, args);
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        return list;
    }
    /**
     * 计算指定key下两个成员点之间的距离
     * @param geoKey
     * @param member1
     * @param member2
     * @param unit 单位
     * @return
     */
    public Distance calDistance(String geoKey, String member1, String member2
            , RedisGeoCommands.DistanceUnit unit){
        Distance distance = redisTemplate.opsForGeo()
                .distance(geoKey, member1, member2, unit);
        return distance;
    }
    /**
     * 根据成员点名称查询位置信息
     * @param geoKey geo key
     * @param members 名称数组
     * @return
     */
    public List<Point> geoPosition(String geoKey, String[] members){
        List<Point> points = redisTemplate.opsForGeo().position(geoKey, members);
        return points;
    }
}

五 实战思路

   基于上述理解和代码,我们可以实现一些简单的demo了。也可以基于此实现一个基于某中心点查询周围商铺之类的功能,但要应用到实战当中还远远不够。在真实的系统中,还需要考虑以下几个问题:

1、redis作为缓存还是数据库使用?

2、redis geo中存储的信息是否完整?是否还需要存储其他辅助信息?

3、可能会有多类位置点,实际需求会要求根据类别查询?

4、当发生数据迁移时,怎样保证redis geo中的数据完整?最多支持存储多少个空间数据?

....

   一些比较容易想到的可能方案,比如结合其他持久化存储使用,做好一致性保障;member中包含id信息,用于查询明细信息;通过多个key对位置数据分类存储等等。但最终还需要根据实际需求,给出整套可行的方案,形成合理的架构设计,这样才能让我们做出的系统不再只是个demo,或者玩具。在后续的文章中,我们会继续进行探讨。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
|
3月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
25天前
|
存储 NoSQL PHP
PHP与Redis结合使用,提升数据存储性能
随着互联网应用的发展,PHP与Redis的结合成为提升数据存储性能的重要手段。PHP作为流行的服务器端语言,常用于网站开发;Redis作为高性能内存数据库,以其快速读写能力,有效优化数据访问速度,减轻数据库压力。两者结合通过缓存机制显著提升应用响应速度,支持高并发场景下的稳定性和可扩展性。
|
2月前
|
存储 NoSQL 定位技术
Redis GEO
10月更文挑战第19天
34 1
|
6月前
|
存储 NoSQL Redis
Redis系列学习文章分享---第九篇(Redis快速入门之好友关注--关注和取关 -共同关注 -Feed流实现方案分析 -推送到粉丝收件箱 -滚动分页查询)
Redis系列学习文章分享---第九篇(Redis快速入门之好友关注--关注和取关 -共同关注 -Feed流实现方案分析 -推送到粉丝收件箱 -滚动分页查询)
70 0
|
2月前
|
存储 NoSQL 大数据
大数据-51 Redis 高可用方案CAP-AP 主从复制 一主一从 全量和增量同步 哨兵模式 docker-compose测试
大数据-51 Redis 高可用方案CAP-AP 主从复制 一主一从 全量和增量同步 哨兵模式 docker-compose测试
35 3
|
2月前
|
存储 NoSQL Java
Spring Boot项目中使用Redis实现接口幂等性的方案
通过上述方法,可以有效地在Spring Boot项目中利用Redis实现接口幂等性,既保证了接口操作的安全性,又提高了系统的可靠性。
43 0
|
4月前
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
89 0
|
4月前
|
存储 JSON NoSQL
揭秘Redis字符串String的隐藏技能!从基础到进阶,让你的数据存储操作秒变高大上!
【8月更文挑战第24天】Redis中的字符串类型作为其基石,不仅能够存储从简单文本到复杂格式如JSON的各种数据,还能通过多样化的命令实现包括但不限于自增自减、设置过期时间等高级功能,极大提升了其实用性和灵活性。例如,使用`SET`命令可添加或更新键值对,`GET`获取值,`DEL`删除键;同时,`INCR`和`DECR`支持对整数值的原子性增减操作,非常适合实现计数器等功能;通过`EXPIRE`命令设置过期时间,则适用于需要限时存储的应用场景。尽管名为“字符串”,但实际上还可存储图片、音频文件的Base64编码等形式的数据,为开发者提供了强大而灵活的工具。
54 0