模型持久化(一):Java 将训练好的模型序列化,存入 KingbaseES 二进制字段

——别再把模型扔在文件系统里了,你的 AI 能力值得一个“家”

大家好,我是那个总在半夜被叫醒、因为线上模型版本和测试环境对不上,又不得不翻遍 NFS 目录找 .model 文件的老架构。你可能已经用 Java 手写了随机森林,跑出了漂亮的 AUC,甚至画出了 ROC 曲线。

但当你把模型对象 RandomForest rf = new RandomForest(...) 训练完后,下一步该放哪儿?

  • 放本地磁盘?→ 容器一重启就没了;
  • 放共享存储?→ 权限混乱、版本打架、审计困难;
  • 放 Git?→ 二进制文件根本没法 diff,还污染代码库。

真正的工程化 AI,必须把模型当作一等公民——有版本、可追溯、高可用、强一致

今天我们就干一件事:用 Java 把训练好的模型序列化成字节流,直接存入电科金仓 KingbaseES(KES)的 BLOB 字段。全程不依赖外部存储,不搞复杂注册中心,只为回答那个灵魂拷问:

“你的模型,到底是不是系统的一部分?”


一、为什么模型要进数据库?

在国产化项目中,我们常把模型和数据割裂:

  • 数据在 KES 里,模型在 MinIO 里,配置在 Nacos 里……
  • 一次上线要改三处,出问题要查三个系统。

KES 作为企业级融合数据库,天然支持结构化 + 非结构化数据
它的 BYTEA 类型(即 BLOB)就是为存储二进制对象设计的——包括模型。

✅ 模型进库的好处:

  • 原子性:模型与元数据(如训练时间、AUC、特征列表)同事务提交;
  • 一致性:避免“数据新、模型旧”的错配;
  • 安全性:复用 KES 的权限体系、加密、审计日志;
  • 可运维:通过 SQL 查询、备份、恢复,无需额外工具链。

二、Java 实现:让模型可序列化

首先,确保你的模型类实现 Serializable

import java.io.Serializable;

public class RandomForest implements Serializable {
    private static final long serialVersionUID = 1L; // 关键!固定版本ID
    
    private final List<DecisionTree> trees;
    private final int numTrees;
    private final Set<String> featureNames;
    
    // 构造函数、训练、预测方法略...
}

⚠️ 注意:

  • 所有成员变量也必须可序列化;
  • 避免存储 ConnectionLogger 等瞬态对象;
  • transient 标记非必要字段(如临时缓存)。

三、将模型转为字节数组

public static byte[] serializeModel(Serializable model) {
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
         ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(model);
        return baos.toByteArray();
    } catch (IOException e) {
        throw new RuntimeException("Failed to serialize model", e);
    }
}

反序列化:

@SuppressWarnings("unchecked")
public static <T extends Serializable> T deserializeModel(byte[] bytes, Class<T> clazz) {
    try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
         ObjectInputStream ois = new ObjectInputStream(bais)) {
        Object obj = ois.readObject();
        if (!clazz.isInstance(obj)) {
            throw new ClassCastException("Deserialized object is not of type " + clazz.getName());
        }
        return (T) obj;
    } catch (Exception e) {
        throw new RuntimeException("Failed to deserialize model", e);
    }
}

四、KES 表设计:为模型建“档案”

CREATE SCHEMA IF NOT EXISTS ai_models;

CREATE TABLE ai_models.model_registry (
    model_id        SERIAL PRIMARY KEY,
    model_name      VARCHAR(100) NOT NULL,      -- 如 'loan_risk_rf_v3'
    model_type      VARCHAR(50)  NOT NULL,      -- 'RandomForest', 'XGBoost'...
    version         VARCHAR(20)  NOT NULL,      -- 语义化版本
    feature_list    TEXT[],                     -- 特征名数组
    auc_score       REAL,
    training_time   TIMESTAMP DEFAULT NOW(),
    model_blob      BYTEA NOT NULL,             -- ← 模型本体
    created_by      VARCHAR(50),
    is_active       BOOLEAN DEFAULT false       -- 是否当前线上版本
);

💡 BYTEA 是 KES 对二进制大对象的标准支持,最大可达 1GB,足够存下千棵树的森林。


五、Java 存取模型:通过 JDBC 操作 BLOB

5.1 保存模型到 KES

public void saveModelToKES(Connection conn, String name, String version, 
                          RandomForest model, List<String> features, double auc) 
        throws SQLException {
    String sql = """
        INSERT INTO ai_models.model_registry 
        (model_name, model_type, version, feature_list, auc_score, model_blob, created_by)
        VALUES (?, ?, ?, ?, ?, ?, ?)
        """;
    
    byte[] modelBytes = serializeModel(model);
    Array featureArray = conn.createArrayOf("TEXT", features.toArray());
    
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, name);
        ps.setString(2, "RandomForest");
        ps.setString(3, version);
        ps.setArray(4, featureArray);
        ps.setDouble(5, auc);
        ps.setBytes(6, modelBytes); // ← 直接写入 BYTEA
        ps.setString(7, System.getProperty("user.name"));
        ps.executeUpdate();
    }
}

5.2 从 KES 加载模型

public RandomForest loadActiveModelFromKES(Connection conn, String modelName) 
        throws SQLException {
    String sql = """
        SELECT model_blob, feature_list 
        FROM ai_models.model_registry 
        WHERE model_name = ? AND is_active = true
        ORDER BY model_id DESC LIMIT 1
        """;
    
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, modelName);
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                byte[] modelBytes = rs.getBytes("model_blob");
                // 可选:校验特征列表是否匹配当前数据
                return deserializeModel(modelBytes, RandomForest.class);
            }
        }
    }
    throw new RuntimeException("Active model not found: " + modelName);
}

🔗 使用 电科金仓 JDBC 驱动 确保 BYTEA 正确映射为 byte[]


六、实战:端到端流程

// 1. 训练模型
RandomForest rf = new RandomForest(100, 10, 10);
rf.train(trainData, featureSet);

// 2. 评估
double auc = evaluate(rf, testData).auc;

// 3. 保存到 KES
saveModelToKES(conn, "loan_risk_rf", "v1.2.0", rf, 
               new ArrayList<>(featureSet), auc);

// 4. 上线(标记为 active)
markModelAsActive(conn, "loan_risk_rf", "v1.2.0");

// 5. 线上服务加载
RandomForest onlineModel = loadActiveModelFromKES(conn, "loan_risk_rf");
boolean risk = onlineModel.predict(userFeatures);

✅ 整个过程无文件、无网络依赖、全在事务内完成


七、为什么这适合国产化场景?

  1. 自主可控:模型存储不依赖 HDFS/S3/MinIO 等外部组件;
  2. 安全合规:复用电科金仓已有的等保、密评、审计能力;
  3. 简化架构:减少中间件,降低运维复杂度;
  4. 高可用:KES 本身支持 RAC、主备、两地三中心,模型自动高可用。

而这套能力,正建立在 电科金仓 KES 提供的企业级数据库引擎之上——它不仅是数据的仓库,更是 AI 能力的载体。


结语:模型,是数据的延伸

在传统观念里,数据库只存“原始数据”。
但在 AI 时代,模型是数据的结晶,是知识的载体,理应享有同等地位

当你能把训练好的随机森林,像一条业务记录一样 INSERT 进 KES,并通过 SELECT 在毫秒内加载上线——你就真正实现了 “AI 与数据同生共长”

因为你知道:最好的模型管理,不是另起炉灶,而是融入现有数据治理体系

—— 一位相信“模型,不该流浪”的架构师

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐