Redis For Annotation方案

背景

GIS现有主流的矢量请求方式,一般有WFS、WMS、WMTS的方式来支持从服务器提供数据,返回到客户端渲染。
WFS提供单个矢量对象,然后由客户端提供机制进行规则限制和渲染处理;
WMS、WMTS由服务器渲染好一张绘制了矢量的瓦片,返回到客户端呈现,所以主要限制规则由服务器完成;

对于大数据量的矢量加载与渲染,到底用什么方式持久化,持久化如何支持建树空间索引,数据请求规则以及渲染机制由客户端还是服务器负责,这些问题都是需要解决的。

Annotation矢量数据

GIS对于矢量的定义如下:

矢量数据是在直角坐标中,用x、y坐标表示地图图形或地理实体的位置和形状的数据。矢量数据一般通过记录坐标的方式来尽可能地将地理实体的空间位置表现得准确无误。

即对于矢量来说,在地图上要准确无误地表现其地理位置,然而,在实际的矢量应用,包括渲染部分的应用中,要求矢量不仅仅是关于地理位置的描述,也需要包含样式描述,如果对象化的矢量,也需要存入Property的属性信息以保证扩展性。

这里可以将其统称为AnnotationFeature:
在这里插入图片描述
为了能够持久化这样的一个矢量对象,就要用相应的数据结构将其描述,而这个记录又不仅仅是单纯的写入,同样也需要考虑读写的效率问题,因为在地图上实时渲染的请求频率几乎是毫秒级别的。那么,既要保证读写信息的完整,又要保证可接收的请求速度,就必须要求存在数据和空间索引的“引擎”机制
特别是面临真正的大数据时,对“引擎”的要求就更高。

关于流程

在介绍Redis方案之前,简单讲讲使用Sqlite作为持久化提供数据索引,图层用RTree作为空间索引的“引擎”,如何加载矢量及其渲染。
Sqlite作为轻量级的文件型数据库,优势当然是相对于MySql来说方便部署。在这里,我们用Sqlite组织矢量的对象化关系,来还原上面图上的结构信息。

数据保存

使用Sqlite的持久化方式:
假设有定义一个AnnotationFeature表,内容大致如下:
在这里插入图片描述
这样就可以将数据的Save和Quey封装起来,具体实现调用Sqlite提供的方法。

矢量仓库

基于上面的延申,可以提出矢量仓库的概念,无论是持久化还是内存方式,只需将具体保存数据的方式封装起来,对外提出通用的接口。可以理解数据仓库就是保存矢量数据,以及根据请求需要(比如ID索引),返回矢量数据。
关键的接口如下(C++形式):
在这里插入图片描述
即,矢量仓库根据提供的路径加载数据,保存入具体实现的存储方式中(内存/Sqlite持久化/…),当外部调用【QueryAnnotationFeature】时,返回矢量对象数据;

图层

相对于矢量仓库的“外部调用”,一般就是矢量图层,可以提供一种与渲染有关的“引擎”机制,它获取到数据后,进行具体的矢量数据渲染到地图绘制。
即,图层关联一个矢量仓库后,在仓库Load完成后(数据准备完成),就可以根据当前屏幕范围内的地图区域,请求范围内矢量,获取当前需要渲染的矢量数据ID号,然后从仓库中对应ID号,请求数据,然后通过数据到绘制的过程,呈现在屏幕内。
其中有一个关键的地方就是,如何知道屏幕内有哪些矢量ID?在这儿的答案就是RTree空间索引,矢量图层内置了一个RTree方式的空间索引,在仓库Load数据的过程回调中,也同时在根据一个ID对应的矢量包围盒,建立空间索引关联。因此在屏幕范围改变时,通过RTree获取当前应该显示的矢量集对应的ID集。

关系图如下:
在这里插入图片描述
图层除了和数据请求流程相关的,还有两个大点,一是矢量渲染机制,二是数据加载策略
矢量渲染机制就包括了聚合的方式,级别显隐的限制,文字级别的限制等,目的就是让一个个矢量对象不全部被显示出来,而是在一个合适的机制下“适当”地呈现出。这个描述得就很微妙了,客观原因当然是一个30G的建筑数据,肯定不能全部对象化地画在地图上,但是又要在屏幕范围内显示一部分,如果拖到1级别的全球,屏幕范围内的区域就是【-180,180】【-90,90】了,数据一请求肯定又是全部,这时就必须要进行限制了,比如在10-18级别才加载数据呈现等。
数据的加载策略主要是针对于和矢量仓库打交道请求数据的情况,这里先按下不表,因为对于现在的图层来说,矢量仓库根据ID提供一个确定的矢量数据即可。


矢量仓库持久化选择

前面也介绍矢量仓库的目的就是提供数据,不管数据是存在内存中还是数据库中,甚至自定义索引文件中。那矢量仓库的能力就只是存储数据吗?那么如果要朝矢量服务的方向发展,还不具备基本的条件——空间索引的能力。首先WFS就是一个很好的参考,作为标准的GIS矢量服务,已经有很成熟的支持手段来提供矢量服务了。
说到这里,对外提供空间索引的体现,最重要的无非就一个接口——TravelExtent

现在整理新的矢量仓库接口,加入了具备空间索引能力的体现:
在这里插入图片描述
既然矢量仓库已经“分担”了图层的空间索引职责,那么是时候把刚才略过的数据加载策略介绍下了。
首先,对于新的矢量图层实现,就不需要持有类似RTree空间索引的方法了,即RTree请求全部换成调用矢量仓库的TravelExtent接口实现一样的功能,也不需要关心矢量仓库的Load回调过程了。但是,重要的地方在于,如何请求“适当”的数据,这又是一个微妙的描述,提供的方法是加入**“限制策略”**的机制,类似WFS等成熟的服务有配置文件的概念,矢量图层也可以通过读配置文件,确认加载范围内一次请求矢量的最大个数、一个矢量的BoundingBox限制规则等(QueryAnnotationExtent接口提供的必要性),这些可以通过增加图层的Set接口提供,也可以规定一个配置文件格式,由用户“填空”,然后图层加载后记录规则即可。

回到持久化选择,既然矢量仓库需要支持空间索引的能力,那么大可通过接口类,封装一个具体实现用WFS请求的方法。不过考虑到更轻量级的方法,Spatialite是不错的选择,可以认为是拥有空间索引能力的Sqlite数据库,为GIS量身定做的。
所以,要说这篇文章的主角:Redis有什么优势,暂时没有特别明显,不过是笔者执着地想试探一番Redis的空间索引能力,因为官方文档说从Version3.2开始,就支持了Geo相关的指令支持。


用Redis实现GIS矢量仓库

REmote DIctionary Server(Redis):key-value 存储系统,是跨平台的nosql的key-value非关系型数据库。

二进制序列化

所以,如果还像使用Sqlite的方式来存储矢量信息就玩不转了,或者说体现不出Redis的真实能力。
不过办法就是直接将矢量信息全部二进制序列化,封装一个可序列化和反序列化IAnnotationFeature矢量对象的方法类即可。
在这里插入图片描述

Redis调用指令

在这里插入图片描述

代码层面

使用C++封装了Redis的源码,提供方法类:CRedisHandler,提供连接、命令请求、返回值获取、事务支持等的接口。

具体内容详见:
《封装Redis使用的初步探索》https://blog.csdn.net/Being__/article/details/120033243

使用该接口在上层封装了针对矢量数据Binary的存取,以及空间索引存储和请求的矢量仓库,完成对Redis的调用。

注意:Redis服务器的实现更像是介于内存和文件型存储之间的中转,内存中的缓存需要在一定机制下触发之后才会进行持久化,或者发送持久化命令后持久化,否则会造成服务器内存撑爆。

写入矢量的Binary数据及其BoundingBox:

	// - 写入Binary
	int paramN = 4;
	std::string cmd = "HMSET";
	std::string strField = toMbString<int>(nID);
	const char* parms[] = { cmd.data() , m_strFileKey.data(), strField.data(), pAnnotationBuffer };
	size_t paramsLens[] = { cmd.size() , m_strFileKey.size(), strField.size(), nAnnotationBufferLen };
	bool bSaveOK = m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens);

	// - 写入GeoExtent
	if (bSaveOK)
	{
		int paramN = 5;
		std::string cmd = "GEOADD";
		std::string strKey = m_strFileKey + "-Geo";
		std::string strMinX = DoubleToString(dMinX);
		std::string strMinY = DoubleToString(dMinY);
		std::string strMaxX = DoubleToString(dMaxX);
		std::string strMaxY = DoubleToString(dMaxY);

		{
			std::string strMember = toMbString<int>(nID * 10 + 1);
			const char* parms[] = { cmd.data() , strKey.data(), strMinX.data(), strMinY.data(), strMember.data() };
			size_t paramsLens[] = { cmd.size() , strKey.size(), strMinX.size(), strMinY.size(), strMember.size() };
			m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens);
		}

		{
			std::string strMember = stlu::toMbString<int>(nID * 10 + 2);
			const char* parms[] = { cmd.data() , strKey.data(), strMinX.data(), strMaxY.data(), strMember.data() };
			size_t paramsLens[] = { cmd.size() , strKey.size(), strMinX.size(), strMaxY.size(), strMember.size() };
			m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens);
		}

		{
			std::string strMember = toMbString<int>(nID * 10 + 3);
			const char* parms[] = { cmd.data() , strKey.data(), strMaxX.data(), strMaxY.data(), strMember.data() };
			size_t paramsLens[] = { cmd.size() , strKey.size(), strMaxX.size(), strMaxY.size(), strMember.size() };
			m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens);
		}

		{
			std::string strMember = stlu::toMbString<int>(icdSaveAnnotation.m_nID * 10 + 4);
			const char* parms[] = { cmd.data() , strKey.data(), strMaxX.data(), strMinY.data(), strMember.data() };
			size_t paramsLens[] = { cmd.size() , strKey.size(), strMaxX.size(), strMinY.size(), strMember.size() };
			m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens);
		}
	}

	// 提交事务 >>>>
	m_RedisHandler.Commit();

	// 尝试一次快照持久化(避免redis服务器撑爆)
	if (nID % 10000 == 0)
	{
		m_RedisHandler.ExecuteSaveCommand();
	}

请求Binary数据:

	// 请求Binary
	class CReplyBinaryCallback : public CRedisHandler::IRedisReplyCallBack
	{
	public:

		CReplyBinaryCallback(, int nID)
			: m_refSharedMemoryComm(rSharedMemoryComm)
			, m_refIcdHeader(icdHeader)
			, m_nID(nID)
			, m_bAcked(false)
		{}
		~CReplyBinaryCallback() {}

		virtual bool OnReplyString(CRedisHandler* pHandler, char* pBuffer, int nLen)
		{
			if (NULL == pBuffer || nLen <= 0)
			{
				return false;
			}
			
			// TODO
			return false;
		}

	public:
	};

	int paramN = 3; // 命令参数个数
	std::string cmd = "HMGET";
	std::string strField = stlu::toMbString<int>(nID);
	const char* parms[] = { cmd.data() , m_strFileKey.data(), strField.data() }; // 命令
	size_t paramsLens[] = { cmd.size() , m_strFileKey.size(), strField.size() }; // 命令中每个参数的个数

	CReplyBinaryCallback callback0();
	m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens, &callback0);

请求范围:

	class CReplyExtentCallback : public CRedisHandler::IRedisReplyCallBack
	{
	public:

		CReplyExtentCallback() {}
		~CReplyExtentCallback() {}

		virtual bool OnReplyString(CRedisHandler* pHandler, char* pBuffer, int nLen)
		{
			if (NULL == pBuffer || nLen <= 0)
			{
				return false;
			}

			if (m_nMaxLimit >= 0 && m_setID.size() >= m_nMaxLimit)
			{
				return true;
			}

			int nID = stlu::stringTo<int>(std::string(pBuffer, nLen)) / 10;
			m_setID.insert(nID);

			return false;
		}

		std::vector<int> ToArray() const
		{
			std::vector<int> vec;

			std::set<int>::const_iterator itr = m_setID.begin();
			for (; itr != m_setID.end(); ++itr)
			{
				vec.push_back(*itr);
			}

			return vec;
		}

	public:
		std::set<int> m_setID;
		int m_nMaxLimit;
	};

	// 请求Geo范围
	int paramN = 6;
	std::string cmd = "GEORADIUS";
	std::string strKey = m_strFileKey + "-Geo";
	std::string strLon = toMbString<double>(dCenterX);
	std::string strLat = toMbString<double>(dCenterY);
	std::string strRadius = toMbString<double>(dRadius);
	std::string strUnit = "m";
	const char* parms[] = { cmd.data() , strKey.data(), strLon.data(), strLat.data(), strRadius.data(), strUnit.data()};
	size_t paramsLens[] = { cmd.size() , strKey.size(), strLon.size(), strLat.size(), strRadius.size(), strUnit.size()};

	CReplyExtentCallback callback0;
	callback0.m_nMaxLimit = icdQueryExtent.m_nMaxLimit;
	bool bExcute = m_RedisHandler.ExecuteCommand(paramN, parms, paramsLens, &callback0);

结果

同样的一个shp文件:【building1.shp】1.02GB,Feature个数6994448个矢量数据,通过解析一个个Feature,并二进制信息后,持久化到Sqlite和rdb文件中:

  1. 磁盘占用:sqlite的*.db文件——2GB;redis的dump.rdb文件——1.79GB
  2. 导入速度(包括解析文件+矢量序列化+写入持久化):sqlite方式55分钟;redis方式90分钟;
  3. 数据请求效率:sqlite和redis到这个量级的请求效率没有区别,地图上矢量正常显示;redis的空间索引请求效率,在范围过大(接近全部被请求),请求的ID过多时,会有明显的等待过程,不过这也和矢量图层组织数据有关,所以范围请求还是需要有“限制策略”来根据实际应用场景控制。

在GIS矢量地图加载渲染的场景中,持久化方式在Redis上并非性能优于其它方式,但这条路是可以走通的,最重要的是空间索引的支持并没有效率上的落差,也给了它在使用上的更多可能性,现在只是初步的探索,也许后面的“矢量仓库”实现可以结合sqlite数据存储+redis空间索引建立的方式去实现,达到更优的策略来达到该场景应用的目的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值