GEO 存储方案与空间索引1 存储方案

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

2 空间索引

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

Redis GEO1 命令

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

2 原理:redis 源码解析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。

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 中寻找答案。

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) 函数中定义了具体的处理逻辑:

redis的数据存在哪(GEO地理位置数据存储方案)(1)

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

redis的数据存在哪(GEO地理位置数据存储方案)(2)

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

redis的数据存在哪(GEO地理位置数据存储方案)(3)

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

redis的数据存在哪(GEO地理位置数据存储方案)(4)

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

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

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

3 操作实践

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

3.1 redis 环境

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

3.2 命令行客户端连接

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

复制代码

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

redis的数据存在哪(GEO地理位置数据存储方案)(5)

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

3.3 geo-zset 操作验证

先通过 geoadd 添加一条记录:

redis的数据存在哪(GEO地理位置数据存储方案)(6)

geopos 查看成员位置:

redis的数据存在哪(GEO地理位置数据存储方案)(7)

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

redis的数据存在哪(GEO地理位置数据存储方案)(8)

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

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

redis的数据存在哪(GEO地理位置数据存储方案)(9)

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,或者玩具。在后续的文章中,我们会继续进行探讨。

作者:程序员架构进阶

原文出处:https://xie.infoq.cn/article/1a064c78c3f793a53bc7f84e6

,