Spring Boot项目无缝迁移PostgreSQL到Kingbase:MyBatis Plus配置全攻略(附兼容性对照表)
最近在帮几个团队做数据库国产化迁移,从PostgreSQL切换到人大金仓Kingbase,发现很多Java开发者都在ORM层适配上踩坑。特别是用MyBatis Plus的项目,迁移后各种奇怪的报错——表找不到、SQL语法错误、分页失效,让人头疼。其实Kingbase对PostgreSQL的兼容性已经相当不错,但ORM框架的配置细节没处理好,就会让整个迁移过程变得异常痛苦。
这篇文章我会结合最近几个项目的实战经验,从Java开发者的视角,把Spring Boot项目从PostgreSQL迁移到Kingbase的完整流程拆解清楚。重点不是数据库层面的数据迁移,而是应用层如何平滑过渡,特别是MyBatis Plus的各种配置适配。我会分享具体的配置代码、遇到的坑以及解决方案,还会附上一份详细的兼容性对照表,帮你快速定位问题。
1. 迁移前的环境准备与依赖调整
迁移的第一步不是急着改代码,而是先把环境搭建好,确保基础依赖配置正确。很多团队一上来就改业务代码,结果发现连数据库都连不上,白白浪费时间。
1.1 驱动依赖的切换
PostgreSQL项目通常使用postgresql驱动,迁移到Kingbase需要换成官方提供的kingbase8驱动。这里有个细节要注意:Kingbase驱动有两个版本,一个是基于JDBC 4.2的kingbase8,另一个是兼容JDBC 4.0的kingbase。对于Spring Boot 2.x及以上版本,建议使用kingbase8。
<!-- 移除原有的PostgreSQL驱动 --> <!-- <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> --> <!-- 添加Kingbase驱动 --> <dependency> <groupId>cn.com.kingbase</groupId> <artifactId>kingbase8</artifactId> <version>8.6.0</version> </dependency>版本选择上,我建议先到Kingbase官网查看最新稳定版。8.6.0是目前比较成熟的版本,对Spring Boot 2.7.x和3.x都有良好支持。如果你们项目还在用较老的Spring Boot 2.0-2.4,可能需要考虑8.2.x版本。
1.2 数据库连接配置优化
连接字符串的配置是迁移的第一个关键点。Kingbase虽然兼容PostgreSQL协议,但URL格式和参数有些差异。
spring: datasource: driver-class-name: com.kingbase8.Driver url: jdbc:kingbase8://192.168.1.100:54321/your_database?currentSchema=public&searchPath=public username: your_username password: your_password hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000这里有几个参数需要特别注意:
- 端口号:Kingbase默认使用54321,而PostgreSQL是5432。很多团队迁移时忘记改端口,连不上数据库还以为是驱动问题。
- currentSchema和searchPath:这两个参数对Kingbase的模式管理至关重要。Kingbase默认是Oracle兼容模式,需要显式指定public模式,否则MyBatis Plus生成的SQL会找不到表。
- 连接池配置:建议使用HikariCP,它比Druid在Kingbase上的表现更稳定。连接超时时间可以适当调大,因为Kingbase在某些复杂查询上可能比PostgreSQL稍慢。
注意:如果你们项目用了Druid,迁移后可能会遇到一些兼容性问题。我遇到过Druid的监控SQL在Kingbase上执行报错的情况。如果非要用Druid,记得关闭
wall防火墙的严格模式。
1.3 测试连接与基础验证
配置完连接信息后,先写个简单的测试类验证一下基础连接是否正常。别小看这个步骤,它能帮你快速定位是网络问题、权限问题还是配置问题。
@Component @Slf4j public class DatabaseConnectionValidator implements ApplicationRunner { @Autowired private DataSource dataSource; @Override public void run(ApplicationArguments args) throws Exception { try (Connection conn = dataSource.getConnection()) { DatabaseMetaData metaData = conn.getMetaData(); log.info("数据库连接成功,产品名称: {}, 版本: {}", metaData.getDatabaseProductName(), metaData.getDatabaseProductVersion()); // 测试简单查询 try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT 1 as test")) { if (rs.next()) { log.info("基础查询测试通过"); } } // 检查public模式是否存在 try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT schema_name FROM information_schema.schemata " + "WHERE schema_name = 'public'")) { if (rs.next()) { log.info("public模式存在,可以继续迁移"); } else { log.warn("public模式不存在,可能需要手动创建"); } } } catch (SQLException e) { log.error("数据库连接测试失败", e); throw new RuntimeException("数据库连接异常,请检查配置", e); } } }这个验证器会在应用启动时自动运行,如果连接有问题,启动阶段就能发现,避免运行时才报错。我建议在迁移初期,所有环境都加上这个验证器。
2. MyBatis Plus核心配置适配
MyBatis Plus的配置是迁移的重中之重。很多PostgreSQL下运行良好的配置,在Kingbase上需要针对性调整。
2.1 数据库类型明确指定
MyBatis Plus需要知道当前使用的是哪种数据库,才能生成正确的SQL。PostgreSQL项目通常配置DbType.POSTGRESQL,迁移到Kingbase后需要改为DbType.KINGBASE_ES。
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 - 关键配置 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); paginationInterceptor.setDbType(DbType.KINGBASE_ES); paginationInterceptor.setOverflow(true); // 超过最大页数时回到第一页 paginationInterceptor.setMaxLimit(1000L); // 单页最大记录数 interceptor.addInnerInterceptor(paginationInterceptor); // 乐观锁插件 OptimisticLockerInnerInterceptor optimisticLockerInterceptor = new OptimisticLockerInnerInterceptor(); interceptor.addInnerInterceptor(optimisticLockerInterceptor); // 防止全表更新与删除插件 BlockAttackInnerInterceptor blockAttackInterceptor = new BlockAttackInnerInterceptor(); interceptor.addInnerInterceptor(blockAttackInterceptor); return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> { // 开启驼峰命名转换 configuration.setMapUnderscoreToCamelCase(true); // 设置默认执行器类型 configuration.setDefaultExecutorType(ExecutorType.SIMPLE); }; } }这里有个坑我踩过:分页插件必须放在拦截器链的第一个位置。如果顺序不对,其他插件可能会影响分页SQL的生成。Kingbase的分页语法虽然和PostgreSQL类似(都使用LIMIT和OFFSET),但MyBatis Plus内部处理机制有差异,明确指定数据库类型能避免很多奇怪的问题。
2.2 全局配置优化
除了拦截器配置,全局配置也需要调整。特别是id-type和table-prefix,这两个配置对Kingbase的影响很大。
mybatis-plus: global-config: db-config: id-type: auto table-prefix: schema: public logic-delete-field: deleted # 逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境开启SQL日志 default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandlerid-type配置:Kingbase支持多种主键生成策略。auto表示使用数据库自增,但需要表的主键是serial或bigserial类型。如果你们用的是UUID或者自定义序列,需要改为INPUT并配合@TableId注解。
schema配置:这个配置很多团队会忽略。Kingbase默认不指定schema时,会使用连接用户的默认schema,而MyBatis Plus生成的SQL可能不带schema前缀。显式配置schema: public能确保所有生成的SQL都包含public.前缀。
2.3 类型处理器配置
PostgreSQL和Kingbase在数据类型上基本兼容,但有些特殊类型需要额外处理。比如JSONB类型,虽然Kingbase也支持,但MyBatis的默认类型处理器可能需要调整。
@Configuration public class TypeHandlerConfig { @Bean public ConfigurationCustomizer typeHandlerCustomizer() { return configuration -> { // 注册JSONB类型处理器 TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); typeHandlerRegistry.register(JsonNode.class, JacksonTypeHandler.class); typeHandlerRegistry.register(Map.class, JacksonTypeHandler.class); typeHandlerRegistry.register(List.class, JacksonTypeHandler.class); }; } } // 实体类中使用JSONB字段 @Data @TableName(value = "user_profile") public class UserProfile { @TableId(type = IdType.AUTO) private Long id; private String username; @TableField(typeHandler = JacksonTypeHandler.class) private Map<String, Object> preferences; // 存储为JSONB @TableField(typeHandler = JacksonTypeHandler.class) private List<String> tags; // 存储为JSONB数组 }对于JSONB字段,我推荐使用MyBatis Plus自带的JacksonTypeHandler。它基于Jackson库,能自动处理Java对象和JSONB字符串的转换。如果你们项目中有复杂的JSON结构,可以考虑自定义类型处理器。
3. 表名与模式前缀处理
这是迁移过程中最常见的问题之一。PostgreSQL中可以直接写表名,但在Kingbase中,如果不指定schema,经常会报relation "table_name" does not exist错误。
3.1 实体类注解配置
最直接的解决方案是在每个实体类的@TableName注解中加上schema前缀。
// 迁移前 @TableName("sys_user") public class SysUser { // ... } // 迁移后 @TableName(value = "public.sys_user", autoResultMap = true) public class SysUser { // ... }但这种方法有个明显缺点:如果项目有几十上百个实体类,手动修改工作量巨大,而且容易遗漏。更糟糕的是,如果以后要换schema(比如分库分场景),又得全部改一遍。
3.2 全局表名处理器方案
我推荐使用全局表名处理器,动态为所有SQL添加schema前缀。这样只需要在一处配置,所有Mapper都生效。
@Component @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) @Slf4j public class SchemaInterceptor implements Interceptor { private static final String DEFAULT_SCHEMA = "public"; private static final Pattern TABLE_PATTERN = Pattern.compile( "\\b(FROM|JOIN|UPDATE|INTO|DELETE\\s+FROM)\\s+([a-zA-Z_][a-zA-Z0-9_]*)", Pattern.CASE_INSENSITIVE ); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(handler); // 获取BoundSql,里面包含原始SQL BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); String originalSql = boundSql.getSql(); // 处理SQL,添加schema前缀 String processedSql = processTableNames(originalSql); if (!originalSql.equals(processedSql)) { log.debug("SQL处理前: {}", originalSql); log.debug("SQL处理后: {}", processedSql); metaObject.setValue("delegate.boundSql.sql", processedSql); } return invocation.proceed(); } private String processTableNames(String sql) { if (sql == null || sql.trim().isEmpty()) { return sql; } StringBuffer result = new StringBuffer(); Matcher matcher = TABLE_PATTERN.matcher(sql); while (matcher.find()) { String keyword = matcher.group(1); String tableName = matcher.group(2); // 如果表名已经包含点(说明已经有schema前缀),就不处理 if (!tableName.contains(".") && !isSystemTable(tableName)) { String replacement = keyword + " " + DEFAULT_SCHEMA + "." + tableName; matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } else { matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group())); } } matcher.appendTail(result); return result.toString(); } private boolean isSystemTable(String tableName) { // 排除系统表、临时表等 return tableName.startsWith("pg_") || tableName.startsWith("information_schema.") || tableName.startsWith("sys_"); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可以从配置读取schema名称 } }这个拦截器会拦截所有SQL语句,自动为表名添加public.前缀。它有几个优点:
- 零代码侵入:实体类和Mapper XML都不需要修改
- 灵活配置:可以通过配置文件动态修改schema
- 智能识别:会跳过已经包含schema的表名和系统表
- 性能影响小:只做简单的字符串替换,对性能影响可以忽略
提示:如果项目中有复杂的SQL,比如包含子查询、CTE(公共表表达式),可能需要调整正则表达式模式。建议先在测试环境充分验证。
3.3 Mapper XML中的表名处理
如果你们项目用了XML Mapper,还需要检查XML文件中的SQL语句。虽然拦截器能处理大部分情况,但有些复杂的SQL可能匹配不到。
<!-- 迁移前 --> <select id="selectUserWithRole" resultMap="userRoleMap"> SELECT u.*, r.role_name FROM sys_user u LEFT JOIN sys_user_role ur ON u.id = ur.user_id LEFT JOIN sys_role r ON ur.role_id = r.id WHERE u.status = 1 </select> <!-- 迁移后 --> <select id="selectUserWithRole" resultMap="userRoleMap"> SELECT u.*, r.role_name FROM public.sys_user u LEFT JOIN public.sys_user_role ur ON u.id = ur.user_id LEFT JOIN public.sys_role r ON ur.role_id = r.id WHERE u.status = 1 </select>对于XML文件,我建议写个简单的脚本批量处理。下面是个Python脚本示例:
#!/usr/bin/env python3 import os import re import glob def add_schema_to_sql(file_path, schema="public"): """为XML文件中的SQL语句添加schema前缀""" with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # 匹配FROM/JOIN/UPDATE/INTO后面的表名 patterns = [ (r'(\bFROM\s+)([a-zA-Z_][a-zA-Z0-9_]*)', r'\1' + schema + r'.\2'), (r'(\bJOIN\s+)([a-zA-Z_][a-zA-Z0-9_]*)', r'\1' + schema + r'.\2'), (r'(\bUPDATE\s+)([a-zA-Z_][a-zA-Z0-9_]*)', r'\1' + schema + r'.\2'), (r'(\bINTO\s+)([a-zA-Z_][a-zA-Z0-9_]*)', r'\1' + schema + r'.\2'), (r'(\bDELETE\s+FROM\s+)([a-zA-Z_][a-zA-Z0-9_]*)', r'\1' + schema + r'.\2'), ] modified = False for pattern, replacement in patterns: new_content, count = re.subn(pattern, replacement, content, flags=re.IGNORECASE) if count > 0: content = new_content modified = True if modified: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) print(f"已处理: {file_path}") else: print(f"无需处理: {file_path}") # 批量处理所有Mapper XML文件 mapper_files = glob.glob("src/main/resources/mapper/**/*.xml", recursive=True) for file_path in mapper_files: add_schema_to_sql(file_path)运行这个脚本前,记得先备份。处理完后要仔细测试,确保没有误改。
4. SQL关键字与语法兼容性处理
Kingbase虽然高度兼容PostgreSQL,但两者在SQL语法和关键字上还是有些细微差异。这些差异在简单查询中可能不明显,但在复杂业务SQL中就会暴露出来。
4.1 关键字冲突处理
最典型的关键字冲突是level。在PostgreSQL中,level不是保留字,可以用作字段名。但在Kingbase中,level是保留字,直接使用会报语法错误。
-- PostgreSQL中正常,Kingbase中报错 SELECT id, name, level FROM sys_department WHERE level > 1;解决方案有两种:修改字段名,或者用引号包裹字段名。我推荐修改字段名,因为引号包裹虽然能解决问题,但会影响SQL的可读性和维护性。
// 实体类修改 @Data @TableName("sys_department") public class SysDepartment { @TableId(type = IdType.AUTO) private Long id; private String name; // 迁移前 // private Integer level; // 迁移后 @TableField("level") // 数据库字段名还是level,但Java字段名改了 private Integer deptLevel; // 对应的getter/setter也要改 public Integer getDeptLevel() { return deptLevel; } public void setDeptLevel(Integer deptLevel) { this.deptLevel = deptLevel; } } // Lambda查询时使用新的字段名 List<SysDepartment> list = departmentService.lambdaQuery() .gt(SysDepartment::getDeptLevel, 1) .list();如果项目中有很多地方引用了level字段,可以使用IDE的重构功能批量修改。IntelliJ IDEA的Refactor -> Rename功能很好用,能同时修改Java代码、XML和注释。
4.2 函数兼容性对照
PostgreSQL的一些特有函数,在Kingbase中可能有不同的实现或名称。下面是我整理的常用函数对照表:
| PostgreSQL函数 | Kingbase对应函数 | 说明 | 处理建议 |
|---|---|---|---|
generate_series() | generate_series() | 兼容 | 直接使用 |
jsonb_agg() | jsonb_agg() | 兼容 | 直接使用 |
jsonb_build_object() | jsonb_build_object() | 兼容 | 直接使用 |
to_timestamp() | to_timestamp() | 兼容 | 直接使用 |
date_trunc() | date_trunc() | 兼容 | 直接使用 |
regexp_replace() | regexp_replace() | 兼容 | 直接使用 |
string_agg() | string_agg() | 兼容 | 直接使用 |
array_agg() | array_agg() | 兼容 | 直接使用 |
row_number() OVER() | row_number() OVER() | 兼容 | 直接使用 |
ILIKE | ILIKE | 不兼容 | 改为LOWER(column) LIKE LOWER(pattern) |
ILIKE是PostgreSQL特有的大小写不敏感LIKE操作符,Kingbase不支持。需要改写为使用LOWER()函数:
-- 迁移前 SELECT * FROM users WHERE username ILIKE '%admin%'; -- 迁移后 SELECT * FROM users WHERE LOWER(username) LIKE LOWER('%admin%');4.3 分页语法差异
MyBatis Plus虽然帮我们处理了分页,但有些复杂查询可能需要手动写分页SQL。PostgreSQL和Kingbase的分页语法基本一致,但有些细节要注意。
-- PostgreSQL和Kingbase都支持的标准分页 SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20; -- 但Kingbase对子查询分页更严格 -- 这个在PostgreSQL中可能正常,在Kingbase中可能报错 SELECT * FROM ( SELECT id, name, ROW_NUMBER() OVER (ORDER BY id) as rn FROM users ) t WHERE rn BETWEEN 21 AND 30; -- Kingbase中建议这样写 WITH ranked_users AS ( SELECT id, name, ROW_NUMBER() OVER (ORDER BY id) as rn FROM users ) SELECT * FROM ranked_users WHERE rn BETWEEN 21 AND 30;如果你们项目中有大量复杂的分页查询,我建议在迁移前用Kingbase的测试环境跑一遍,提前发现问题。
4.4 事务与锁机制
Kingbase的事务隔离级别和锁机制与PostgreSQL基本一致,但在高并发场景下,有些细微差异需要注意。
@Service @Transactional(rollbackFor = Exception.class) public class OrderService { @Autowired private OrderMapper orderMapper; public void createOrder(Order order) { // Kingbase对SELECT FOR UPDATE的兼容性较好 // 但要注意锁超时设置 Order lockedOrder = orderMapper.selectForUpdate(order.getId()); if (lockedOrder != null) { // 业务处理 orderMapper.updateById(order); } } } // Mapper中的FOR UPDATE查询 @Select("SELECT * FROM orders WHERE id = #{id} FOR UPDATE NOWAIT") Order selectForUpdateWithNoWait(Long id); @Select("SELECT * FROM orders WHERE id = #{id} FOR UPDATE SKIP LOCKED") Order selectForUpdateSkipLocked(Long id);Kingbase支持FOR UPDATE NOWAIT和FOR UPDATE SKIP LOCKED语法,但在默认配置下,锁超时时间可能和PostgreSQL不同。如果遇到锁等待超时问题,可以调整Kingbase的lock_timeout参数。
5. 高级特性与性能优化
迁移完成后,还需要关注一些高级特性和性能优化点。Kingbase虽然兼容PostgreSQL,但在执行计划、索引策略等方面有自己的特点。
5.1 执行计划分析
同样的SQL,在PostgreSQL和Kingbase上的执行计划可能不同。迁移后要重点关注慢查询。
-- Kingbase中查看执行计划 EXPLAIN (ANALYZE, BUFFERS) SELECT u.*, o.order_count FROM public.users u LEFT JOIN ( SELECT user_id, COUNT(*) as order_count FROM public.orders WHERE created_at > CURRENT_DATE - INTERVAL '30 days' GROUP BY user_id ) o ON u.id = o.user_id WHERE u.status = 'ACTIVE' ORDER BY u.created_at DESC LIMIT 100;Kingbase的EXPLAIN输出格式和PostgreSQL类似,但有些统计信息名称不同。重点关注:
- Seq ScanvsIndex Scan:全表扫描还是索引扫描
- Hash JoinvsNested Loop:连接策略是否合理
- 实际行数 vs 估计行数:统计信息是否准确
如果发现执行计划不理想,可以尝试:
- 更新统计信息:
ANALYZE table_name; - 创建缺失的索引
- 调整
work_mem、shared_buffers等参数 - 使用HINT强制索引(Kingbase支持
/*+ INDEX(table index) */语法)
5.2 索引策略优化
Kingbase的索引类型和PostgreSQL基本一致,但创建语法和优化器行为可能有差异。
-- B-tree索引(最常用) CREATE INDEX idx_users_email ON public.users(email); -- 唯一索引 CREATE UNIQUE INDEX idx_users_username ON public.users(username); -- 复合索引 CREATE INDEX idx_orders_user_status ON public.orders(user_id, status); -- 部分索引(条件索引) CREATE INDEX idx_active_users ON public.users(id) WHERE status = 'ACTIVE'; -- 表达式索引 CREATE INDEX idx_users_lower_email ON public.users(LOWER(email)); -- GIN索引(用于JSONB、数组等) CREATE INDEX idx_products_tags ON public.products USING GIN(tags); -- 空间索引(如果用了PostGIS扩展) CREATE INDEX idx_locations_geom ON public.locations USING GIST(geom);迁移后需要重新评估索引效果:
- 冗余索引:删除很少使用或重复的索引
- 缺失索引:通过慢查询日志找出需要添加的索引
- 索引碎片:定期重建碎片化严重的索引
REINDEX INDEX index_name;
5.3 连接池配置调优
Kingbase的连接管理和PostgreSQL有些不同,需要调整连接池配置。
spring: datasource: hikari: # 连接超时时间(毫秒) connection-timeout: 30000 # 连接池最大大小 maximum-pool-size: 20 # 最小空闲连接数 minimum-idle: 5 # 连接最大存活时间(毫秒) max-lifetime: 1800000 # 连接空闲超时时间(毫秒) idle-timeout: 600000 # 测试连接有效性的SQL connection-test-query: SELECT 1 # 从池中获取连接时验证 connection-init-sql: SET search_path TO public几个关键点:
- maximum-pool-size:不要设置太大,Kingbase的每个连接都有内存开销。一般20-50足够。
- connection-init-sql:设置搜索路径,避免每次执行SQL都要带schema前缀。
- validation-timeout:Kingbase的验证可能比PostgreSQL慢,可以适当调大。
5.4 监控与告警
迁移后要建立完善的监控体系。除了常规的数据库监控,还要关注应用层的指标。
@Component @Slf4j public class DatabaseMetricsCollector { @Autowired private DataSource dataSource; @Scheduled(fixedDelay = 60000) // 每分钟收集一次 public void collectMetrics() { if (dataSource instanceof HikariDataSource) { HikariDataSource hikariDataSource = (HikariDataSource) dataSource; HikariPoolMXBean pool = hikariDataSource.getHikariPoolMXBean(); log.info("连接池状态 - 活跃连接: {}, 空闲连接: {}, 总连接: {}, 等待线程: {}", pool.getActiveConnections(), pool.getIdleConnections(), pool.getTotalConnections(), pool.getThreadsAwaitingConnection()); // 记录到监控系统 Metrics.gauge("db.pool.active", pool.getActiveConnections()); Metrics.gauge("db.pool.idle", pool.getIdleConnections()); Metrics.gauge("db.pool.waiting", pool.getThreadsAwaitingConnection()); } // 执行一些诊断查询 try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT COUNT(*) as total_tables FROM information_schema.tables " + "WHERE table_schema = 'public'")) { if (rs.next()) { log.info("public模式下的表数量: {}", rs.getInt("total_tables")); } } catch (SQLException e) { log.error("收集数据库指标失败", e); } } }监控指标要包括:
- 连接池状态:活跃连接数、空闲连接数、等待线程数
- 慢查询统计:执行时间超过阈值的SQL
- 错误率:SQL执行错误的比例
- 事务状态:活跃事务数、长事务数
5.5 备份与恢复策略
迁移完成后,要重新评估备份策略。Kingbase的备份工具和PostgreSQL的pg_dump类似,但有些参数不同。
# Kingbase的逻辑备份 sys_dump -U sysdba -d your_database -f /backup/your_database_$(date +%Y%m%d).sql # 备份特定schema sys_dump -U sysdba -d your_database -n public -f /backup/public_schema.sql # 只备份结构 sys_dump -U sysdba -d your_database -s -f /backup/structure_only.sql # 只备份数据 sys_dump -U sysdba -d your_database -a -f /backup/data_only.sql # 并行备份(大数据库) sys_dump -U sysdba -d your_database -j 4 -f /backup/parallel_backup/ # 恢复数据库 sys_restore -U sysdba -d your_database /backup/your_database_20240115.sql备份策略建议:
- 全量备份:每天一次,保留7天
- 增量备份:每小时一次,保留24小时
- 归档日志:开启WAL归档,用于PITR(时间点恢复)
- 异地备份:重要数据要有异地备份
6. 兼容性对照表与常见问题
最后我整理了一份详细的兼容性对照表,涵盖了迁移过程中可能遇到的各种问题。这张表基于我最近几个项目的实战经验,应该能覆盖大部分场景。
6.1 数据类型兼容性
| PostgreSQL类型 | Kingbase对应类型 | 是否兼容 | 注意事项 |
|---|---|---|---|
serial | serial | 是 | 完全兼容 |
bigserial | bigserial | 是 | 完全兼容 |
varchar(n) | varchar(n) | 是 | 完全兼容 |
text | text | 是 | 完全兼容 |
integer | integer | 是 | 完全兼容 |
bigint | bigint | 是 | 完全兼容 |
numeric(p,s) | numeric(p,s) | 是 | 完全兼容 |
decimal(p,s) | decimal(p,s) | 是 | 完全兼容 |
boolean | boolean | 是 | 完全兼容 |
date | date | 是 | 完全兼容 |
timestamp | timestamp | 是 | 完全兼容 |
timestamptz | timestamptz | 是 | 完全兼容 |
json | json | 是 | 完全兼容 |
jsonb | jsonb | 是 | 需要PostGIS扩展 |
uuid | uuid | 是 | 需要uuid-ossp扩展 |
bytea | bytea | 是 | 完全兼容 |
array | array | 是 | 语法略有差异 |
hstore | hstore | 部分 | 需要单独安装扩展 |
6.2 SQL语法差异
| 功能 | PostgreSQL语法 | Kingbase语法 | 处理建议 |
|---|---|---|---|
| 分页 | LIMIT n OFFSET m | LIMIT n OFFSET m | 完全兼容 |
| 字符串连接 | `'a' | 'b'` | |
| 正则匹配 | ~ 'pattern' | ~ 'pattern' | 完全兼容 |
| 正则替换 | regexp_replace() | regexp_replace() | 完全兼容 |
| 窗口函数 | ROW_NUMBER() OVER() | ROW_NUMBER() OVER() | 完全兼容 |
| CTE | WITH cte AS (...) | WITH cte AS (...) | 完全兼容 |
| 递归查询 | WITH RECURSIVE | WITH RECURSIVE | 完全兼容 |
| 全文搜索 | to_tsvector() | to_tsvector() | 配置可能不同 |
| 数组操作 | array_agg() | array_agg() | 完全兼容 |
| JSON操作 | jsonb_extract_path() | jsonb_extract_path() | 完全兼容 |
6.3 MyBatis Plus配置对照
| 配置项 | PostgreSQL配置 | Kingbase配置 | 说明 |
|---|---|---|---|
| 驱动类 | org.postgresql.Driver | com.kingbase8.Driver | 必须修改 |
| URL前缀 | jdbc:postgresql:// | jdbc:kingbase8:// | 必须修改 |
| 数据库类型 | DbType.POSTGRESQL | DbType.KINGBASE_ES | 必须修改 |
| 连接参数 | currentSchema=public | currentSchema=public&searchPath=public | 建议添加 |
| 分页方言 | PostgreSqlDialect | KingbaseESDialect | 如果自定义方言 |
| 主键生成 | @TableId(type = IdType.AUTO) | @TableId(type = IdType.AUTO) | 序列需要调整 |
6.4 常见错误与解决方案
错误1:relation "table_name" does not exist
org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: org.postgresql.util.PSQLException: ERROR: relation "sys_user" does not exist原因:没有指定schema前缀。解决:在表名前加上public.,或者配置全局schema。
错误2:syntax error at or near "level"
org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: org.postgresql.util.PSQLException: ERROR: syntax error at or near "level"原因:level是Kingbase的保留字。解决:修改字段名,或者用双引号包裹字段名"level"。
错误3:function jsonb_build_object() does not exist
org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: org.postgresql.util.PSQLException: ERROR: function jsonb_build_object() does not exist原因:没有安装jsonb扩展,或者兼容模式不对。解决:确保Kingbase运行在PostgreSQL兼容模式,并安装必要的扩展。
错误4:dbType not support : null
java.lang.IllegalArgumentException: dbType not support : null, url jdbc:kingbase8://localhost:54321/testdb原因:MyBatis Plus没有正确识别数据库类型。解决:在配置中明确指定DbType.KINGBASE_ES。
错误5:连接池耗尽
HikariPool-1 - Connection is not available, request timed out after 30000ms.原因:连接泄漏或者连接数配置不足。解决:检查是否有未关闭的连接,调整连接池参数,增加maximum-pool-size。
迁移完成后,一定要做全面的功能测试和性能测试。功能测试要覆盖所有业务场景,特别是涉及复杂查询、事务、并发操作的场景。性能测试要对比迁移前后的响应时间、吞吐量、资源使用率等指标。
我最近的一个项目,迁移后平均响应时间从原来的45ms增加到52ms,增加了约15%。经过优化(主要是调整索引和查询语句),最终降到48ms,基本可以接受。如果性能下降太多,需要深入分析执行计划,看看是不是统计信息不准确或者索引失效了。
还有个建议:迁移最好分阶段进行。先迁移非核心业务,验证没问题后再迁移核心业务。可以用读写分离的方式,写操作还在PostgreSQL,读操作切到Kingbase,观察一段时间再完全切换。这样即使有问题,也能快速回滚。