很长一段时间没有更新博客了。
近日,工作中解决了一个大数据量的传输问题。
我们有几万 至 几十万条数据,甚至更多的数据需要通过后端处理和传输给浏览器。
这个时候传输的json数据可能有 十几 至 几十兆 的数据量。
java 如何如何快速处理数据,压缩数据。让用户能够快速的看到页面上的数据。
服务器处理数据的时间 + 网络io传输的时间 总时间最小
。当然js处理这些数据,将其展示到浏览器的时间不计入,其主要的原因是当下的用户个人电脑往往性能足够,可以快速处理这些数据。作为一个java 开发者,在设计接口的时候我们往往会用对象进行存储数据。
例如:
@Data
public class Data {
private String field1;
private String field2;
... ...
}
/**
* sample interface
*/
public interface A {
public List<Data> method();
}
这种方式我们非常乐意去写,而且也非常的容易理解前端和后端的沟通是比较方便的。
但是 这种接口并不适合大数据量的传输,如果我们有十几万条数据(十几万个 Data对象
),通过这种方式我们会传输很多冗余的内容。
下面的数据结构
造成的 问题一:重复的字段名称
[
{
"field1": "value1",
"field2": "value2-1",
...
},
{
"field1": "value1",
"field2": "value2-2",
...
},
...
]
比方说重复的字段名称field1、field2
会通过json的方式传输给前端。占用了网络带宽。
造成的问题二:没法压缩字段值中重复的数据
比如上述例子中的 field1
如果存在很多的重复值value1
的话,传输过程中也会浪费我们的带宽。
问题一的解决方案很简单,我们可以利用数组这种紧凑的数据结构来去除重复的字段名称;
我们可以把数据结构改造成这样:
"columns":["field1","field2",...]
"data":[
["value1","value2-1",...],
["value1","value2-2",...],
...
]
这种方式在日常工作中也见到过,相信很多开发人员也见过。
在开发过程中我们常常会用Mybatis/MybatisPlus/JPA
等数据库持久层框架进行开发,往往我们通过sql得到数据后就直接转换成了一个Bean对象,然后通过Bean再将这种数据转换成上述的格式,但这样明显多了中间一层,实际上是可以避免的,毕竟数据量如果达到十几万以上时会造成额外的 内存 和 性能 开销。
因此建议避免先转换为Bean再转上述结构,应该直接得到上述结构。
怎么实现的,可以多种多样,可以用原生的 jdbc 操作也可以用上述持久层框架的一些高级用法。
问题二的产生依赖于我们现有的数据特征。
比较适合 数据值重复率高的场景,比方说 "value1"
出现了很多次。那么就有必要压缩这类数据,这样可以大大的减少传输的数据大小。
当然往往这种数据我们可能会想到将其转换为下面的结构
案例一:
[
{
"field1": "value1",
"field2": ["value2-1","value2-2",...]
...
},
...
]
或者类似的结构
案例二:
[
{
"value1": ["value2-1","value2-2",...]
...
},
...
]
总之我们的目的是压缩数据,但是上述数据结构需要权衡一下 java处理数据的时候 + 传输的时间
总值,上述的结构可能会需要 java 额外的转换和处理,消耗总时间未必是一个最佳值(因为我们往往需要结合数据库表的设计,往往这种层级的数据结构的依赖于我们多张表的联表查询得到
)。
上述结构无法做到的一件事情
特殊场景:
以案例一的数据结构为例子:假设我们的 field2 字段是来自数据库中column1 + '-' + column2
得到的,并且colum1
的值在数据中出现了非常多的重复。假设column1的值有2种,分别是 value1 和 value2
。如果我们想将这里的重复数据提取出来,起到压缩数据的效果。
如果是在案例一 或者 案例二的数据结构基础上再度提取数据的话并不太好操作。
一旦我们实现了该结构,还要想着如何还原得到我们想要的数据结构。
如果存在3级以上乃至更多级的嵌套数据,虽然做到了数据大小最小,但是需要额外的消耗更多的处理。未必是一种最佳方案。
为了要达到 java 处理数据时间最短,我们希望从数据库得到的数据尽可能的少做额外的运算操作。
假设a表
column1 | column2 |
---|---|
ZhangSan | nan |
LiSi | nv |
假设b表
column1 | column3 | column4 |
---|---|---|
ZhangSan | 98 | 100 |
LiSi | 50 | 59 |
原先的query我们可能是这样的结构,需要联表查询
select a.column1, a.column2|| '-' || b.column3, b.column4
from table1 as a left join table2 as b
where a.column1=b.column1
联表的query结果
column1 | column2-column3 | column4 |
---|---|---|
ZhangSan | nan-98 | 100 |
LiSi | nv-50 | 59 |
为了压缩数据,首先我们要将query中的拼接逻辑去除,不然传给java的结果已经是字符串,如果还要额外的处理会造成额外的计算开销。
将query改造成下面的结构,去掉字段拼接逻辑
select a.column1, a.column2, b.column3, b.column4
from table1 as a left join table2 as b
where a.column1=b.column1
在上述的前提下我们可以通过 java 将数据进行去重操作。另外上述的拼接逻辑需要由后端传给前端,让前端进行拼接。
因此新的数据结构要求如下:
a.column2|| '-' || b.column2
,这个规则是固定的)那么我想到的数据结构是这样的
{
//1、字段的拼接逻辑
"logic":["column1","column2-column3","column4"]
//2、 数组下标对应的字段名称
"columns":["column1","column2","column3","column4"]
//3、需要压缩的字段,假设我们希望压缩column2和column3字段,则这里面填入
"needCompressedColumn":["column2","column3"]
//4、利用数组去除冗余的字段名称,需要结合2 实现,data中数组中的值代表对应的column值
data:[
["ZhangSan",0,0,100],
["LiSi",1,1,100],
...
]
//5、去重后的数据集中放一起
noRepeatData:{
columns2:["nan","nv"],
columns3:[98,50]
}
}
之所以这样设计的目的是
1、还原起来逻辑简单
我们知道:["ZhangSan",0,0,100]
结合"columns":["column1","column2","column3","column4"]
可以很容易得到
下面的结构,根据下标一 一对应即可
{
"column1":"ZhangSan",
"column2": 0,
"column3": 0,
"column4": 100
}
column2,column3
noRepeatData
中取出对应的column2中下标为0的值是"nan" 同理column3也一样可以得到值98就可以得到我们需要的{
"column1":"ZhangSan",
"column2": "nan",
"column3": 98,
"column4": 100
}
"logic":["column1","column2-column3","column4"]
我们可以很容易的将其转换为{
"column1":"ZhangSan",
"column2-column3": "nan-98",
"column4": 100
}
这里的逻辑转换可以通过js来完成。
而对于java 只需要做一个去重操作即可。
其逻辑就是遍历 得到的联表结果,一行一行处理,在处理每一个字段时,判断一下该字段是否需要压缩。
如果需要压缩则得到值后看下是否前面已经存入到noRepeatData 中了,如果存入了则只有使用之前的索引,如果没有则添加到noRepeatData中并且得到该值的下标存入 当前行数组。
这样就可以非常快速的得到上述 理想的数据结构
。
并且我们可以根据数据特征去配置需要压缩的字段,可以灵活的去使用在任意一个大数据量传输的场景。
可能需要改动的点:
query需要稍微变一下
该改动的影响很小,只是改变了java返回的数据结构,js 再加一个公共的数据结构转换的函数即可做到压缩数据的效果。
相信会有非常不错的性能提升。
而且该方法适用于任意需要压缩数据的场景。都可以快速得到压缩后的数据结构,并且可以通过压缩后的结构快速的得到任意你想要的数据结构