当你的Java应用还在“啃”CSV时,聪明人已经用Parquet “喝”数据了
各位Java开发同胞们,大家好! 今天咱们不聊虚无缥缈的架构,也不卷那些花里胡哨的新框架。咱们来聊聊一个 “看着平平无奇,实则内功深厚” 的文件格式——Apache Parquet。
如果你现在的项目还在用 CSV 存海量数据,每次读取都像在等Windows 98开机一样漫长,那这篇博客就是为你量身定做的“急救包”。
一、 什么是 Parquet?别再把它当成普通文件了
简单来说,Parquet 是一种为 Hadoop 生态圈设计的列存式(Columnar)存储格式。
我知道,听到“Hadoop”有些同学已经开始关网页了,别急!这玩意儿早就出圈了,现在是大数据的通用货币。
行存 vs 列存:一场关于吃面的战争 🍜
为了让你秒懂,咱们举个栗子🌰:
假设你有一张用户表:ID, Name, Age, Email, Address。
- CSV(行存) :就像一碗杂酱面。所有的数据(面条、菜码、炸酱)都拌在一起。你想吃一口黄瓜丝(只查Address字段),对不起,你得把这勺面全吃下去(扫描整行),再把剩下的吐出来(过滤其他字段)。IO 开销极大,数据库表示很累。
- Parquet(列存) :就像回转寿司。ID是一条传送带,Name是一条传送带。你想看所有人的Email?直接去Email那条传送带拿就行了,不用碰其他数据。而且因为同一列数据类型一致,压缩率极高(通常能比CSV小3-5倍)。
结论: Parquet = 省空间 + 省IO + 速度快。
二、 什么时候该用 Parquet?(应用场景)
- 数据分析(OLAP) :如果你经常写
SELECT COUNT(*)、SUM、GROUP BY,Parquet 是你的神。 - 数据仓库(Data Lake) :存原始日志、埋点数据。几百G的日志文件?用 Parquet 压缩一下,老板看了都说省钱(存储费)。💸
- ETL 中间结果:数据管道中,不同阶段的数据交换格式。
- Java 微服务里的批量导出:别再导出Excel把内存撑爆了,试试 Parquet。
三、 Java 落地实战:如何优雅地“盘”它?
好了,干货来了。在 Java 里操作 Parquet,主要有两种方式: “原装硬核派” (直接使用 Parquet libs)和 “工具人躺平派” (使用 Apache Arrow 或 Spark,但在 JVM 里运行)。
对于大多数后台开发,直接使用 Parquet-MR (parquet-avro/parquet-hadoop) 是最直接的。
1. 依赖准备:先把锅烧热 🍳
在你的 pom.xml里加上这些佐料:
<dependencies>
<!-- Parquet 核心 -->
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-hadoop</artifactId>
<version>1.13.1</version> <!-- 版本号请检查最新 -->
</dependency>
<!-- 我们需要 Avro 来定义 Schema,因为它最方便 -->
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-avro</artifactId>
<version>1.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.3</version>
</dependency>
</dependencies>
2. 定义 Schema:画图纸 📐
Parquet 是强 Schema 的。不像 CSV 那样“乱来”,你必须先定义数据结构。我们用 Avro 来定义:
{
"namespace": "com.example.parquet",
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "age", "type": ["int", "null"]}, // 允许 null
{"name": "email", "type": "string"}
]
}
3. 写入 Parquet:造数据 🏭
下面这段代码展示了如何把 Java 对象写成 Parquet 文件。
import org.apache.parquet.column.ParquetProperties;
import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.parquet.avro.AvroParquetWriter;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import java.io.IOException;
import java.nio.file.Paths;
public class ParquetWriteExample {
public static void main(String[] args) throws IOException {
// 1. 加载 Schema
Schema schema = new Schema.Parser().parse(
ParquetWriteExample.class.getResourceAsStream("/user.avsc")
);
String filePath = "users.parquet";
// 2. 创建 Writer (Builder模式,很现代)
try (ParquetWriter<GenericRecord> writer = AvroParquetWriter
.<GenericRecord>builder(Paths.get(filePath))
.withSchema(schema)
.withCompressionCodec(CompressionCodecName.SNAPPY) // 必选Snappy,快且稳
.withRowGroupSize(ParquetProperties.DEFAULT_BLOCK_SIZE)
.build()) {
// 3. 写入数据
for (int i = 0; i < 1000; i++) {
GenericRecord user = new GenericData.Record(schema);
user.put("id", i);
user.put("name", "User_" + i);
user.put("age", i % 10 == 0 ? null : 20 + i % 30); // 模拟空值
user.put("email", "user" + i + "@example.com");
writer.write(user);
}
}
System.out.println("✅ Parquet 文件写入完成!快去看看文件大小吧。");
}
}
4. 读取 Parquet:吃数据 🍽️
读取更有趣,特别是投影下推(Projection Pushdown) ,这是 Parquet 的灵魂!
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.avro.generic.GenericRecord;
import java.io.IOException;
import java.nio.file.Paths;
public class ParquetReadExample {
public static void main(String[] args) throws IOException {
String filePath = "users.parquet";
// 注意:这里我们没有指定 Schema,它会自动读取文件自带的 Schema
try (ParquetReader<GenericRecord> reader =
AvroParquetReader.<GenericRecord>builder(Paths.get(filePath)).build()) {
GenericRecord record;
int count = 0;
while ((record = reader.read()) != null) {
// 假设我们只关心 name 和 email
// 即使文件里有 id 和 age,只要你不 get,底层 IO 就不会读那一列!
String name = record.get("name").toString();
String email = record.get("email").toString();
if (count++ < 10) { // 只打印前10个
System.out.println("👤 Name: " + name + ", 📧 Email: " + email);
}
}
System.out.println("✅ 总共读取了 " + count + " 条记录。");
}
}
}
四、 避坑指南(血泪史)⚠️
-
Schema Evolution(模式演化) :
- 可以:加字段(给默认值)、删字段。
- 不可以:改字段类型(比如 Int 变 String)。这会让你的 Reader 直接报错给你看。😡
-
小文件问题:
- Parquet 是为大文件设计的。如果你生成了成千上万个几KB的小 Parquet 文件,NameNode(如果用HDFS)会爆炸,读取效率也会极低。合并小文件!
-
Java 内存:
- 虽然 Parquet 省 IO,但如果你一次性
read()太多数据到内存,OOM(内存溢出)还是会找上门的。建议配合流式处理(Streaming)。
- 虽然 Parquet 省 IO,但如果你一次性
五、 总结
| 特性 | CSV | Parquet |
|---|---|---|
| 体积 | 🐘 巨大 | 🐦 小巧 (压缩率高) |
| 读取速度 | 🐢 慢 (全量扫描) | 🚀 快 (列裁剪) |
| Schema | ❌ 无 (全靠猜) | ✅ 强类型 (自带元数据) |
| 适用场景 | Excel打开看看 | 大数据分析、存储 |
一句话总结:
如果你的数据量超过 10万行,或者你需要频繁按列查询,立刻、马上、现在就把 CSV 换成 Parquet。
别让你的 Java 程序跑得像个老爷车,给它换上 Parquet 这个涡轮增压引擎吧!🏎️💨
有问题欢迎在评论区留言,咱们下期见!👋
六、Java Parquet 工具类
1、设计目标
| 能力 | 说明 |
|---|---|
| ✅ 读写分离 | 写 / 读 / 投影读取解耦 |
| ✅ 泛型支持 | 支持 POJO ↔ Parquet |
| ✅ Schema 管理 | Avro Schema 自动生成 |
| ✅ 压缩策略 | Snappy / Gzip 可选 |
| ✅ 流式读取 | 防止 OOM |
| ✅ 可扩展 | 支持自定义 Converter |
2、Maven 依赖(稳定组合)
<dependencies>
<!-- Parquet -->
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-hadoop</artifactId>
<version>1.13.1</version>
</dependency>
<!-- Avro(Schema & 泛型) -->
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-avro</artifactId>
<version>1.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.3</version>
</dependency>
</dependencies>
3、核心工具类:ParquetUtils
1️⃣ 工具类入口
package com.example.parquet;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.reflect.ReflectData;
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.parquet.avro.AvroParquetWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* ✅ 生产级 Parquet 工具类
*/
public final class ParquetUtils {
private ParquetUtils() {}
/* ========================= 写 ========================= */
public static <T> void write(
Path path,
Iterable<T> data,
Class<T> clazz,
CompressionCodecName codec
) throws IOException {
Schema schema = ReflectData.get().getSchema(clazz);
try (ParquetWriter<GenericRecord> writer = AvroParquetWriter
.<GenericRecord>builder(path)
.withSchema(schema)
.withCompressionCodec(codec)
.withPageSize(4096)
.withRowGroupSize(128 * 1024 * 1024)
.build()) {
for (T item : data) {
writer.write(AvroConverter.toRecord(item, schema));
}
}
}
/* ========================= 读 ========================= */
public static <T> List<T> readAll(
Path path,
Class<T> clazz,
Function<GenericRecord, T> converter
) throws IOException {
List<T> result = new ArrayList<>();
try (ParquetReader<GenericRecord> reader =
AvroParquetReader.<GenericRecord>builder(path).build()) {
GenericRecord record;
while ((record = reader.read()) != null) {
result.add(converter.apply(record));
}
}
return result;
}
/* ========================= Stream ========================= */
public static <T> Stream<T> stream(
Path path,
Function<GenericRecord, T> converter
) throws IOException {
ParquetReader<GenericRecord> reader =
AvroParquetReader.<GenericRecord>builder(path).build();
Iterator<T> it = new Iterator<>() {
private GenericRecord next;
@Override
public boolean hasNext() {
try {
next = reader.read();
if (next == null) {
reader.close();
return false;
}
return true;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public T next() {
return converter.apply(next);
}
};
return StreamSupport.stream(
((Iterable<T>) () -> it).spliterator(), false
);
}
}
4、POJO ↔ Avro 转换器(关键点)
final class AvroConverter {
static <T> GenericRecord toRecord(T obj, Schema schema) {
return new org.apache.avro.generic.GenericData.Record(obj, schema);
}
}
✅ 推荐:生产环境建议使用 Avro Generated Class 或 MapStruct 做显式映射,避免反射。
5、示例 POJO
package com.example.model;
public class User {
private int id;
private String name;
private Integer age;
private String email;
// getter / setter / constructor
}
6、使用示例(真实业务)
✅ 写 Parquet
List<User> users = userService.loadUsers();
ParquetUtils.write(
Path.of("/data/users.parquet"),
users,
User.class,
CompressionCodecName.SNAPPY
);
✅ 读 Parquet(投影)
List<String> emails = ParquetUtils.readAll(
Path.of("/data/users.parquet"),
User.class,
record -> record.get("email").toString()
);
✅ 流式读取(防 OOM)
try (Stream<String> stream = ParquetUtils.stream(
Path.of("/data/users.parquet"),
r -> r.get("name").toString()
)) {
stream.limit(1000).forEach(System.out::println);
}
7、生产增强建议(非常重要 ⚠️)
| 场景 | 建议 |
|---|---|
| 大文件 | RowGroup ≥ 128MB |
| 并发写 | 每个线程一个文件 |
| Schema 演进 | 禁止删字段 / 改类型 |
| 监控 | 记录写入行数 & 文件大小 |
| 校验 | 写完后校验 footer |
| 云存储 | 支持 HDFS / S3A |