写点什么

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

  • 2022 年 6 月 25 日
  • 本文字数:4165 字

    阅读完需:约 14 分钟

地理位置数据存储方案——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;
@Componentpublic 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,或者玩具。在后续的文章中,我们会继续进行探讨。

发布于: 刚刚阅读数: 4
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
地理位置数据存储方案——Redis GEO_redis_程序员架构进阶_InfoQ写作社区