JAVA在内存中操作列式数据实现介绍(一)

180 阅读17分钟

定义与概述

Apache Arrow 是一个跨语言开发平台,用于内存中高性能分析计算。它的主要目的是通过定义一种统一的内存格式来加速数据的处理和交换。这种格式可以在不同的计算系统(如数据分析库、数据库等)之间高效地共享数据,避免了数据在不同格式之间转换所带来的开销。它就像是一种数据的 “通用语言”,使得数据能够在各种分析工具和环境中快速流动和处理。例如,在数据从存储系统(如数据库)读取到分析工具(如 Python 中的数据分析库)的过程中,Arrow 格式可以让数据的传输更加高效

主要特点

  • 列式存储:Arrow设计的内存格式特别适合于列式数据处理,使得在分析大型数据集时能够显著提高性能。
  • 零拷贝读取:Arrow支持在不同编程语言之间进行数据的零拷贝读取,这大大减少了数据在不同系统间传递时的开销。
  • 标准化的数据格式:Arrow定义了一种标准化的数据格式,使得不同的数据处理工具和库能够无缝地共享数据。
  • 广泛的生态系统支持:Arrow支持多种编程语言,包括C++、Java、Python、Go、R等,并且与许多流行的数据处理工具和库集成。
  • 高性能计算:Arrow优化了内存使用和CPU缓存,提供了快速的向量化和并行处理能力

优势

  1. 提高数据处理速度:通过减少数据序列化和反序列化的开销,Arrow可以显著提高数据处理的速度。
  2. 简化数据集成:由于支持多种语言和工具,Arrow简化了数据集成过程,使得在不同的系统和库之间移动数据变得更加容易。
  3. 促进工具和库的互操作性:Arrow作为一个桥梁,连接了各种数据处理工具和库,使得它们可以更容易地一起工作。
  4. 优化内存使用:列式存储和紧凑的数据布局有助于减少内存占用,提高内存使用效率。
  5. 社区支持:作为一个Apache软件基金会项目,Arrow拥有一个活跃的社区,不断推动项目向前发展。

应用场景

  • 数据交换和互操作性:在不同的数据分析系统之间进行数据交换。例如,一个用 Python 编写的数据分析脚本和一个用 Java 编写的大数据处理系统之间需要共享数据。使用 Apache Arrow 格式,可以方便地将数据从一个系统传输到另一个系统,而不用担心数据格式不兼容的问题。
  • 加速数据分析库:许多数据分析库(如 Pandas、PyArrow 等)都支持 Apache Arrow 格式。当这些库使用 Arrow 格式的数据时,可以利用其高效的内存布局和数据存储方式,加快数据读取、处理和计算的速度。例如,在处理大规模数据集时,使用 Arrow 格式可以显著减少数据加载和预处理的时间。
  • 大数据和分布式计算:在大数据处理环境中,如 Hadoop 和 Spark 生态系统中,Arrow 可以作为一种中间数据格式来提高数据在不同节点之间的传输效率。它有助于在分布式计算环境中优化数据的流动和共享,使得数据能够更快地在各个计算节点之间传递,从而加速整个大数据处理的流程

项目工程引入

<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-vector</artifactId>
<version>17.0.0</version>
</dependency>
<dependency>

<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-unsafe</artifactId>
<version>17.0.0</version>
<scope>runtime</scope>
</dependency>

关于arrow的内存简单介绍

arrow-memory-core

arrow-memory-core是Apache Arrow的核心内存管理模块。它提供了基础的内存分配和内存管理功能,是其他内存管理模块的基础。这个模块定义了内存分配器接口(MemoryAllocator)和内存管理的基本数据结构(如BufferArrowBuf等),用于在Arrow中创建和操作内存缓冲区。

主要功能包括:

  • 提供了基于Java NIO的BufferAllocator实现。
  • 定义了内存分配和回收的策略。
  • 实现了基础的内存缓冲区操作。

arrow-memory-unsafe

arrow-memory-unsafe模块提供了一个基于Java的sun.misc.Unsafe类实现的内存分配器。这个模块允许Apache Arrow直接在Java堆外分配内存,从而能够绕过JVM的垃圾回收机制,提高内存使用效率和性能。

主要特点包括:

  • 使用sun.misc.Unsafe进行堆外内存分配。
  • 减少了垃圾回收的开销。
  • 需要谨慎使用,因为不当的使用可能导致内存泄漏和难以调试的问题。

arrow-memory-netty

arrow-memory-netty模块则是基于Netty框架的内存管理实现。Netty是一个提供异步事件驱动的网络应用程序框架和工具,用于快速开发高性能、高可靠性的网络服务器和客户端程序。

这个模块的主要功能包括:

  • 利用Netty的内存管理能力来分配和回收内存。
  • 可以与Netty的其他组件更好地集成,例如在网络传输中使用Arrow格式。
  • 提供了池化的内存分配器,可以显著提高内存分配和回收的效率

arrow类型

类型分类

  1. 基本数据类型向量(Primitive Vectors)

    IntVector:用于存储 32 位整数数据。它是最基本的整数向量类型,适用于处理普通的整数量化信息,比如计数、简单的索引等。在内存中,它会为每个整数元素分配 4 个字节的存储空间。除了之前提到的常见方法,它还支持批量设置和获取值的操作,例如 setInts(int index, int[] values, int length) 可以从指定索引开始批量设置多个整数值,getInts(int index, int[] destination, int length) 可以从指定索引开始将多个整数值读取到目标数组中。

    注意:

    按照字节位数创建无符号向量UInt8Vector,UInt4Vector,UInt1Vector,UInt2Vector

    有符号向量SmallIntVector(16位),IntVector(32位),BigIntVector(64位),TinyIntVector(8位)

//无符号创建向量

BufferAllocator allocator= new RootAllocator(Long.MAX_VALUE);
final UInt8Vector uInt8Vector = new UInt8Vector("uint8", allocator);
final UInt4Vector uInt4Vector = new UInt4Vector("uint4", allocator);
final UInt2Vector uInt2Vector=new UInt2Vector("unit2", allocator);
final UInt1Vector uInt1Vector = new UInt1Vector("uint1", allocator);
//有符号创建向量
TinyIntVector tinyIntVector=new TinyIntVector("tinyIntVector", allocator);
SmallIntVector smallIntVector=new SmallIntVector("smallIntVector", allocator);
BigIntVector bigIntVector=new BigIntVector("bigIntVector", allocator);
  • FloatVector:存储单精度(32 位)浮点数。单精度浮点数能够在一定程度上表示小数,但精度相对较低。它适用于对精度要求不是特别高,但需要节省内存空间或者对性能有较高要求的场景,例如一些简单的科学计算、数据的初步分析等。其内部会按照 IEEE 754 标准的单精度浮点数格式来存储数据。
//半精度浮点数
  Float2Vector floatVector = new Float2Vector(EMPTY_SCHEMA_PATH, allocator)
  //单精度浮点数
  Float4Vector floatVector = new Float4Vector(EMPTY_SCHEMA_PATH, allocator)
  //双精度浮点数
  Float8Vector floatVector = new Float8Vector(EMPTY_SCHEMA_PATH, allocator)
  • LongVector:存储 64 位长整数数据。相较于 IntVector,它能表示的数值范围更大,适合处理需要更大整数范围的场景,比如处理非常大的计数、时间戳(精确到毫秒或微秒级别时可能需要使用长整型来存储)等。在内存布局上,每个长整数元素占用 8 个字节的空间。

  • DoubleVector:存储双精度(64 位)浮点数。双精度浮点数的精度更高,能够更准确地表示小数,适合对数值精度要求较高的科学计算、金融计算等场景,比如金融交易中的价格计算、科学研究中的精确数值模拟等。与 FloatVector 类似,它也是按照 IEEE 754 标准的双精度浮点数格式存储数据,但每个元素占用的内存空间是 8 个字节。

  • BooleanVector:存储布尔值(true 或 false)。这种向量类型在处理逻辑判断结果、标志位等场景非常有用,比如表示某个条件是否满足、某个开关是否打开等。在内存中,通常会使用一个比特位来表示一个布尔值,但为了方便操作和管理,Arrow 可能会将多个布尔值打包成一个字节或多个字节进行存储。

  1. 可变长度数据类型向量(Variable - Width Vectors)

    • VarCharVector:主要用于存储可变长度的字符数据(字符串)。它可以根据实际字符串的长度动态分配存储空间,适合存储长度不固定的文本信息,比如用户的姓名、地址、评论等。在存储时,Arrow 会将字符串的内容以字节数组的形式存储,并记录每个字符串的长度信息。对于频繁进行字符串操作的场景,使用 VarCharVector 可以提高内存利用率和操作效率,但在进行字符串比较等操作时,可能需要额外的处理。
try (
//创建一个根分配器来管理内存
BufferAllocator allocator=new RootAllocator();
//创建VarCharVector实例
VarCharVector varCharVector=new VarCharVector("stringvert", allocator)
){
//分配向量空间,假设我们想要存储3个字符串
varCharVector.allocateNew(3);
varCharVector.set(0, "one".getBytes());
varCharVector.set(1, "two".getBytes());
varCharVector.set(2, "three".getBytes());
//设置向量中的实际元素数量
varCharVector.setValueCount(3);
System.out.println("Vector created in memory: " + varCharVector);

}
  • BinaryVector:用于存储可变长度的二进制数据。与 VarCharVector 类似,它可以存储不同长度的二进制数据块,比如图片、音频、视频等文件的二进制内容,或者自定义的二进制格式数据。在存储方式上,也是将二进制数据以字节数组的形式存储,并记录数据的长度信息。在处理二进制数据时,BinaryVector 提供了方便的接口来进行数据的读写和操作。
  1. 复合数据类型向量(Composite Vectors)

    • StructVector:类似于编程语言中的结构体概念,它可以将多个不同类型的字段组合在一起形成一个复合结构。每个字段可以是基本数据类型向量或者其他复合数据类型向量。例如,可以创建一个 StructVector 来表示一个学生的信息,其中包含学生的姓名(VarCharVector)、年龄(IntVector)、成绩(DoubleVector 等)。通过 StructVector,可以方便地对一组相关的数据进行统一的管理和操作,在数据处理和分析中非常有用。
    • ListVector:用于存储列表类型的数据结构。它的元素可以是任意类型的向量,比如一个 ListVector 中可以包含多个 IntVector 作为列表的元素,或者包含 StructVector 等复杂类型的元素。这种向量类型适合处理嵌套的列表数据,比如存储一个班级中每个学生的成绩列表,或者一个文档中包含的多个段落等。在实现上,ListVector 会包含一个指向元素向量的引用,以及记录每个列表元素长度的信息,以便进行正确的访问和操作.

    注意

    ListViewVector: 逻辑视图,它引用了现有的ListVector或其他列表类型向量,可以避免数据复制,从而提高性能;

    FixedSizeListVector:用于存储固定长度列表的向量类型 .

// 创建一个根分配器来管理内存
try (BufferAllocator allocator = new RootAllocator()) {
// 创建一个ListVector
ListVector listVector = new ListVector("listVector", allocator, null, null);
// 定义列表中元素的类型并将元素类型添加到ListVector
/* Explicitly add the dataVector */
MinorType type = MinorType.INT;
listVector.addOrGetVector(FieldType.nullable(type.getType()));
// 分配向量空间
listVector.allocateNew();
// 使用UnionListWriter写入数据

UnionListWriter writer = listVector.getWriter();
writer.startList();
writer.writeInt(1);
writer.writeInt(2);
writer.writeInt(3);
writer.endList();
// 可以继续添加更多的列表
writer.startList();
writer.writeInt(4);
writer.writeInt(5);
writer.endList();
// 设置向量中的实际元素数量
listVector.setValueCount(2); // 我们添加了两个列表
// 清理ListVector
listVector.clear();
} catch (Exception e) {
e.printStackTrace();
}

  • MapVector(映射向量):用于存储键值对集合的数据结构向量,类似于编程语言中的MapDictionary。它可以有效地处理具有映射关系的数据,如存储用户 ID(键)和用户详细信息(值)的映射关系,或者产品代码(键)和产品属性(值)的映射关系

简单MapVector操作向量案例

int count = 5;
try (MapVector mapVector = MapVector.empty("map", allocator, false)) {
mapVector.allocateNew();
UnionMapWriter mapWriter = mapVector.getWriter();
for (int i = 0; i < count; i++) {
mapWriter.startMap();
for (int j = 0; j < i + 1; j++) {
mapWriter.startEntry();
mapWriter.key().bigInt().writeBigInt(j);
mapWriter.value().integer().writeInt(j);
mapWriter.endEntry();
}
mapWriter.endMap();
}
mapWriter.setValueCount(count);
UnionMapReader mapReader = mapVector.getReader();
for (int i = 0; i < count; i++) {
mapReader.setPosition(i);
for (int j = 0; j < i + 1; j++) {
mapReader.next();
Long keyLong=mapReader.key().readLong().longValue();
System.out.println();
assertEquals(j, mapReader.key().readLong().longValue(), "record: " + i);
assertEquals(j, mapReader.value().readInteger().intValue());
}
}

}
  1. 字典编码相关向量(Dictionary - Encoded Vectors)

    • DictionaryVector:这种向量类型涉及字典编码技术。它首先会构建一个字典,将数据集中出现的唯一值存储在字典中,并为每个唯一值分配一个唯一的索引。然后,原始数据中的值会被替换为对应的字典索引进行存储。这样在数据重复率较高的情况下,可以大大减少数据的存储空间,并且提高数据的处理效率,比如对于存储大量的字符串标签、枚举类型等数据非常有效。在使用 DictionaryVector 时,需要先构建字典,然后将数据转换为字典索引进行存储,在读取数据时,再根据字典索引将数据还原为原始值1。
    • DictionaryLookupVector:主要用于根据字典索引查找对应的实际值。它与 DictionaryVector 配合使用,当需要将字典编码后的数据还原为原始数据形式时,或者需要根据字典索引进行快速的数据查询和检索时,DictionaryLookupVector 会发挥作用。它提供了快速查找字典中对应值的方法,以便在数据处理过程中方便地进行数据的转换和查询1。
  2. 时间相关向量

    • TimeStampVector:存储时间戳数据,能够精确到秒、毫秒、微秒甚至更高的精度,具体的精度可以根据需求进行设置。时间戳可以是基于某个特定的时间基准(比如 Unix 时间戳,即从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间的秒数),也可以是基于其他自定义的时间基准。在实际应用中,TimeStampVector 常用于处理需要精确时间记录的场景,如日志分析、事件追踪、金融交易时间记录等。
    • DateVector:用于存储日期数据,精确到日。它可以方便地处理与日期相关的业务场景,比如记录生日、活动日期、合同日期等。与 TimeStampVector 相比,DateVector 只关注日期部分,不包含时间信息,因此在一些只需要处理日期信息的场景下更加简洁和高效。
    • DurationVector:存储时间间隔或持续时间数据。例如,可以用来表示两个时间点之间的时间差,或者某个事件持续的时间长度。在存储时,会根据指定的时间单位(如秒、分钟、小时等)来表示时间间隔,以便进行时间间隔的计算和比较。
  3. 高精度数值向量

    • DecimalVector:用于存储高精度的十进制数据。在金融和会计领域等对数值精度要求极高的场景中应用广泛,因为这些领域需要精确地处理货币金额、财务报表数据等。DecimalVector 可以指定小数点后的位数,从而保证数值的精度和准确性,避免浮点数运算可能带来的精度损失。它在内存中的存储方式会根据指定的精度和数值范围进行优化,以提高存储效率和计算性能。
    • Decimal256Vector:是一种更高精度的十进制向量类型,相比于 DecimalVector,它可以支持更大范围和更高精度的十进制数值计算。适用于对数值精度要求非常高,并且数值范围较大的场景,比如高精度的科学计算、复杂的金融模型计算等。
  4. 特殊用途向量

    • BitVector:专门用于存储位向量数据,即每个元素都是一个比特位。可以用于表示二进制标志、位掩码等数据,比如可以用一个 BitVector 来表示一组权限的开关,每个比特位代表一种权限是否开启。在存储和操作上,BitVector 会对比特位进行高效的管理和操作,以便节省内存空间并提高处理效率。
    • NullVector:用于表示数据中的空值。在实际数据处理中,经常会遇到某些位置的数据缺失的情况,NullVector 可以与其他向量类型结合使用,记录哪些位置的数据是缺失的。它会维护一个有效性位图(validity bitmap),其中的每个比特位对应一个数据元素,如果该比特位为 0,表示对应的数据元素为 null;如果为 1,表示数据元素有效。通过 NullVector,可以在数据处理过程中正确地处理空值,避免因空值导致的计算错误或异常

列式向量数据排序

基本类型排序IntVectorLongVectorFloatVectorDoubleVector等基本数据类型向量

第一种 排序结果返回原有向量


BufferAllocator allocator = new RootAllocator(1024 * 1024);
try (IntVector vec = new IntVector("", allocator)) {
vec.allocateNew(10);
vec.setValueCount(10);
// 填写数据
vec.set(0, 10);
vec.set(1, 8);
vec.setNull(2);
vec.set(3, 10);
vec.set(4, 12);
vec.set(5, 17);
vec.setNull(6);
vec.set(7, 23);
vec.set(8, 35);
vec.set(9, 2);

// 数据排序
FixedWidthInPlaceVectorSorter sorter = new FixedWidthInPlaceVectorSorter();
VectorValueComparator<IntVector> comparator =
DefaultVectorComparators.createDefaultComparator(vec);
//在原有数据向量修改成排序结果
sorter.sortInPlace(vec, comparator);
System.out.println(vec);

第二种 排序返回新的向量结果

//创建向量源数据
try (IntVector vec = new IntVector("", allocator)) {
ValueVectorDataPopulator.setVector(
vec, 0, 1, 2, 3, 4, 5, 30, 31, 32, 33, 34, 35, 60, 61, 62, 63, 64, 65, 6, 7, 8, 9, 10, 11,
36, 37, 38, 39, 40, 41, 66, 67, 68, 69, 70, 71);
// 向量排序
OutOfPlaceVectorSorter<IntVector> sorter = new GeneralOutOfPlaceVectorSorter<>();
VectorValueComparator<IntVector> comparator =
DefaultVectorComparators.createDefaultComparator(vec);
//创建排序后的向量
try (IntVector sortedVec =
(IntVector) vec.getField().getFieldType().createNewSingleVector("", allocator, null)) {
sortedVec.allocateNew(vec.getValueCount());
sortedVec.setValueCount(vec.getValueCount());
sorter.sortOutOfPlace(vec, sortedVec, comparator);
// 验证结果
int[] actual = new int[sortedVec.getValueCount()];
IntStream.range(0, sortedVec.getValueCount()).forEach(i -> actual[i] = sortedVec.get(i));

assertArrayEquals(
new int[] {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71
},
actual);
}
}

针对复合类型数据结构排序

简单案例 StructVector

//创建pojo
record Person(String name,int age) {
}
public void testPersonSort(List<Person> people) {
//创建根分配器
RootAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
StructVector root=StructVector.empty("start", allocator);
root.addOrGet("age", FieldType.nullable(new ArrowType.Int(32, true)), IntVector.class);
root.addOrGet("name", FieldType.nullable(new ArrowType.Utf8()), VarCharVector.class);
//排序后向量创建
StructVector root1=StructVector.empty("start2", allocator);
root1.addOrGet("age", FieldType.nullable(new ArrowType.Int(32, true)), IntVector.class);
root1.addOrGet("name", FieldType.nullable(new ArrowType.Utf8()), VarCharVector.class);
// 获取向量
VarCharVector nameVector = (VarCharVector) root.getChild("name");
IntVector ageVector = (IntVector) root.getChild("age");
// 填充数据
try {
root.allocateNew();
root.setInitialCapacity(people.size());
for (int i = 0; i < people.size(); i++) {
Person person = people.get(i);
ageVector.set(i, person.age());
nameVector.set(i, person.name().getBytes());

root.setIndexDefined(i);
}
root.setValueCount(people.size());
root1.allocateNew();
root1.setInitialCapacity(people.size());
root1.setValueCount(people.size());
VectorValueComparator<IntVector> intComparator=DefaultVectorComparators.createDefaultComparator(root.getChild("age", IntVector.class));

intComparator.attachVector(root.getChild("age", IntVector.class));
VectorValueComparator< VarCharVector> nameComparator=DefaultVectorComparators.createDefaultComparator(root.getChild("name", VarCharVector.class));
nameComparator.attachVector(root.getChild("name", VarCharVector.class));
//创建structVector的排序器 先比较age 然后比较name
VectorValueComparator<StructVector> structComparator=new VectorValueComparator<StructVector>() {
@Override
public VectorValueComparator<StructVector> createNew() {
return this;
}
@Override
public int compareNotNull(int index1, int index2) {
//先比较年龄,然后比较姓名,由小到大排序规则排序
int result= intComparator.compare(index1, index2);
System.out.println("i"+index1+"---"+index2);
if(result!=0) {
return result;
}
return nameComparator.compare(index1, index2);
}
};
GeneralOutOfPlaceVectorSorter<StructVector> generalOutOfPlaceVectorSorter=new GeneralOutOfPlaceVectorSorter<>();
generalOutOfPlaceVectorSorter.sortOutOfPlace(root, root1, structComparator);
toStringStructVector(root1);
} catch (Exception e) {e.printStackTrace();
}
}
}
public void baseBaseCharVector() {
Random random=new Random();
List<Person> persons=new ArrayList<>();
for (int i = 19; i >0; i--) {
int data=random.nextInt(1000);
System.out.println(data+"a"+":"+i);
persons.add(new Person(data+"a", i));
}

try {
testPersonSort(persons);
} catch (Exception e) {
e.printStackTrace();
}
}

排序结果如下: image.png