3. 静态数据源设计:三种主流接入方式
数据源设计,说白了就是解决一个问题——你的数据到底从哪来?
我做了这么多年数据流架构,见过太多项目在数据接入阶段就翻车了。有的是 CSV 文件编码不对,有的是 JDBC 连接池爆了,还有的是 API 限流没处理好。嗯,今天咱们就把这三种最常用的数据源掰开揉碎讲清楚。
核心观点:静态数据源的设计原则是「一次读取,多次复用」。数据在启动时加载到内存,后续计算不再依赖外部存储。
3.1 文件系统数据源:CSV / JSON / Parquet
文件系统是最直观的数据源。你想想看,大部分团队最开始都是拿 CSV 或者 JSON 文件做实验的。但这里有个坑——文件格式的选择直接影响性能和开发效率。
3.1.1 CSV 文件读取
CSV 看起来简单,实际上坑最多。我记得有一次项目,CSV 文件里某个字段包含了逗号,结果解析直接错位,整张表都乱了。
我个人习惯用 Apache Commons CSV 或者 OpenCSV,它们能处理带引号的字段。核心代码长这样:
// 使用 Apache Commons CSV 读取
Reader reader = Files.newBufferedReader(Paths.get("data.csv"));
Iterable<CSVRecord> records = CSVFormat.DEFAULT
.withFirstRecordAsHeader()
.withTrim()
.parse(reader);
for (CSVRecord record : records) {
String id = record.get("id");
String name = record.get("name");
// 映射到业务对象
}
小技巧:CSV 文件一定要做 Schema 推断。我建议先读前 100 行判断字段类型,避免全量扫描。另外,文件编码统一用 UTF-8,BOM 头记得去掉。
3.1.2 JSON 文件读取
JSON 比 CSV 灵活,但体积大。静态数据场景下,我推荐用 Jackson 或者 Gson 做反序列化。注意一点——如果 JSON 文件很大(超过 100MB),别一次性读到内存里。
// Jackson 流式读取大 JSON
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(new File("data.json"));
// 跳过外层数组
parser.nextToken();
while (parser.nextToken() == JsonToken.START_OBJECT) {
MyData data = objectMapper.readValue(parser, MyData.class);
// 逐条处理
}
3.1.3 Parquet 文件读取
Parquet 是列式存储格式,压缩率高,查询快。我在做大数据项目时特别喜欢用它。但要注意——Parquet 的 Schema 是自描述的,读取时不需要额外定义。
// 使用 Apache Parquet 读取
ParquetReader<GenericRecord> reader = AvroParquetReader
.<GenericRecord>builder(new Path("data.parquet"))
.build();
GenericRecord record;
while ((record = reader.read()) != null) {
// 直接按字段名访问
System.out.println(record.get("name"));
}
| 格式 | 压缩率 | 读取速度 | Schema 支持 | 适用场景 |
|---|---|---|---|---|
| CSV | 低 | 中 | 弱(需推断) | 小数据量、快速验证 |
| JSON | 中 | 中 | 中(嵌套结构) | API 对接、配置数据 |
| Parquet | 高 | 快 | 强(自描述) | 大数据量、分析场景 |
避坑指南:我曾经在项目中直接用 CSV 做生产数据源,结果字段类型推断错误导致数值精度丢失。后来我强制要求所有文件数据源必须显式声明 Schema,哪怕 CSV 也要配一个 schema.json。
3.2 数据库数据源:JDBC 连接
数据库作为静态数据源,核心思路是「一次性全量拉取」。你想想看,如果每次计算都去查数据库,那就不叫静态数据流了。
我个人习惯在应用启动时,通过 JDBC 把配置表、字典表、维度表全部加载到内存。这里有几个关键点:
- 连接池管理:用 HikariCP 或者 Druid,别自己 new Connection
- 查询优化:只查需要的列,加 WHERE 条件过滤
- 结果集映射:用 RowMapper 或者 ORM 框架
// HikariCP 配置示例
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setConnectionTimeout(5000);
HikariDataSource dataSource = new HikariDataSource(config);
// 全量加载
String sql = "SELECT id, name, status FROM dim_product WHERE is_active = 1";
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
List<Product> products = jdbc.query(sql, (rs, rowNum) -> {
Product p = new Product();
p.setId(rs.getLong("id"));
p.setName(rs.getString("name"));
p.setStatus(rs.getInt("status"));
return p;
});
经验之谈:数据库数据源一定要做「增量更新」的预案。虽然叫静态数据源,但数据不可能永远不变。我一般会加一个版本号字段,启动时对比版本号决定是否重新加载。
3.3 API 数据源:REST / gRPC
API 数据源是最灵活的,但也是最不稳定的。你想想看,网络延迟、服务限流、数据格式变化,任何一个环节出问题都会导致数据加载失败。
我记得有一次对接第三方 API,对方突然改了返回字段名,我们的解析器直接崩了。从那以后,我强制要求所有 API 数据源必须做「Schema 校验」和「熔断降级」。
3.3.1 REST API 调用
// 使用 OkHttp + Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build())
.build();
ApiService service = retrofit.create(ApiService.class);
Response<List<MyData>> response = service.getData().execute();
if (response.isSuccessful()) {
List<MyData> data = response.body();
// 缓存到内存
}
3.3.2 gRPC 调用
gRPC 性能更好,适合内部服务间调用。但静态数据场景下,我建议用「一次性 Unary 调用」,别开流式。
// gRPC 阻塞调用
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 9090)
.usePlaintext()
.build();
DataServiceGrpc.DataServiceBlockingStub stub =
DataServiceGrpc.newBlockingStub(channel);
DataRequest request = DataRequest.newBuilder()
.setDataType("product")
.setVersion("latest")
.build();
DataResponse response = stub.getStaticData(request);
// 处理响应数据
| 特性 | REST API | gRPC |
|---|---|---|
| 协议 | HTTP/1.1 + JSON | HTTP/2 + Protobuf |
| 性能 | 中等 | 高(二进制序列化) |
| Schema 定义 | 无强制要求 | 强类型 .proto 文件 |
| 适用场景 | 外部系统对接 | 内部微服务 |
避坑指南:我曾经遇到 API 数据源在凌晨 3 点超时,导致整个应用启动失败。后来我加了「降级策略」——如果 API 调用失败,从本地缓存文件加载上一次的数据。虽然数据可能不是最新的,但至少系统能跑起来。
3.4 统一抽象:DataSource 接口
三种数据源虽然接入方式不同,但最终都要统一成一个接口。我个人习惯这样设计:
public interface DataSource<T> {
/**
* 加载数据,返回不可变集合
*/
List<T> load();
/**
* 获取数据版本号
*/
String getVersion();
/**
* 刷新数据(增量更新用)
*/
default void refresh() {
// 默认不实现
}
}
这样上层计算逻辑只需要依赖 DataSource 接口,不用关心底层是文件、数据库还是 API。你想想看,如果哪天要把 CSV 换成数据库,只需要换一个实现类,业务代码一行都不用改。
总结一下:静态数据源设计的核心是「统一抽象 + 按需加载」。文件系统适合小数据量快速验证,数据库适合结构化维度数据,API 适合动态更新的配置数据。选型时考虑数据量、更新频率和稳定性,别盲目追求技术栈。