引言 在企业数字化转型和信创替代的浪潮中,越来越多的组织选择将传统商业数据库替换为国产数据库产品。电科金仓(KingbaseES)作为国内领先的 关系型数据库 管理系统,在政府、金融、能源等关键领域得到广泛应用。然而,数据库迁移并非简单的”搬家”过程,尤其是在处理大对象(LOB)数据时,跨平台传输中的完整性与一致性风险不容忽视。本文将深入探讨在迁移至金仓数据库过程中,BLOB/CLOB等大对象数据可能面临的挑战及应对策略。
一、大对象数据迁移的核心挑战 1.1 什么是大对象数据? 大对象(Large Object, LOB)是指那些无法在常规SQL表中直接 存储 的大容量数据,包括:
BLOB(Binary Large Object) :二进制大对象,用于存储图像、音频、视频等二进制数据CLOB(Character Large Object) :字符大对象,用于存储大文本、XML文档等字符数据在企业应用中,这 类 数据广泛存在于:
文档管理系统(合同、报告的扫描件) 多媒体内容平台(图片、视频资源) 日志系统(大容量文本日志) 医疗影像系统(CT、MRI图像数据) 1.2 迁移过程中的典型风险 风险1:数据损坏与截断 场景描述 :某金融机构在从 Oracle 迁移至金仓数据库时,发现部分客户合同扫描件(BLOB数据)在传输后无法正常打开,文件大小明显小于原始数据。
根本原因 :
字符编码不匹配导致二进制数据被错误解析 传输过程中使用了不支持大数据量的中间件 网络传输超时导致数据包丢失 目标数据库的LOB存储参数配置不当 风险2:兼容模式差异引发的数据丢失 场景描述 :某政务 系统 迁移时,发现原Oracle数据库中的CLOB字段数据在金仓PG兼容模式下无法完整写入。
根本原因 : 金仓数据库提供两种兼容模式,其LOB处理机制存在显著差异:
特性 Oracle兼容模式 PG兼容模式 存储方式 直接存储BLOB/CLOB类型 统一存储为OID类型,实际数据在系统表中 JDBC接口 Blob/Clob接口完全实现 Clob写入接口未实现,仅支持读取 更新操作 支持setString、setCharacterStream等 仅能通过Blob接口更新
实际影响 :如果迁移脚本未考虑兼容模式差异,在PG模式下使用Clob的写入方法将导致操作失败。
风险3:大对象引用完整性破坏 场景描述 :迁移后应用系统报错”找不到大对象数据”,但主表记录存在。
根本原因 :
在PG兼容模式下,用户表仅存储OID引用,实际数据在pg_largeobject系统表中 迁移工具可能只复制了用户表数据,未同步系统表中的LOB数据 事务未正确提交导致OID与实际数据映射关系丢失 二、金仓数据库大对象处理机制 深度 解析 2.1 Oracle兼容模式下的LOB处理 在Oracle兼容模式下,金仓数据库提供了与Oracle高度一致的LOB处理能力:
表结构定义 :
1 2 3 4 5 6 7 8 CREATE TABLE document_archive( doc_id INT PRIMARY KEY, doc_name VARCHAR(200), doc_content CLOB, -- 文档内容 doc_attachment BLOB -- 附件 );
数据插入示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 方式1: 使用PreparedStatement直接插入 String sql = "INSERT INTO document_archive VALUES(?, ?, ?, ?)"; PreparedStatement pstmt = conn.prepareStatement(sql); // 插入CLOB数据 File textFile = new File("/data/contract.txt"); Reader reader = new FileReader(textFile); pstmt.setInt(1, 1001); pstmt.setString(2, "销售合同"); pstmt.setCharacterStream(3, reader); // CLOB字段 // 插入BLOB数据 File imageFile = new File("/data/signature.jpg"); InputStream inputStream = new FileInputStream(imageFile); pstmt.setBinaryStream(4, inputStream); // BLOB字段 pstmt.executeUpdate(); 
关键优势 :
支持多种插入方法(setCharacterStream、setString、setBinaryStream、setBytes) 完整的更新能力(setString、setCharacterStream、setAsciiStream) 与Oracle应用代码高度兼容,迁移改造成本低 2.2 PG兼容模式下的LOB处理 PG兼容模式采用了 PostgreSQL 的大对象管理机制,需要特别注意:
表结构定义 :
1 2 3 4 5 6 7 8 CREATE TABLE document_archive( doc_id INT PRIMARY KEY, doc_name VARCHAR(200), doc_content OID, -- 注意:使用OID类型 doc_attachment OID );
数据读取示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 String sql = "SELECT * FROM document_archive WHERE doc_id = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, 1001); ResultSet rs = pstmt.executeQuery(); if(rs.next()) { // 读取CLOB数据(仅支持读取操作) Clob clob = rs.getClob("doc_content"); String content = clob.getSubString(1, (int)clob.length()); // 读取BLOB数据 Blob blob = rs.getBlob("doc_attachment"); byte[] data = blob.getBytes(1, (int)blob.length()); } 
重要限制 :
Clob接口仅实现了读取方法 ,setString、setCharacterStream等写入方法未实现更新大对象数据必须使用Blob接口 需要理解OID引用机制,避免孤立对象产生 2.3 两种模式的选择建议 场景 推荐模式 理由 从Oracle迁移 Oracle兼容模式 代码改造量最小,API完全兼容 从PostgreSQL迁移 PG兼容模式 保持原有架构逻辑 新建系统 Oracle兼容模式 功能更完整,开发更便捷 性能敏感场景 需实测对比 两种模式性能特性不同
三、大对象迁移失败的典型案例分析 案例1:医疗影像系统迁移中的BLOB损坏 背景 :某三甲医院HIS系统从Oracle 11g迁移至金仓V8,包含约500万条患者影像记录(CT、MRI图像),单个BLOB对象最大达200MB。
问题表现 :
迁移后约3%的影像文件无法在PACS系统中正常显示 部分图像显示不完整,底部出现灰色区域 数据库日志显示”LOB数据长度不匹配”警告 问题定位 :
传输工具限制 :使用的ETL工具默认LOB缓冲区仅64MB,超过此大小的数据被截断网络超时 :大对象传输时间过长,中间网络设备超时断开连接字符集问题 :迁移脚本错误地将BLOB当作CLOB处理,导致二进制数据被字符集转换破坏解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // 正确的大对象迁移代码示例 public void migrateLargeBlob(Connection sourceConn, Connection targetConn) throws SQLException, IOException { // 源数据库查询 String selectSql = "SELECT image_id, image_data FROM medical_images"; Statement stmt = sourceConn.createStatement(); ResultSet rs = stmt.executeQuery(selectSql); // 目标数据库准备 String insertSql = "INSERT INTO medical_images VALUES(?, ?)"; PreparedStatement pstmt = targetConn.prepareStatement(insertSql); // 关键:关闭自动提交,使用手动事务控制 targetConn.setAutoCommit(false); int batchCount = 0; while(rs.next()) { int imageId = rs.getInt("image_id"); Blob sourceBlob = rs.getBlob("image_data"); // 关键:使用流式传输,避免内存溢出 InputStream inputStream = sourceBlob.getBinaryStream(); pstmt.setInt(1, imageId); pstmt.setBinaryStream(2, inputStream, sourceBlob.length()); pstmt.addBatch(); batchCount++; // 每100条提交一次,避免事务过大 if(batchCount % 100 == 0) { pstmt.executeBatch(); targetConn.commit(); System.out.println("已迁移 " + batchCount + " 条记录"); } inputStream.close(); } // 提交剩余数据 pstmt.executeBatch(); targetConn.commit(); rs.close(); stmt.close(); pstmt.close(); } 
关键改进点 :
使用setBinaryStream而非setBytes,避免一次性加载整个对象到内存 明确指定数据长度:setBinaryStream(index, stream, length) 分批提交事务,防止超大事务导致的回滚风险 增加数据完整性校验(MD5/SHA256哈希对比) 案例2: 文档管理系统 CLOB字符集转换错误 背景 :某政府机关公文系统迁移,包含约100万份历史公文(CLOB存储),涉及大量中文内容。
问题表现 :
迁移后部分文档出现乱码 特殊字符(如”𠮷”、“𨋢”)显示为”?” 文档长度与原始数据不一致 根本原因 :
源数据库字符集 :Oracle使用ZHS16GBK目标数据库字符集 :金仓使用UTF8迁移工具 :未正确处理字符集转换,部分4字节UTF8字符被截断正确的迁移方法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public void migrateClobWithCharset(Connection sourceConn, Connection targetConn) throws SQLException, IOException { String selectSql = "SELECT doc_id, doc_content FROM documents"; Statement stmt = sourceConn.createStatement(); ResultSet rs = stmt.executeQuery(selectSql); String insertSql = "INSERT INTO documents VALUES(?, ?)"; PreparedStatement pstmt = targetConn.prepareStatement(insertSql); while(rs.next()) { int docId = rs.getInt("doc_id"); Clob sourceClob = rs.getClob("doc_content"); // 关键:使用Reader确保字符正确读取 Reader reader = sourceClob.getCharacterStream(); StringBuilder content = new StringBuilder(); char[] buffer = new char[4096]; int charsRead; while((charsRead = reader.read(buffer)) != -1) { content.append(buffer, 0, charsRead); } reader.close(); // 验证内容长度 long originalLength = sourceClob.length(); int migratedLength = content.length(); if(originalLength != migratedLength) { System.err.println("警告: 文档 " + docId + " 长度不匹配 (原始:" + originalLength + ", 迁移:" + migratedLength + ")"); } pstmt.setInt(1, docId); pstmt.setString(2, content.toString()); pstmt.executeUpdate(); } rs.close(); stmt.close(); pstmt.close(); } 
字符集配置检查清单 :
✅ 确认源数据库字符集:SELECT * FROM nls_database_parameters WHERE parameter='NLS_CHARACTERSET' ✅ 确认目标数据库字符集:SHOW server_encoding ✅ JDBC连接串指定字符集:jdbc:kingbase8://host:port/db?characterEncoding=UTF-8 ✅ 对比迁移前后数据MD5值 案例3:PG兼容模式下的Clob更新失败 背景 :某企业选择金仓PG兼容模式,迁移后发现应用无法更新公告内容(CLOB字段)。
错误代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 原Oracle应用代码 String sql = "SELECT content FROM announcements WHERE id=? FOR UPDATE"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, 1001); ResultSet rs = pstmt.executeQuery(); if(rs.next()) { Clob clob = rs.getClob("content"); // 在PG兼容模式下,此方法未实现,抛出异常! clob.setString(1, "更新后的公告内容"); } 
错误信息 :
1 2 3 java.sql.SQLFeatureNotSupportedException: Method Clob.setString(long, String) is not yet implemented.
正确的修复方法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 方法1: 使用Blob接口更新(推荐) String sql = "SELECT content FROM announcements WHERE id=? FOR UPDATE"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, 1001); ResultSet rs = pstmt.executeQuery(); if(rs.next()) { Blob blob = rs.getBlob("content"); // 注意:使用Blob而非Clob String newContent = "更新后的公告内容"; // 使用setBytes方法 blob.setBytes(1, newContent.getBytes("UTF-8")); } // 方法2: 使用UPDATE语句直接更新 String updateSql = "UPDATE announcements SET content=? WHERE id=?"; PreparedStatement updateStmt = conn.prepareStatement(updateSql); updateStmt.setString(1, "更新后的公告内容"); updateStmt.setInt(2, 1001); updateStmt.executeUpdate(); 
兼容性改造建议 :
在PG兼容模式下,统一使用Blob接口处理大对象 封装LOB操作工具类,屏蔽底层差异 更新操作优先使用SQL语句而非LOB对象方法 四、大对象迁移的最佳实践 4.1 迁移前的准备工作 1. 数据评估与分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 统计LOB字段分布 SELECT table_name, column_name, data_type, COUNT(*) as record_count, AVG(DBMS_LOB.GETLENGTH(column_name)) as avg_size, MAX(DBMS_LOB.GETLENGTH(column_name)) as max_size FROM user_tab_columns c JOIN user_tables t ON c.table_name = t.table_name WHERE data_type IN ('BLOB', 'CLOB') GROUP BY table_name, column_name, data_type; 
评估指标 :
LOB字段总数量 单个对象最大尺寸 总数据量 是否存在空LOB或损坏LOB 2. 环境配置 检查 源数据库(Oracle) :
1 2 3 4 5 6 7 8 9 10 -- 检查字符集 SELECT * FROM nls_database_parameters WHERE parameter IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET'); -- 检查LOB存储参数 SELECT segment_name, bytes/1024/1024 as size_mb FROM user_segments WHERE segment_type = 'LOBSEGMENT';
目标数据库(金仓) :
1 2 3 4 5 6 7 8 9 10 11 -- 检查字符集 SHOW server_encoding; -- 检查LOB相关参数(PG兼容模式) SHOW lo_compat_privileges; -- 检查表空间 SELECT spcname, pg_size_pretty(pg_tablespace_size(spcname)) FROM pg_tablespace;
3. 制定迁移策略 数据量级 推荐策略 工具选择 < 10GB 全量一次性迁移 JDBC程序/金仓迁移工具 10GB - 100GB 分批迁移 自定义脚本+并行处理 > 100GB 增量迁移 CDC工具+数据校验
4.2 迁移过程中的质量控制 1. 数据完整性校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class LobIntegrityChecker { /** * 计算LOB数据的MD5哈希值 */ public String calculateBlobMD5(Blob blob) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); InputStream input = blob.getBinaryStream(); byte[] buffer = new byte[8192]; int bytesRead; while((bytesRead = input.read(buffer)) != -1) { md.update(buffer, 0, bytesRead); } input.close(); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(); for(byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } /** * 对比源库和目标库的LOB数据 */ public void verifyMigration(Connection sourceConn, Connection targetConn) throws Exception { String sql = "SELECT id, data_blob FROM large_objects ORDER BY id"; Statement sourceStmt = sourceConn.createStatement(); Statement targetStmt = targetConn.createStatement(); ResultSet sourceRs = sourceStmt.executeQuery(sql); ResultSet targetRs = targetStmt.executeQuery(sql); int mismatchCount = 0; while(sourceRs.next() && targetRs.next()) { int id = sourceRs.getInt("id"); Blob sourceBlob = sourceRs.getBlob("data_blob"); Blob targetBlob = targetRs.getBlob("data_blob"); String sourceMD5 = calculateBlobMD5(sourceBlob); String targetMD5 = calculateBlobMD5(targetBlob); if(!sourceMD5.equals(targetMD5)) { System.err.println("数据不一致! ID=" + id + ", 源MD5=" + sourceMD5 + ", 目标MD5=" + targetMD5); mismatchCount++; } } System.out.println("校验完成,发现 " + mismatchCount + " 条不一致记录"); sourceRs.close(); targetRs.close(); sourceStmt.close(); targetStmt.close(); } } 
2. 性能监控与优化 迁移性能监控指标 :
每秒处理记录数(TPS) 网络带宽使用率 数据库CPU/内存/IO使用率 事务提交延迟 优化技巧 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public class OptimizedLobMigrator { private static final int BATCH_SIZE = 100; private static final int THREAD_COUNT = 4; public void parallelMigrate(Connection sourceConn, Connection targetConn) throws Exception { // 1. 获取总记录数 String countSql = "SELECT COUNT(*) FROM large_objects"; Statement stmt = sourceConn.createStatement(); ResultSet rs = stmt.executeQuery(countSql); rs.next(); int totalRecords = rs.getInt(1); rs.close(); stmt.close(); // 2. 计算每个线程处理的范围 int recordsPerThread = totalRecords / THREAD_COUNT; // 3. 创建线程池 ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); List<Future<Integer>> futures = new ArrayList<>(); for(int i = 0; i < THREAD_COUNT; i++) { int startId = i * recordsPerThread + 1; int endId = (i == THREAD_COUNT - 1) ? totalRecords : (i + 1) * recordsPerThread; Future<Integer> future = executor.submit(() -> migrateRange(sourceConn, targetConn, startId, endId) ); futures.add(future); } // 4. 等待所有线程完成 int totalMigrated = 0; for(Future<Integer> future : futures) { totalMigrated += future.get(); } executor.shutdown(); System.out.println("迁移完成,共处理 " + totalMigrated + " 条记录"); } private int migrateRange(Connection sourceConn, Connection targetConn, int startId, int endId) throws SQLException { String selectSql = "SELECT * FROM large_objects WHERE id BETWEEN ? AND ?"; PreparedStatement selectStmt = sourceConn.prepareStatement(selectSql); selectStmt.setInt(1, startId); selectStmt.setInt(2, endId); String insertSql = "INSERT INTO large_objects VALUES(?, ?)"; PreparedStatement insertStmt = targetConn.prepareStatement(insertSql); ResultSet rs = selectStmt.executeQuery(); int count = 0; targetConn.setAutoCommit(false); while(rs.next()) { int id = rs.getInt("id"); Blob blob = rs.getBlob("data_blob"); insertStmt.setInt(1, id); insertStmt.setBlob(2, blob); insertStmt.addBatch(); count++; if(count % BATCH_SIZE == 0) { insertStmt.executeBatch(); targetConn.commit(); } } insertStmt.executeBatch(); targetConn.commit(); rs.close(); selectStmt.close(); insertStmt.close(); return count; } } 
4.3 迁移后的验证与回退 1. 全面验证清单 ✅ 数据完整性验证 :
记录数量对比:SELECT COUNT(*) FROM table_name LOB对象数量对比 数据哈希值对比(MD5/SHA256) 抽样人工核查(随机抽取1%数据) ✅ 功能验证 :
LOB数据读取功能 LOB数据更新功能 大对象查询性能 应用系统端到端测试 ✅ 性能验证 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 查询性能测试 EXPLAIN ANALYZE SELECT * FROM large_objects WHERE id = 12345; -- 插入性能测试 EXPLAIN ANALYZE INSERT INTO large_objects VALUES(99999, lo_import('/tmp/test.dat')); -- 更新性能测试 EXPLAIN ANALYZE UPDATE large_objects SET data_blob = lo_import('/tmp/new.dat') WHERE id = 12345; 
2. 应急回退方案 场景1:数据损坏严重
立即停止应用访问目标库 切换回源数据库 分析失败原因,修复迁移脚本 清空目标库数据,重新迁移 场景2:性能不达标
保持源库在线,双写模式运行 优化目标库配置(索引、参数调优) 逐步切换流量 确认稳定后下线源库 回退脚本示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #!/bin/bash # 应急回退脚本 echo "开始回退操作..." # 1. 停止应用服务 systemctl stop application-service # 2. 修改数据库连接配置 sed -i 's/jdbc:kingbase8/jdbc:oracle:thin/g' /app/config/database.properties sed -i 's/kingbase_host/oracle_host/g' /app/config/database.properties # 3. 重启应用服务 systemctl start application-service # 4. 验证服务状态 sleep 10 curl -f http://localhost:8080/health || echo "回退失败,请人工介入!" echo "回退完成" 
五、金仓数据库LOB优化建议 5.1 存储优化 Oracle兼容模式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 创建表时指定LOB存储参数 CREATE TABLE documents( doc_id INT PRIMARY KEY, doc_content CLOB ) LOB (doc_content) STORE AS ( TABLESPACE lob_data ENABLE STORAGE IN ROW -- 小于4000字节的LOB内联存储 CHUNK 8192 -- 每次分配8KB CACHE -- 启用缓存 ); 
PG兼容模式 :
1 2 3 4 5 6 7 -- 创建专用表空间存储大对象 CREATE TABLESPACE lob_space LOCATION '/data/kingbase/lob'; -- 将大对象表移动到专用表空间 ALTER TABLE pg_largeobject SET TABLESPACE lob_space;
5.2 性能调优参数 1 2 3 4 5 6 7 8 9 10 11 -- 调整LOB缓存大小(Oracle兼容模式) ALTER SYSTEM SET db_cache_size = 2G; -- 调整大对象缓存(PG兼容模式) ALTER SYSTEM SET shared_buffers = '4GB'; ALTER SYSTEM SET effective_cache_size = '12GB'; -- 调整工作内存 ALTER SYSTEM SET work_mem = '256MB';
5.3 监控与维护 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- 监控LOB空间使用(Oracle兼容模式) SELECT segment_name, segment_type, tablespace_name, bytes/1024/1024 as size_mb FROM user_segments WHERE segment_type LIKE '%LOB%' ORDER BY bytes DESC; -- 清理孤立的大对象(PG兼容模式) VACUUM FULL pg_largeobject; -- 查找未被引用的大对象 SELECT lo.oid FROM pg_largeobject_metadata lo LEFT JOIN documents d ON d.doc_content = lo.oid WHERE d.doc_content IS NULL; 
六、总结与展望 大对象数据迁移是数据库替换过程中的高风险环节,需要充分理解源数据库和目标数据库的LOB处理机制差异。针对金仓数据库的迁移,关键要点包括:
核心要点回顾 选择合适的兼容模式 :
从Oracle迁移优先选择Oracle兼容模式 理解PG兼容模式的Clob接口限制 采用正确的迁移方法 :
使用流式传输处理大对象,避免内存溢出 明确指定数据长度和字符集 实施分批提交和并行处理 建立完善的验证机制 :
数据完整性校验(哈希对比) 功能验证(读写操作测试) 性能验证(响应时间对比) 做好应急预案 :
保留源数据库作为回退路径 制定详细的回退流程 建立监控告警机制 未来发展方向 随着金仓数据库的持续演进,LOB处理能力将进一步增强:
智能迁移工具 :自动识别LOB类型,选择最优迁移策略增量同步能力 :支持LOB数据的实时同步,缩短停机窗口跨模式兼容 :统一Oracle和PG模式的LOB接口,降低应用改造成本性能优化 :引入列式存储、压缩算法,提升LOB数据处理效率数据库国产化替代是一项系统工程,需要技术团队、业务部门和厂商的紧密配合。通过科学的规划、严谨的实施和持续的优化,完全可以实现平稳、安全的数据库迁移,为企业数字化转型奠定坚实基础。