3. 静态数据源设计:三种主流接入方式

数据源设计,说白了就是解决一个问题——你的数据到底从哪来?

我做了这么多年数据流架构,见过太多项目在数据接入阶段就翻车了。有的是 CSV 文件编码不对,有的是 JDBC 连接池爆了,还有的是 API 限流没处理好。嗯,今天咱们就把这三种最常用的数据源掰开揉碎讲清楚。

核心观点:静态数据源的设计原则是「一次读取,多次复用」。数据在启动时加载到内存,后续计算不再依赖外部存储。

静态数据源接入架构图 静态数据源接入架构 静态数据引擎 启动时加载 · 内存常驻 文件系统数据源 CSV / JSON / Parquet 本地文件 / HDFS / S3 格式解析器 + Schema推断 数据库数据源 JDBC 连接 连接池管理 SQL 查询 + 结果集映射 API 数据源 REST / gRPC HTTP 客户端 + 重试 JSON 反序列化 三种数据源统一抽象为 DataSource 接口 上层计算逻辑无需关心数据来源 数据加载 Schema 校验 内存缓存

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 适合动态更新的配置数据。选型时考虑数据量、更新频率和稳定性,别盲目追求技术栈。