朝花夕拾

A Development Engineer, a Life Liver, a Hope Holder

业务背景

   如果我们在某些场景中,需要存储和查询批量轨迹点(查询历史轨迹)。比如,一个小件员过去几天的经纬度坐标数据。如果app采集小件员轨迹的频率是1个/1秒。那么一天如果工作8小时,一天一个小件员的轨迹量是86060 = 28800个轨迹点。我们通常的做法是将小件员的实时位置信息采集存储到Hbase。那么如果有客户一次查询3天的轨迹数据,那么Hbase内部查询+HBASE到本服务的网络IO+本服务(不做任何操作直接范围)到客户端的网络IO会是一个比较大的RT。目前测试,使用阿里内部的HBASE一次查询3W+的轨迹点耗时>1s。这对某些业务是不能忍受的。所以我们必须采取其他办法对这些轨迹点进行压缩。

目前采取的轨迹存储

存储DB:HBASE
RK: userId_timestamp
val: f:lon, f:lat
这种方式的存储,如果需要查询某个用户一段时间的轨迹序列,只需:

startRk = userId_startQueryTimestamp;
endRk = userId_endQueryTimestamp;
hbase.scan(startRk, endRk);

如上面伪代码所示,如果查询结果轨迹点较少,其实是没问题的。但是当如果在给定查询时间区间内有很多的轨迹点。这种查询就会出现上面描述的问题。

存储压缩算法

思路:我们能不能把一个人的散列轨迹数据拟合成一段轨迹记录。比如,小件员一天的轨迹散列,我们通过离线任务将其拟合成一条完整的轨迹记录。按天轨迹的查询Hbase的压力转移到了单条轨迹记录的查询压力。如果单条轨迹某一列数据较大同样会产生性能问题。那么我们接下来想办法压缩这单条数据就OK了。

10进位转高进位算法压缩

假如我们用一下方式存储一个用户一段时间的完整轨迹数据

#Line(lon1,lat1;lon2,lat2;..........lonN,latN)

那么,如果我们按照经纬度原始字符串存储,那么当轨迹点较多时会是一个比较大的存储压力。这时,我们可以使用经纬度转高经纬度的方案采取压缩。具体实施如下:
//1. 将经纬度转为整数
int lonLatNum = (int) lonLat * 1000000;
StringBuilder sb ;
Stack s;
// 2. 循环编码
while(n != 0)
int index = n % radix
char c = getChar(index);
n /= radix;
s.push(c);
while(!s.empty())
sb.append(s.pop());


这样经过几次转换,10位的经纬度字符串就压缩为3,4位的字符串了。这种压缩方式的压缩比例经测试大概是72%左右

差值压缩

这种压缩思路是,我们只记录轨迹序列第一个轨迹点的精确值,后面的经纬度信息只保留与第一个经纬度之间的差值

StringBuilder.append(Lon[0]).append(',').append(Lat[0]).append(';').append(Lon[1] - Lon[0]).append(',').append(Lat[1]-Lat[0])....append(Lon[n] - Lon[n-1]).append(',').append(Lat[n] - Lat[n-1]));

这种压缩算法在经纬度精度越大的时候效果越明显。经测试,当经纬度为6位小数位精度时,压缩比可以到达75%。而且在[100 - &)的区间里,压缩比非常稳定。