java 将查询到的值 存到缓冲区_如何实现一个JAVA堆外的内存数据库

32f3a2f2ebd4690cda9e91c393ff5736.gif

    本文将阐述如何在JAVA堆外的的内存中创建一个存储上GB甚至TB的数据库,当然,首先你得有这么大的物理内存。:)

当Java堆大小被限制为非常小(比如,16 MB)时,您可以创建一个内存中、堆外的数据存储,该存储可以容纳千兆字节的数据——甚至更多。

过程是这样的:

我使用Java的MappedByteBuffer类以及RandomAccessFile和Java NIO(New Input/Output)FileChannel为一个项目开发了一个基本的存储解决方案。我的目标是在这个存储器中存储潜在的大量数据。

优点:存储系统速度快;它不使用Java堆;使用方便;数据被持久化。然而,尽管数据是在堆外存储的,但我最初的实现使用Java HashMap作为数据的索引。该索引在完全加载时消耗了大量堆,这成为了一个问题。

因此,我决定增强实现,对索引使用相同的方法,使用MappedByteBuffer而不是HashMap。我还增强了数据存储和索引,以使用基类ByteBuffer,它提供了在内存中创建数据存储或持久化数据存储的选项——都在Java堆之外。

另一个优点是,虽然数据的读写速度很快,因为它们在内存中,但不会产生大型垃圾收集事件的开销,也不会受到非常大的Java堆的干扰。在维护非常小的Java堆的同时,您可以有效地存储tb级的内存数据(当然,如果您有足够的物理内存的话)。请继续阅读,了解如何做到这一点;您可以在这里下载所有代码:代码地址。

ByteBuffer class

java.nio.ByteBuffer继承于java.nio中的ByteBuffer类。用于将原始数据类型的值存储在内存中的字节数组中,并具有随机访问功能。您可以写入和读取除布尔型之外的所有基本类型,并且它会自动将要存储和检索的值转换为字节序列。除了基本类型之外,还可以写入和读取字节数组,这在存储字符串或支持序列化的Java对象时非常有用。

为了确保在不影响Java堆的情况下获得最高的性能,可以使用allocateDirect方法为ByteBuffer分配内存。尽管这些缓冲区比非直接缓冲区有更高的分配成本,但使用它们读写数据更有效,因为它们使用本机操作系统的I/O操作,并且不会受到垃圾回收的影响。

ByteBuffer允许相对引用和绝对引用,这意味着您可以一个接一个地写入一组值,并且类在每次调用后自动更新缓冲区内的活动位置(写入数据的位置)。或者您可以在每次调用中指定存储和检索值的位置。例如,下面代码中使用相对引用存储一系列值。

Person person = new Person();//…
ByteBuffer bb = ByteBuffer.allocateDirect(size);
bb.putInt(person.age);
bb.putFloat(person.weeklySalary);
bb.put(person.lastName.getBytes());
bb.put(person.firstName.getBytes());
bb.put((byte)(person.fullTime == true ? 1 : 0 ));

每次写入时,指向下一步将数据写入缓冲区的位置的指针都会自动更新。注意,虽然不能直接存储布尔值,但可以将其编码为字节,如上面代码的最后一行所示。检索遵循相同的模式,如清下面代码所示。

Person person = new Person();
Person.age = bb.getInt();
person.weeklySalary = bb.getFloat();//...

要正确地读取一系列值,您需要知道(并设置)您的起始位置。在此之后,您就可以按顺序进行读取,在读取时依赖ByteBuffer来保持缓冲区的位置。例如,如果你存储的值在ByteBuffer的开始,你需要设置位置回到开始通过以下调用:

bb.position(0); // Set the buffer position to the beginning

注意,读取存储的字符串值是有问题的。为了读取作为字符串构造函数使用的正确字节数,还需要存储字符串长度。尽管这并不是特别困难,因为您编写代码来处理字符串数据、布尔数据和跟踪值集(记录)存储的位置,但是您已经有了内存数据库的开端。这正是我创建NoHeapDB类的原因,我将在本文后面讨论这个类。

在此之前,让我们研究一下MappedByteBuffer和持久性。

使用MappedByteBuffer来持久化数据

ByteBuffer允许您使用内存中的字节区域来存储和检索数据。它的子类MappedByteBuffer允许您将文件的区域作为ByteBuffer映射到内存中。

因此,当您向MappedByteBuffer写入和读取值时,数据被存储到或从它映射到的磁盘文件中读取。

这种持久性模型有一些优点:它与ByteBuffer工作方式相同(使用随机访问以及相对和绝对定位),但没有物理内存大小的限制。此外,MappedByteBuffer使用底层操作系统直接进行文件和内存映射,这意味着可以避免大堆,并利用操作系统高度调优的内部特性来获得最佳性能。

要将文件映射到虚拟内存并创建MappedByteBuffer,需要调用FileChannel.map。您可以创建java.nio。FileChannel对象有三种方式(每一种都在下面第三段代码中显示):

  • 打开或创建RandomAccessFile,并调用其getChannel方法。

  • 打开或创建一个文件,并在其FileInputStream或FileOutputStream对象上调用getChannel。这将会各自产生一个只读(read only)或读/写(read/write)通道。

  • 显式创建FileChannel实例,设置读或写模式。

// Method 1: Using RandomAccessFile
RandomAccessFile raFile = new RandomAccessFile(path, "rw");
raFile.setLength(initialSize);
FileChannel fc = raFile.getChannel();
MappedByteBuffer mbb =
fc.map(FileChannel.MapMode.READ_WRITE, 0, // position
fc.size()); // size// Method 2: Using File
File file = new File(path);
file.createNewFile();
FileInputStream fis = new FileInputStream(file);
FileChannel readChannel = fis.getChannel();
MappedByteBuffer readMap =
readChannel.map(FileChannel.MapMode.READ_ONLY, 0, // position
readChannel.size()); // size// Method 3: Creating a FileChannel
FileChannel fc =
FileChannel.open( FileSystems.getDefault().getPath(path),
StandardOpenOption.WRITE,
StandardOpenOption.READ);
MappedByteBuffer mbb =
fc.map(FileChannel.MapMode.READ_WRITE, 0, // position
fc.size()); // size

然后,可以像对ByteBuffer那样对MappedByteBuffer进行读写。不同之处在于更改被持久化到最初映射的文件中。

此外,使用duplicate方法,您可以为原始缓冲区的内容创建多个视图,但是使用单独的缓冲区位置、限制和标记值。使用这些视图,您可以安全地从多个线程并发地读写同一底层内容(本例中是基于文件的)中的不同位置。这允许非常高吞吐量的内存和文件操作。

为了演示这个过程,下面第四段代码为每个线程复制了单个ByteBuffer(或MappedByteBuffer)。为每个写到缓冲区的不同区域的线程创建一组线程,还创建一组读取器线程。每个写线程在它写入缓冲区的值前面加上它的线程号,当读线程输出最新的值时,可以看到它的线程号。

ByteBuffer bb = ByteBuffer.allocateDirect(10_000);
Runnable r = new Runnable() {@Overridepublic void run() {
ByteBuffer localBB = bb.duplicate();
String name = Thread.currentThread().getName();
Integer threadNum = Integer.valueOf(name);int start = threadNum * Long.BYTES;
localBB.position(start);
localBB.mark();while ( true ) {// Make sure the first digit is the thread number. This// way when it's read and printed out, it will prove // that each thread is writing to its own buffer position// with no interference from the other threads
Long val = Long.valueOf(""+(threadNum+1)+System.nanoTime());
localBB.putLong(val);
localBB.reset();
Thread.yield();
}
}
};int maxThreads = 12;// Start the writer threads, where each writes to the bufferfor ( int t = 0; t < maxThreads; t++ ) {
Thread thread = new Thread(r);
thread.setName(""+t);
thread.start();
}// read the values from the different parts of the buffer and// output them over and overwhile ( true ) {for ( int t = 0; t < maxThreads; t++ ) {
bb.position(t*Long.BYTES);
Long val = bb.getLong();
System.out.print(val+", ");
}
System.out.println();
Thread.yield();
}
NoHeapDB的实现

当您以字节数组存储数据时(映射到持久文件存储或不映射到持久文件存储),您需要管理数据以方便地定位它。可以将数据写成有序列表或相同大小的数据数组,然后简单地遍历。但通常,您需要一种更灵活、更高效的查找方法。NoHeapDBStore类(noheapdb是一个虚包类)通过存储可迭代的数据并利用散列表作为按键进行O(1)数据查找的索引来实现这一点。这个索引是在FixedHash类中实现的,我也将在本文中介绍。

要使用NoHeapDBStore,需要创建一个“数据存储”,就像在SQL数据库中创建一个表一样。您需要提供名称、初始大小和存储类型(仅用于指示持久性或内存存储)。初始大小的默认值是100 MB,存储类型的默认值是所有数据的内存持久性。您可以选择为所有持久数据存储指定一个主目录。每个存储都表示为NoHeapDBStore类的一个实例,原始数据以ByteBuffer类型保存在成员变量buffer中。

创建内存中的存储。通过createMessageJournalBB方法创建内存中的数据存储。内存是用ByteBuffer.allocateDirect方法分配的,它请求JVM使用本机内存(而不是Java堆)作为缓冲区的后备存储。您对这个缓冲区中的字节所做的操作不受垃圾收集的影响(尽管当没有对它的更多引用时,整个ByteBuffer将被垃圾收集,就像对任何Java对象一样)。

为什么你不需要垃圾收集。一般来说,自动内存管理是没有问题的,最好称为垃圾收集(GC)。尽管随着JVM的每次发布,GC都得到了改进,但大型堆(很多gb甚至tb)上的收集事件发生时仍然会干扰应用程序线程的执行。对于某些类型的应用程序,比如有实时需求的应用程序,这可能会导致严重错误。将数据保存在内存中但不放在Java堆中,可以保证不受GC干扰的高速数据访问。

创建持久存储。持久性存储是通过createMessageJournalMBB方法创建的,这里稍微有点复杂,需要使用RandomAccessFile、FileChannel和MappedByteBuffer创建持久存储,请查看下面第五段代码:

protected final boolean createMessageJournalMBB(String journalPath) {try {// First create the directory and file within
File filePath = new File(journalFolder);
filePath.mkdir();
File file = new File(journalPath);
fileExists = file.exists();
journal = new RandomAccessFile(journalPath, "rw");if ( fileExists ) {// Existing file; so use its existing length
bufferSize = (int)journal.length();
} else {// New file; set the starting length
journal.setLength(bufferSize);
}
channel = journal.getChannel();
buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, bufferSize);if ( ! fileExists) {// New journal file; write the file header data
writeJournalHeader(journal);
currentEnd = journal.getFilePointer();
} else {// Iterate through the existing records and find its current end
currentEnd = scanJournal();
}
}catch (Exception e) {
logger.log(Level.SEVERE, "createMessageJournalMBB Exception: ", e);
}return false;
}

在上面这段代码中,首先创建文件的基目录。接下来,使用存储名称创建文件本身,然后使用该文件创建RandomAccessFile读/写流(在代码中称为日志)。如果该文件存在,则检索该文件的长度;否则,文件大小将设置为默认的初始存储大小。最后,通过映射日志的FileChannel创建一个读/写MappedByteBuffer。

为了将来的兼容性,一些文件头数据由日志的名称和文件格式的版本组成。扫描现有的数据存储日志文件(来自以前的执行)以获取活动记录,并为每个活动记录建立索引,以便通过scanJournal方法快速检索。

创建数据存储索引。每个数据存储保存一组记录,其中每个记录被标记为活动或不活动。非活动记录是指已删除的记录;它们被简单地标记为这样,它们的位置可以在以后重用。但是,为了快速检索O(1)记录,会创建一个哈希表索引,允许您以name-value对的形式存储和检索数据。

此实现的索引是一个固定大小的哈希表(与可扩展哈希相反)。然而,当固定大小的哈希表的加载因子达到一个阈值时,它实际上会增长。

索引被创建为用于内存中存储的ByteBuffer,并从RandomAccessFile创建为用于持久存储的MappedByteBuffer。它的默认起始大小是数据存储大小的四分之一,或者最小为64 MB。考虑到索引只存储键的散列版本和记录本身在数据存储中的位置,这通常已经足够大了。

但是,您有一个选择:您可以在索引中存储原始键,也可以在散列版本中存储原始键(因为它实际上是用来查找存储记录位置的bucket的)。由于它更小,存储哈希值(一个四字节的整数)比基于字符串的键更快、更紧凑。

数据存储记录结构。数据存储缓冲区(在内存中或持久存储)的结构使得所有记录都是连续的。除了数据之外,每个记录还包含记录的大小(以字节为单位)、它的类型以及指示它是否处于活动状态(即未删除)的标志。因此,可以连续遍历记录。当一个记录被删除时,它被简单地标记为已删除。这意味着可以将新记录追加到记录列表的末尾,或者插入到非活动记录的位置(如果有非活动记录且恰好与之匹配)。如果新记录更小,不活动的记录被分割成足够大的空间,以容纳新记录和一个更小的不活动的记录(参见下图)。

eaaeceb831b1d2267cd9c2041855bdd3.png

 在已删除记录的位置插入一个12字节的记录

要回收的非活动记录需要足够大,既能容纳新记录(数据和记录头),又至少能容纳另一个头,以容纳剩余的非活动记录空间。稍后我将详细介绍这个过程。

存储数据记录。类似于ByteBuffer基类,NoHeapDB通过一系列put方法支持数据存储:

  • putInteger

  • putShort

  • putLong

  • putFloat

  • putDouble

  • putString

  • putObject

  • putChar

每个方法都接受作为字符串的键和值本身,并且每个方法都调用实际实现存在的私有putVal方法。

首先,根据数据类型确定数据长度。接下来,确定新记录的位置。这个过程是在setNewRecordLocation方法中执行的,该方法与getStorageLocation(参见第六段代码)一起找到一个空闲的(不活动的)槽,以便使用新记录回收。如果没有找到,该方法将新记录追加到缓冲区的末尾。

代码6:搜索要插入新记录的空闲槽

// Is there an exact match?
LinkedList records = emptyIdx.get(recordLength);if (records != null && !records.isEmpty()) {
location.offset = records.remove();// No need to append an empty record; just return offset
location.newEmptyRecordSize = -1;return location;
}// Store empty record slots to be removed
ArrayList toRemove = new ArrayList<>();// No exact size match, find one just large enoughfor (Integer size : this.emptyIdx.keySet()) {// Enough room for the new record and another empty // record with a header and at least one byte of data?if (size >= recordLength + Header.HEADER_SIZE + 1) {
records = emptyIdx.get(size);if (records == null || records.size() == 0) { // This was the last empty record of this size// so delete the entry in the index and continue// searching for a larger empty region (if any)
toRemove.add(size);continue;
}
location.offset = records.remove();// You need to append an empty record after the new record// taking the size of the header into account
location.newEmptyRecordSize =
size - recordLength - Header.HEADER_SIZE;int newOffset = (int)
location.offset + recordLength + Header.HEADER_SIZE;// Store the new empty record's offset
storeEmptyRecord( newOffset, location.newEmptyRecordSize );break;
}
}

所有不活动的记录位置存储在一个按大小升序排序的ArrayList中。准确地说,每个ArrayList条目都是该大小的非活动记录位置的LinkedList。首先,检查新记录的确切大小,在这种情况下,可以批量使用它。由于ArrayList是按大小排序的,所以只需用新的记录大小调用get,该位置就会从“空”列表中删除并返回。

如果没有找到精确的匹配,则遍历列表,直到找到一个足够大的记录,以及一个额外的空记录(用于维护连续的记录列表)。回收的非活动槽从空列表中删除,并根据其新大小将新的非活动槽(分割原始槽以供重用的结果)添加回该空列表中。

从技术上讲,这种标记和重用已删除记录的行为是一种垃圾收集形式。此外,它的处理时间可以根据遍历的非活动记录的数量而变化。从这个意义上说,它也是不可预测的。然而,它的处理成本分散在存储新记录和(在较小程度上)删除现有记录的行为上。因此,影响是有限的,这是您作为开发人员可以控制的。例如,确保在应用程序的时间关键部分不以任何方式修改数据存储,还可以确保不会发生这种处理。

最后,数据记录存储在缓冲区中(参见代码7)。记录头由表示记录是活动的(未删除)的字节、数据类型(一个字节)和数据长度(四个字节)组成。之后,将数据值写入缓冲区。

// First write the record headerbuffer.put(ACTIVE_RECORD); // 1 bytebuffer.put(type);          // 1 bytebuffer.putInt(datalen);    // 4 bytes// Write record valueswitch ( type ) {case LONG_RECORD_TYPE:buffer.putLong( (Long)val );break;case INT_RECORD_TYPE:buffer.putInt( (Integer)val );break;case DOUBLE_RECORD_TYPE:buffer.putDouble( (Double)val );break;case FLOAT_RECORD_TYPE:buffer.putFloat( (Float)val );break;case SHORT_RECORD_TYPE:buffer.putShort( (Short)val );break;case CHAR_RECORD_TYPE:buffer.putChar( (char)val );break;case TEXT_RECORD_TYPE:buffer.put( ((String)val).getBytes() );break;case BYTEARRAY_RECORD_TYPE:buffer.put( (byte[])val );break;
}

代码7:在数据存储中存储数据记录

最后一步是为新记录建立索引,以便快速检索。通过FixedHash.put方法获取数据存储中记录的键和偏移量,它首先使用该键找到一个哈希表bucket。然后,它将自己定位到哈希表中的那个位置(bucket),并检查它是否已经被占用。

注意:代码通过调用ByteBuffer来搜索偏移量。位置,然后调用标记保存位置。接下来,调用ByteBuffer.get将文件位置向前推进一个字节并读取值:“已占用”指示器。如果bucket是空闲的,调用reset会将文件位置移动回bucket的起始位置。或者,使用位置作为参数来调用get(),在不移动文件位置的情况下读取值。但是,我发现顺序调用mark和reset方法总体上更快(请参见代码清单8)。

如果bucket是空闲的,那么键长度、键哈希码和记录偏移量都被写入索引缓冲区。可选地,您也可以让索引存储原始键,但我发现这样做会降低速度,并在缓冲区中占用更多空间,而您真正需要的只是键的哈希码来定位键。如果您喜欢存储密钥,可以将KEY_SIZE值设置为大于0的值,这样它就知道要复制多少字节。

代码清单8: 通过将键存储在哈希表中用于O(1)查找来索引数据记录

offset = getHashBucket(key.hashCode() );
indexBuffer.position(offset);
indexBuffer.mark();byte occupied = indexBuffer.get();if ( occupied == 0 ) {// found a free slot; go back to the beginning of it
indexBuffer.reset();
}else {
collisions++;
offset = findBucket(key, offset, false);// found a free slot; seek to it
indexBuffer.position(offset);
}// Write the data//
indexBuffer.put((byte)key.length() );
indexBuffer.putInt(key.hashCode()); // for resolving collisions, hashCode is faster than comparing stringsif ( KEY_SIZE > 0 ) {byte[] fixedKeyBytes = new byte[KEY_SIZE];
System.arraycopy(key.getBytes(), 0, fixedKeyBytes, 0, key.length());
indexBuffer.put( fixedKeyBytes );
}
indexBuffer.putLong( value ); // indexed record location

如果有冲突,则调用findBucket(参见代码清单9)来遍历表以查找下一个空哈希bucket。如果冲突是因为索引中的键已经存在,则重用该位置:添加一个具有相同键的新值将替换之前的值。否则,一个桶一个桶地迭代表,直到找到一个空闲的桶。

代码清单9。方法findBucket在碰撞后查找下一个空桶。

while ( occupied > 0 && ! found) {int keyHash = indexBuffer.getInt();if ( keyHash == key.hashCode() ) {if ( KEY_SIZE > 0 ) {
indexBuffer.position(
offset + 1 + Integer.BYTES + KEY_SIZE );
}
found = true;break;
}else {// Check for rollover past the end of the table
offset += INDEX_ENTRY_SIZE_BYTES;if ( offset >= (sizeInBytes - INDEX_ENTRY_SIZE_BYTES)) {// Wrap to the beginning, skipping the first bucket// since it's reserved for the first record pointer
offset = INDEX_ENTRY_SIZE_BYTES;
}// Skip to the next bucket
indexBuffer.position(offset);
occupied = indexBuffer.get();
}
}

由于每个索引项的字节数相同,只需调用ByteBuffer即可。在缓冲区中前进那么多字节的位置。检查缓冲区的末尾是否已经到达,如果到达,代码将包装到开始处。

检索数据记录。既然我已经解释了记录是如何存储在数据存储中并随后建立索引的,现在就可以深入研究数据检索了。就像每个原始数据类型都有一系列的putX方法一样,也有一系列的getX方法用于检索:

  • getInteger

  • getShort

  • getLong

  • getFloat

  • getDouble

  • getString

  • getObject

  • getChar

每个方法都接受一个String类型的键,并返回适当的数据类型。每个方法在实际实现存在的地方调用私有getVal方法(参见清单10)。首先,调用使用索引检索给定键的记录位置。如果在索引中没有找到记录位置,则返回null。

代码清单10。从数据存储中检索数据记录

Long offset = index.get(key);//...// Jump to this record's offset within the journal file
buffer.position(offset.intValue());// First, read in the headerbyte active = buffer.get();if (active != 1) {return null;
}byte type = buffer.get();int dataLength = buffer.getInt();// Next, read in the databyte[] bytes;switch ( type ) {case LONG_RECORD_TYPE:
val = buffer.getLong();break;case INT_RECORD_TYPE:
val = buffer.getInt();break;case DOUBLE_RECORD_TYPE:
val = buffer.getDouble();break;case FLOAT_RECORD_TYPE:
val = buffer.getFloat();break;case SHORT_RECORD_TYPE:
val = buffer.getShort();break;case CHAR_RECORD_TYPE:
val = buffer.getChar();break;case BYTEARRAY_RECORD_TYPE:
bytes = new byte[dataLength];
buffer.get(bytes);
val = bytes;break;case TEXT_RECORD_TYPE:
bytes = new byte[dataLength];
buffer.get(bytes);
val = new String(bytes);break;
}

如果找到文件位置,则将其设置为返回的偏移量(来自索引)。接下来,读取活动记录指示符字节。如果记录是活动的,则记录的数据类型和长度分别作为字节和整数读取。最后,根据记录的类型读取记录的数据,并且仅在字符串或字节数组的情况下,读取其长度。让我们备份并检查索引查找是如何找到记录的位置的(参见代码清单11)。

代码清单11:从索引中加载记录位置

public Long get(String key) {int bucketOffset = getHashBucket( key.hashCode() );
indexBuffer.position(bucketOffset);byte occupied = indexBuffer.get();if ( occupied > 0 ) {
bucketOffset = findBucket(key, offset, true);
}if ( bucketOffset == -1 ) {// Record not foundreturn -1L;
}// Return the location of the data recordreturn indexBuffer.getLong();
}

首先,通过散列键来定位索引中的bucket。缓冲区定位到返回的偏移量,并读取一个字节(占位标志)。如果桶被标记为已占用,则调用findBucket(参见代码清单9)来确定这是否确实是记录的散列桶。它通过比较键并迭代,直到找到键或遇到空桶(表示该键不在索引中)为止。最后,如果找到了键,记录在数据存储中的位置将作为长值从索引中读取并返回。

删除数据记录。从数据存储中删除记录很简单(参见清单10)。在索引中快速查找确定记录是否存在,如果存在,则返回其位置。

代码清单10。从数据存储中删除一条记录

// Locate the message in the journal
offset = getRecordOffset(key);if (offset == -1) {return false;
}// read the header (to get record length); then set it as inactive
buffer.position(offset.intValue());
buffer.put(INACTIVE_RECORD);
buffer.put(EMPTY_RECORD_TYPE);
datalength = buffer.getInt();// Store the empty record location and size for later reuse
storeEmptyRecord(offset, datalength);// Remove from the journal index
index.remove( key );

缓冲区位置设置为记录的开始位置,第一个字节设置为指示该记录是不活动的,记录类型设置为指示槽为空。然后将记录在缓冲区中的位置及其长度添加到空记录列表中,以便在向数据存储中添加新记录时回收。

最后,这个记录的索引项也被删除。这个过程也很简单:键的哈希桶被定位。如果桶被占用,则使用findBucket进行碰撞检查(再次参见清单9),就像获取记录一样。如果确实找到了这个键,那么桶的第一个字节(已占用标志)被设置为0,表示桶不再被占用。

遍历记录。我已经介绍了按键检索记录,但是您还可以遍历数据存储中的所有记录。从缓冲区的开始(参见图2),读取每个记录的活动字节、数据类型和长度。如果记录是活动的,则返回它。如果它不是活动的,则跳过记录位置并检查下一个记录,依此类推。 

8be12998b70f2216bc12dcefe2416f97.png

图2:数据存储中的记录结构示例

迭代首先在方法iterateStart中查找缓冲区的开始部分(如果它是持久的数据存储,则跳过文件头),然后通过调用getNextRecord搜索下一个活动记录(参见清单11)。开始搜索的位置作为参数传递。

while ( !found && current < (bufferSize - Header.HEADER_SIZE)) {boolean active = true;if (buffer.get() == INACTIVE_RECORD) {
active = false;
}// Read record type
type = buffer.get();if (type == EMPTY_RECORD_TYPE) {
buffer.position((int)currentEnd);break; // end of data records in file
}// Get the data lengthint datalen = buffer.getInt();
recordSize = Header.HEADER_SIZE + datalen;if ( active) {// Found the next active record
found = true;// Store the location of the start of the next record
iterateNext = current + recordSize;
}else {// skip past the data to the beginning of the next record
current += recordSize;
buffer.position( (int)current );
}
}if ( found ) {// Return the recordreturn getValue(current, type);
}

代码循环,直到找到活动记录或到达文件的末尾为止。首先,读取活动记录字节,然后是数据类型和数据长度。如果该记录处于非活动状态,则使用该记录的数据长度,缓冲区指针将被提升到下一个记录。如果该记录是活动的,则将标志设置为结束循环,并检索并返回该记录的值。存储下一条记录的位置,并在调用iterateNext方法时使用它。这样你可以反复调用iterateNext,直到返回一个空值,表示没有更多的记录,如下所示:

Object recordObj = iterateStart();while ( recordObj != null ) {// Do something with the returned record
...// Get the next record in the data store
recordObj = iterateNext();
}

指定日志和索引大小。您可以选择指定数据存储缓冲区的起始大小(这也决定了索引大小);否则,它默认为100 MB。缓冲区将自动扩展为100 MB增量,当它被填满时。但是,如果您知道需要多少存储空间,那么从一开始就相应地调整数据存储的大小会更有效。

扩展数据存储的代码因内存或持久缓冲区的不同而不同。清单12显示了内存中的版本。

ByteBuffer newBuffer = ByteBuffer.allocateDirect((int)newLength);
if ( buffer.hasArray() ) {
byte[] array = buffer.array();
newBuffer.put( array );
}
else {
buffer.position(0);
newBuffer.put(buffer);
}
buffer = newBuffer;
journalLen = buffer.capacity();

索引会自动调整为数据存储初始大小的四分之一,并且当它通过一个加载阈值(缺省情况下为75%)时,还会自动扩展(并重新散列)。

使用NoHeapDB实现

要使用堆外数据存储(在这里下载所有代码),请创建NoHeapDB类的一个实例,它本质上是用于所有操作的API。由于Java的ByteBuffer类的限制,您不能创建一个比带符号的四字节整数最大值(2,147,483,647字节,或2 GB)更大的数据存储。为了克服这个限制,您可以创建多个数据存储并在它们之间细分数据。或者,您可以扩展NoHeapDBStore类的实现,以创建一个ByteBuffer对象数组,以便对类的用户进行聚合、封装和隐藏。

NoHeapDB类包含DataStore API,它有两个构造函数。一个使用一个字符串来表示数据存储备份文件应该写入的位置。否则,默认为用户的主目录。在这两种情况下,都会创建一个名为JavaOffHeap的目录,该目录将为每个持久数据存储包含两个文件:一个用于数据,另一个用于索引。

有三个createDataStore方法。第一个有名字;第二个接受一个名称和一个类型(IN_MEMORY或persistent);第三个接受名称、类型和以兆字节为单位的起始大小。默认的类型是IN_MEMORY,默认的起始大小是100mb,记住数据存储会根据需要以100mb的增量增长。

NoHeapDB db = new NoHeapDB();
db.createStore("MyTestDataStore",
DataStore.Storage.IN_MEMORY, //or DataStore.Storage.PERSISTED256); // in MB

要插入数据,请调用其中一个putX方法。您可以继续按名称引用数据存储。或者,您可以获得对内部数据存储实现的引用,并直接在其上调用方法。它的API也与ByteBuffer的API匹配。

String key = ...;
String value = ...;
db.putString("MyTestDataStore", key, value);// or
Integer intValue = ...;
DataStore store = db.getStore("MyTestDataStore");
store.putInteger(key, intValue);

检索数据也同样容易。在适当的情况下调用一个getX方法,为查找提供一个键。如果没有找到该值,则返回null。

Integer intValue = db.getInteger("MyTestDataStore", key);// or
DataStore store = db.getStore("MyTestDataStore");
intValue = store.getInteger(key);

调用remove方法删除数据值条目,提供值的键值,例如:

String key = ...;
db.remove("MyTestDataStore", key);// or
DataStore store = db.getStore("MyTestDataStore");
store.remove(key);

要在不使用键的情况下遍历数据存储中的条目,可以调用iterateStart开始并重复调用iterateNext,直到返回一个空值。如果数据存储中没有值,iterateStart将返回null。

Object val = db.iterateStart("MyTestDataStore");while ( val != null ) {// do something with val// ...
val = db.iterateNext("MyTestDataStore");
}

你可以通过调用deleteStore来删除整个数据存储(也就是说,删除它所有的条目和删除它作为持久化存储创建的备份数据文件),例如:

db.deleteStore("MyTestDataStore");
总结

NoHeapDB实现的优势是显而易见的:

  • 基于内存的存储性能

  • 高速、高效、磁盘持久性

  • 没有垃圾收集损失,因为数据在Java堆之外

  • 易用性

  • 100%纯Java解决方案

这个实现还可以更进一步。这里有一些潜在改进的例子:

  • 使用ByteBuffer对象的内部数组来超过2gb的最大大小限制。

  • 添加安全选项,例如数据加密和用户身份验证。

  • 扩展MappedByteBuffer的使用,使其仅在内存中缓存文件的一部分。这对于大于物理RAM的基于文件的数据集非常有用。

  • 提供云中的第二层存储。

这些只是我正在进行的一些增强。您可以在这里下载NoHeapDB存储的当前实现。http://www.ericbruno.com/NoHeapDB-EricBruno.zip

Java数据库查询结果的输出 摘自:北京海脉信息咨询有限公司   利用Java开发数据库应用时,经常需要在用户界面上显示查询结果。我们可以利用Vector、JTable、AbstractTableModel等三个类较好地解决这一问题。 类Vector:   定义如下: public class Vector extends AbstractList implements List , Cloneable , Serializable{…} 类JTable:   JTable组件是Swing组件中比较复杂的小件,隶属于javax.swing包,它能以二维表的形式显示数据。类Jtable: 定义如下: public class JTable extends JComponent implements TableModelListener, Scrollable, TableColumnModelListener, ListSelectionListener, CellEditorListener, Accessible{…} 类AbstractTableModel:   定义如下: public abstract class AbstractTableModel extends Object implements TableModel, Serializable{…}   生成一个具体的TableModel作为AbstractTableMode的子类,至少必须实现下面三个方法: public int getRowCount(); public int getColumnCount(); public Object getValueAt(int row, int column);   我们可以建立一个简单二维表(5×5): TableModel dataModel = new AbstractTableModel() { public int getColumnCount() { return 5; } public int getRowCount() { return 5;} public Object getValueAt(int row, int col) { return new Integer(row*col); } }; JTable table = new JTable(dataModel); JScrollPane scrollpane = new JScrollPane(table); 数据库及其连接方法:   我们采用Sybase数据库数据库存放数据库服务器中。路径为:D:WORKER,数据库名为:worker.dbf。具有以下字段: 字段名 类型 Wno(职工号) VARCHAR Wname(职工名) VARCHAR Sex(性别) VARCHAR Birthday(出生日期) DATE Wage(工资) FLOAT   要连接此数据库,需使用java.sql包中的类DriverManager。此类是用于管理JDBC驱动程序的实用程序类。它提供了通过驱动程序取得连接、注册,撤消驱动程序,设置登记和数据库访问登录超时等方法。   具体连接方法如下:   定位、装入和链接SybDriver类。 driver="com.sybase.jdbc.SybDriver"; SybDriver sybdriver=(SybDriver) Class.forName(driver).newInstance();   注册SybDriver类。 DriverManager.registerDriver(sybdriver);   取得连接(SybConnection)对象引用。 user="sa"; password=""; url="jdbc:sybase:Tds:202.117.203.114:5000/WORKER"; SybConnection connection= (SybConnection)DriverManager.getConnection (url,user,password); 建立完连接后,即可通过Statement接口进行数据库查询与更改。 实现方法:   对象声明。   AbstractTableModel tm;   //声明一个类AbstractTableModel对象   JTable jg_table;//声明一个类JTable对象   Vector vect;//声明一个向量对象   JScrollPane jsp;//声明一个滚动杠对象   String title[]={"职工号","职工名",   "性别","出生日期","工资"};   //二维表列名   定制表格。   实现抽象类AbstractTableModel对象tm中的方法:   vect=new Vector();//实例化向量   tm=new AbstractTableModel(){   public int getColumnCount(){   return title.length;}//取得表格列数   public int getRowCount(){   return vect.size();}//取得表格行数   public Object getValueAt(int row,int column){   if(!vect.isEmpty())   return   ((Vector)vect.elementAt(row)).elementAt(column);   else   return null;}//取得单元格中的属性   public String getColumnName(int column){   return title[column];}//设置表格列名   public void setValueAt   (Object value,int row,int column){}   //数据模型不可编辑,该方法设置为空   public Class getColumnClass(int c){   return getValueAt(0,c).getClass();   }//取得列所属对象类   public boolean isCellEditable(int row,int column){   return false;}//设置单元格不可编辑,为缺省实现   };   定制表格:   jg_table=new JTable(tm);//生成自己的数据模型   jg_table.setToolTipText("显示全部查询结果");   //设置帮助提示   jg_table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);   //设置表格调整尺寸模式   jg_table.setCellSelectionEnabled(false);   //设置单元格选择方式   jg_table.setShowVerticalLines(true);//   设置是否显示单元格间的分割线   jg_table.setShowHorizontalLines(true);   jsp=new JScrollPane(jg_table);//给表格加上滚动杠   显示查询结果。   连接数据库:已给出。   数据库查询:   Statement stmt=connection.createStatement();   ResultSet rs=stmt.executeQuery   ("select * from worker");   显示查询结果:   vect.removeAllElements();//初始化向量对象   tm.fireTableStructureChanged();//更新表格内容   while(rs.next()){   Vector rec_vector=new Vector();   //从结果集中取数据放入向量rec_vector中   rec_vector.addElement(rs.getString(1));   rec_vector.addElement(rs.getString(2)); rec_vector.addElement(rs.getString(3)); rec_vector.addElement(rs.getDate(4));   rec_vector.addElement(new Float(rs.getFloat(5)));   vect.addElement(rec_vector);   //向量rec_vector加入向量vect中   }   tm.fireTableStructureChanged();   //更新表格,显示向量vect的内容   实现示图中记录前翻、后翻的效果,有两种方法:   如果软件环境支持JDBC2.0,可直接利用rs.prevoius()和rs.next()获得记录,然后通过类JTextField中的setText()方法,显示出各个字段。   如果不支持JDBC2.0,则可利用向量Vector按行取出JTable中数据。自定义一个指针,用来记录位置。当指针加1时,取出上一行数据放入Vector中显示;指针减1时,取出下一行数据显示。显示方法同上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值