学过操作系统的人都知道请求分页存储管理方法,这也是为啥1G内存的计算机能够处理超过1G的数据量的原因。最近做项目遇到大数据量处理的问题,结合请求分页算法的原来,成功的处理了一亿条数据。到这儿来分享一下给大家:
首先是一亿条数据的存储问题,这么多数据放在内存里,肯定会溢出的,更不用提数据的处理了。所以肯定要借助于磁盘存储了。结合操作系统的文件系统里的层次索引结构,我们可以设计出高效的存储方法。具体的算法思想如下:
先将一亿条数据分成10000组,那么1组有10000条数据。正常内存里放着几个组,内存里存放的组的上限比如说是10个,也就是内存最多放10组。如果要到的数据不在内存里,那么从磁盘里面取出来。假如当前内存里已经有10组了,那么就先要将最不活跃的那一组先从内存里销毁,再取出目标组代替之。我们需要建立一个关于组的索引,同时组内也要有一个索引。那么我们可以简单的通过组索引和组内索引,轻松的找到目标的位置。一般情况下,我们可以把一个组写入一个文件。
我们可以更近一步,设计出更多层次的结构。比如说:将10亿条数据分成10块,1块就是1亿条,再将一块分成10000组,一组10000条。这就是三层索引了。
我们需要注意的是:组的大小是个未知的值。如果组的大小设置的过小,那么组的数量会很多,实际的文件就会很多,假如有个遍历操作,需要建立的输入输出流数量就是文件的数量,那么重复的建立输入输出流就会非常的耗时耗资源。假如组的大小设置的过大,那么读取一个组的时候,将组文件转化成指定的数据结构就会很耗时。那么,获取一个给定值时,就会因此很慢。所以,组的大小,需要实际中根据性能要求去反复的调试、实验,以获取一个最佳值。
个人测试了好久,觉得最影响性能还是I/O操作。所以,应该考虑一些高效的I/O操作方法。比如说字节缓冲机制等等。
不多说:最简单的Java代码实现如下:
/**
* 双层次索引顺序表
*
* 索引结果请不要用Iterator,用它效率非常差,Iterator是针对链表结构的,比如说LinkedList
* @author zhou@since 2012-8-7下午5:33:00
*/
public class TwoLevelArrayList extends AbstractList {
/**
* ArrayList<Group>
*/
private ArrayList groupArray;// 第一层次
/**
* 当前的group,它都是alive
*/
private Group currentGroup;
private int currentGroupIndex;
private String cacheFilePath;
private static final String FILE_EXTENSION = ".part";
/**
* cacheFile是个文件夹
*
* @param parentFile
*/
public TwoLevelArrayList(File parentFile) {
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!parentFile.isDirectory()) {
parentFile.delete();
throw new UnsupportedOperationException("cacheFile必须是个文件夹!");
}
String id = UUID.randomUUID().toString();// 生成一个临时文件夹
cacheFilePath = parentFile.getAbsolutePath() + File.separator + id;
File cacheFile = new File(cacheFilePath);
cacheFile.mkdirs();
// 默认是只存在一个分组的
groupArray = new ArrayList(1);
currentGroup = new Group(0);
groupArray.add(currentGroup);
currentGroupIndex = 0;
}
public Object get(int index) {
int groupIndex = index / Group.MAXSIZE;// 组序号
int inGroupIndex = index % Group.MAXSIZE;// 组内序号
setCurrentGroup(groupIndex);
return currentGroup.get(inGroupIndex);
}
public int size() {
int groupSize = groupArray.size();
if (groupSize == 1) {
return ((Group)groupArray.get(0)).size();
}
// 最后一个Gruop里面的数量肯定少于Group.MAXSIZE,而之前的里面的数量肯定是等于Group.MAXSIZE的
return Group.MAXSIZE * (groupSize - 1) + ((Group)groupArray.get(groupSize - 1)).size();
}
public Object set(int index, Object element) {
int groupIndex = index / Group.MAXSIZE;// 组序号
int inGroupIndex = index % Group.MAXSIZE;// 组内序号
setCurrentGroup(groupIndex);
return currentGroup.set(inGroupIndex, element);
}
public boolean add(Object o) {
int groupSize = groupArray.size();
Group lastgroup = ((Group)groupArray.get(groupSize - 1));
int groupindex = lastgroup.size() == Group.MAXSIZE ? groupSize : groupSize - 1;
setCurrentGroup(groupindex);
return currentGroup.add(o);
}
public void add(int index, Object element) {
int groupIndex = index / Group.MAXSIZE;// 组序号
int inGroupIndex = index % Group.MAXSIZE;// 组内序号
setCurrentGroup(groupIndex);
currentGroup.add(inGroupIndex, element);
}
public int indexOf(Object o) {
return -1;// 这个要支持吗?假如需要,必须考虑一个高效的遍历算法
}
public void clear() {
currentGroup.clear();
groupArray.clear();
new File(cacheFilePath).delete();
}
public Iterator iterator() {
throw new UnsupportedOperationException();
}
private void setCurrentGroup(int groupIndex) {
if (currentGroupIndex == groupIndex) {
return;
} else {
try {
// first:先将当前的给sleep了
sleepCurrentGroup();
// second:将新的currentGroup给唤醒。如果新的currentGroup不存在,那么无需唤醒操作
currentGroupIndex = groupIndex;
if (currentGroupIndex >= groupArray.size()) {// 无需唤醒
currentGroup = new Group(currentGroupIndex);
groupArray.add(currentGroupIndex, currentGroup);
} else {
currentGroup = (Group)groupArray.get(currentGroupIndex);
wakeupCurrentGroup();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void sleepCurrentGroup() throws Exception {
File file = new File(cacheFilePath + File.separator + currentGroupIndex + FILE_EXTENSION);
if (!file.exists()) {
file.createNewFile();
}
OutputStream outputStream = new FileOutputStream(file);
GZIPOutputStream gzipouptStream = new GZIPOutputStream(outputStream);
currentGroup.sleep(gzipouptStream);
gzipouptStream.flush();
gzipouptStream.close();
outputStream.flush();
outputStream.close();
}
private void wakeupCurrentGroup() throws Exception {
File file = new File(cacheFilePath + File.separator + currentGroupIndex + FILE_EXTENSION);
if (!file.exists()) {
return;
}
InputStream inputStream = new FileInputStream(file);
GZIPInputStream gzipInputStrean = new GZIPInputStream(inputStream);
currentGroup.wakeup(gzipInputStrean);
gzipInputStrean.close();
inputStream.close();
}
}
组的数据结构:import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Group是映射到一个文件的
* @author zhou @since 2012-8-7下午5:33:00
*/
public class Group {
private int index;// 当前组所处于的位置
/**
* usually the Object of this ArrayList is an ArrayList
*/
private ArrayList m_data;
/**
* 当前Group里最多放10000个Item
*/
public static final int MAXSIZE = 10000;
// 需要一个量来保存size,而不是通过计算m_data得到,因为一些死的Group里面的m_data是不存在的。不然会有性能影响
// size是不依赖与m_data的,但是,却又与m_data.size()始终保持相等
private int size;
public Group(int index) {
this.setIndex(index);
m_data = new ArrayList();
}
/**
* prepareData应该统一在一个组要进行任何一个操作时准备,而且应该放在外面处理,而不是在每个操作方法里去判断是否准备完毕。
* 如果在每个操作方法里判断,那么需要重复判断“操作数*MAXSIZE”次,很影响性能。但是在外面处理,就需要用的人特别小心了,不然一不小心就出错了
*/
public void wakeup(InputStream inputStream) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
try {
if (reader.ready()) {
while (isNotEmpty((line = reader.readLine()))) {
String[] args = line.split(",");
m_data.add(Arrays.asList(args));
}
}
reader.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
trimToSize();
}
}
public static boolean isNotEmpty(String str) {
return str != null && str.length() != 0;
}
public Object get(int index) {
return m_data.get(index);
}
public boolean add(Object o) {
m_data.add(o);
size++;
return true;
}
public void add(int index, Object element) {
m_data.add(index, element);
size++;
}
public Object remove(int index) {
size--;
return m_data.remove(index);
}
public void trimToSize() {
m_data.trimToSize();
size = m_data.size();
}
public int size() {
return size;
}
public void clear() {
if (m_data != null) {
m_data.clear();
}
size = 0;
}
public void sleep(OutputStream outputStream) {
write2File(outputStream);
m_data.clear();
}
private void write2File(OutputStream outputStream) {
PrintWriter outWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream)));
StringBuffer sb = new StringBuffer();
for (int i = 0; i < size; i++) {
ArrayList data = (ArrayList)m_data.get(i);
Object ob = null;
for (int j = 0, len = data.size() - 1; j < len; j++) {
ob = data.get(j);
sb.append(ob == null ? "&&&" : ob.toString() + ",");
}
ob = data.get(data.size() - 1);
sb.append(ob == null ? "&&&" : ob.toString()+"\n");
}
sb.deleteCharAt(MAXSIZE);
outWriter.print(sb.toString());
outWriter.flush();
outWriter.close();
outWriter = null;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public Object set(int index, Object element) {
return m_data.set(index, element);
}
}
import java.io.File;
import java.util.ArrayList;
//测试类:
public class TestArrayList {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
System.out.println(r.maxMemory() / 1024 / 1024);
// ArrayList list = new ArrayList();
// int count = 5000000;
// long start = System.currentTimeMillis();
// while (count > -1) {
// list.add("1");
// count--;
// }
// System.out.println(System.currentTimeMillis() - start);
String demopath_original = "D:\\test";
TwoLevelArrayList list = new TwoLevelArrayList(new File(demopath_original));
int count = 0;
long start = System.currentTimeMillis();
while (count < 5000000) {
ArrayList xx = new ArrayList();
xx.add("1" + "a" + count);
xx.add("2");
xx.add("3");
xx.add("4");
xx.add("5");
xx.trimToSize();
list.add(xx);
count++;
}
System.out.println(System.currentTimeMillis() - start);
start = System.currentTimeMillis();
System.out.println(list.get(100000));//获取第100000个数据
System.out.println(System.currentTimeMillis() - start);
list.clear();
}
}
个人的eclipse启动的JVM的内存为63M当然,Group里面放的数据和读取的操作可以自己定义。我个人试了下500W行的数据,分组大小为1W的情况,取一个不在当前内存里的组的情况下,花了200ms。主要的操作还都是I/O上,如果用缓冲机制,那么应该可以降到50ms一下。这就非常OK了。
大家可以看到只有63M的内存能轻松的读取、处理500W行的数据量,这还是非常具有诱惑力的。