mysql数据库迁移到瀚高数据库的完整踩坑记录:从数据搬迁到tomcat部署的一次性梳理
如果你现在的任务是把线上正在跑的MySQL数据库迁移到瀚高数据库,同时对后续的应用部署、Tomcat启动、SQL兼容性改造一脑子问号,那这篇文章应该正是你需要的。我前前后后做过几轮类似项目,踩过的坑基本都集中在几个地方:表结构转换、数据搬运方式、SQL语法差异、JDBC驱动与连接池配置、以及Tomcat部署war包后各种起不来的问题。这篇就把这些整理成一份能照着干的实操笔记。
先说结论:瀚高数据库不是MySQL的“换皮”,它的内核是基于PostgreSQL体系的,所以几乎所有MySQL习惯性的写法搬到瀚高之后都要经过一轮改造。整个迁移过程里,数据本身往往不是最麻烦的,真正耗时的是“让原来的SQL和应用跑在新数据库上还能有一样的表现”。下面我按迁移的推进顺序来写:前期准备、表结构与数据迁移、SQL语法改造、应用部署与Tomcat问题、常见问题排查。
1. 迁移前的整体思路与前期准备
很多人接到“把MySQL迁到瀚高”这个需求后,第一反应是找工具把数据导过去,导完发现应用起不来,再回头补SQL兼容和工作,最后整个项目周期被拖得很长。这其实是在前期没有把“数据库切换”当成一个系统工程来对待。《MySQL数据库迁移到瀚高数据库》这个标题看起来只有一句话,实际覆盖的范围包括:数据库对象转换、存量数据搬迁、SQL兼容、应用配置修改、服务启动验证、Tomcat容器侧改造。任何一环漏了,后面都会在测试环境或生产环境炸给你看。
1.1 瀚高数据库是什么,为什么语法和数据迁移要单独处理
瀚高数据库(HighGo Database)在国内数据库市场里属于老牌厂商,它的一条主要产品线就是基于PostgreSQL内核做的企业版数据库。这一点很关键,因为它决定了后面所有的技术判断:你在MySQL上写习惯的那些SQL,无论是函数、系统表、存储过程还是自带工具,大部分都不能直接平移到瀚高上。MySQL和PostgreSQL是两套不同的SQL实现,它们的引擎设计、类型系统、函数库、事务机制、索引结构都有差异。瀚高继承了PG体系,所以迁移的本质是“MySQL到PostgreSQL”的迁移,而不是简单的“换一个连接串”。只有提前把这一点想清楚,你才会理解为什么要做建表语句转换、为什么要改JDBC驱动、为什么应用里那些SQL会报错。
另外还要区分一个概念:瀚高有面向不同场景的版本,有些是兼容Oracle模式的,有些是兼容PostgreSQL模式的。你手上的瀚高数据库是不是开启了Oracle兼容参数,决定了部分语法能不能直接兼容。别假设所有功能默认全开,迁移前先找DBA或官方文档确认版本型号和兼容模式,这个动作能少走很多弯路。
1.2 迁移前要盘点的东西:版本、对象、应用依赖
正式动手迁移之前,我强烈建议先做一次完整的盘点,宁可多花两天,也别直接开干。你需要搞清楚以下几件事。
第一,源端MySQL的版本。不同MySQL版本的SQL行为差异很大,特别是5.7和8.0在窗口函数、通用表表达式、字符集排序规则上都不一样,这会直接影响迁移工具的解析结果和建表语句的转换。第二,数据库对象的清单。我这里的清单不是指“有多少张表”,而是指表、视图、存储过程、函数、触发器、事件、调度任务、自定义函数等全套对象。MySQL里的EVENT(事件调度器)在瀚高里没有对应物,需要改成操作系统的定时任务或应用层任务,这类对象如果你前期没盘出来,后面生产环境的某个功能就会悄悄失效。第三,字段类型和特殊依赖。比如JSON字段、全文索引、空间数据、分区表、视图嵌套、外键级联等,这些在转换时都不是一行命令能搞定的。
应用依赖也要同步盘点。你需要在代码仓库里搜一遍,把所有包含SQL的Mapper文件、JPA实体、存储过程调用、数据库工具脚本都找出来。注意不是只找项目工程目录,还包括jenkins构建脚本里的SQL初始化文件、运维平台上的后台脚本、数据对账脚本。另外还要确认应用的JDBC驱动版本、连接池用的是DBCP还是Druid还是HikariCP、ORM框架是MyBatis还是Hibernate,这些都会影响后面改造的方案。
1.3 迁移工具和方案选型
迁移方案没有银弹,我实际用下来比较靠谱的路线是:小数据量(几百MB以内)靠官方迁移工具或手工SQL转换;中等数据量用ETL工具抽数据;大数据量(几十GB以上)建议分表分批处理,配合文件导入导出。之所以不把宝全部押在一个工具上,是因为工具只能处理“表结构和数据的规则性转换”,碰到存储过程、触发器、复杂视图这种逻辑型对象,基本都要人工介入。
当前瀚高官方有提供配套的迁移工具,它能够读取MySQL的元数据和建表语句,自动生成对应瀚高的DDL,同时支持历史数据的迁移。这个工具的优点是对数据类型的映射内置得比较全,比如TINYINT转SMALLINT、DATETIME转TIMESTAMP这些都是现成规则。缺点也很明显,它对SQL函数和存储过程基本无能为力,这一块必须靠人改。所以我的建议是:工具用来做“批量搬运”,人的精力用来做“逻辑改造”,两边配合才能控制好迁移质量。
如果你的数据量不大,也可以直接走脚本路线。先用mysqldump把结构和数据导成SQL文件,然后用文本处理把MySQL特有的语法替换掉,最后用psql导入瀚高。这种方法最笨但最可控,适合数据量在几万行以内、表结构清晰的项目。数据量再大一点,我会优先考虑把数据导成CSV,然后用瀚高提供的COPY命令批量导入,速度比逐条INSERT快非常多。
2. 表结构和存量数据迁移实操
这个阶段是整个迁移的核心环节。很多人卡在“数据明明导过去了,但应用查询报错”这上面,本质是因为表结构转换时丢了类型、默认值、约束或者注释信息。下面我把建表转换和数据搬运的实操过程拆开讲,并给出我验证过的做法。
2.1 建表语句怎么从MySQL改成瀚高
先看一个最常见的用户表例子。MySQL下的建表语句大概是这样的:
CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL COMMENT '用户名', password VARCHAR(128) DEFAULT NULL, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段SQL拿到瀚高里直接执行,前几个错误就够你受的:AUTO_INCREMENT不识别、COMMENT关键字不识别、KEY关键字在PG系列数据库里不识别、ENGINE子句不识别。需要转换成下面这样:
CREATE TABLE sys_user ( id BIGSERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(128) DEFAULT NULL, status SMALLINT DEFAULT 1, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_username ON sys_user (username); COMMENT ON COLUMN sys_user.id IS '主键';这里有几个转换规则需要重点记。自增列在MySQL里用AUTO_INCREMENT,在瀚高里可以用BIGSERIAL或者GENERATED BY DEFAULT AS IDENTITY。我建议新表尽量用IDENTITY,因为它是SQL标准写法,可读性更好,而且后期如果要迁移到其他PG系数据库也不容易出问题。如果你的表已经建好并且有存量数据,用SERIAL也没关系,但要记得把序列的当前值调到已有数据的最大值后面,不然插入新记录时会报主键冲突。
字段类型映射方面,我给出一份我项目里实践过的映射表,可以参考:
| MySQL字段类型 | 瀚高字段类型 | 备注 |
|---|---|---|
| TINYINT | SMALLINT | MySQL的TINYINT(1)常用来表示布尔,这里也可以考虑转BOOLEAN |
| INT / INTEGER | INTEGER | 正常转换 |
| BIGINT | BIGINT | 注意MySQL的BIGINT UNSIGNED在瀚高里没有完全对应的无符号类型 |
| VARCHAR(n) | VARCHAR(n) | 注意MySQL里VARCHAR长度按字符算,PG也按字符算,基本兼容 |
| TEXT | TEXT | 两者都是可变长文本,但PG的TEXT性能优于VARCHAR超长场景 |
| DATETIME | TIMESTAMP | MySQL DATETIME不包含时区,对应TIMESTAMP WITHOUT TIME ZONE |
| TIMESTAMP | TIMESTAMP WITH TIME ZONE | 建议显式指定,避免时区问题 |
| DATE | DATE | 一致 |
| DECIMAL(p,s) | DECIMAL(p,s) | 一致,但注意取值范围有差异 |
| BLOB / LONGBLOB | BYTEA | PG里处理二进制用的是BYTEA |
| JSON | JSONB | JSONB支持索引和高效检索,推荐使用 |
字段类型转换只是第一步,默认值和约束也要一起处理。MySQL里常见的DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,在瀚高里CURRENT_TIMESTAMP可以直接用,但ON UPDATE CURRENT_TIMESTAMP是不支持的。这个行为差异很容易被忽略:MySQL里更新一行数据时,update_time字段会自动刷新,迁到瀚高后这个刷新不会发生,需要在应用的UPDATE语句里显式给update_time赋值,或者用触发器来实现自动更新。我见过不止一个项目上线后才发现“为什么编辑资料后修改时间没有变化”的问题,根因就在这里。
2.2 数据搬运的几种方式和一条实测可用的路径
表结构转换完后,就是存量数据迁移。如果表数量不多、数据量也不大,最简单的方式是生成INSERT语句,然后直接在瀚高里执行。但需要注意,MySQL导出的INSERT语句可能带反引号、带ENGINE=InnoDB等语句碎片,不能直接扔给瀚高。更稳妥的办法是让迁移工具来生成INSERT,它会把不兼容的语法过滤掉。
数据量稍大时,我建议走CSV导出再导入的路线。在MySQL侧可以用命令行或客户端把表数据导出成CSV,注意导出时统一编码为UTF-8,日期格式建议写成YYYY-MM-DD HH:MM:SS这种PG能直接识别的形式。然后在瀚高里使用COPY命令批量导入,比如:
COPY sys_user FROM '/data/sys_user.csv' WITH (FORMAT CSV, HEADER true, DELIMITER ',', NULL 'NULL');这里有一个容易被坑的点:CSV里的NULL值表示方式。MySQL导出时通常会把NULL导出为“NULL”字符串(不带引号),或者导出为空。导入时要通过NULL参数告诉瀚高哪种字符代表空值,否则NULL会变成真正的字符串“NULL”,导致查询条件IS NULL失效。我在一次物料主数据迁移中就遇到过这个问题,花了半天才发现。
数据量再大的话,建议分批迁移。比如按主键范围分批,每批5万到10万行,避免一次事务过大导致数据库内存膨胀或日志暴涨。分批迁移的另一个好处是中途出错时只需要重跑失败的那一批,不用全部推倒重来。
2.3 迁移后的校验、序列和自增列对齐
数据导进去只是开始,校验才是避免事故的关键。最基本的校验是行数比对:对每张表分别统计迁移前后的COUNT()。这里要特别说明,COUNT()会做全表扫描,表很大的时候会消耗不少时间,但比起上线后才发现数据对不上,这点代价太值了。如果你的表有分页或对账需求,还可以对关键表做SUM汇总比对,比如金额字段、数量字段的合计值是否一致。
更精细的校验是抽样比对。随机抽取几条记录,把源端和目标端的字段值进行逐字段比对,重点看:时间字段是否有时区偏移、NULL值有没有变成空字符串、字符集有没有乱码、精度较高的DECIMAL是否被截断。我自己习惯写几个简单的SQL把抽样结果导成Excel再人工比对,虽然土,但很有效。
自增列对齐这一步经常被忽略。BIGSERIAL或IDENTITY生成的序列在导入数据后,序列的当前值不会自动跳到已有数据的最大值。比如你在MySQL里已经有了id=1000的数据,导入瀚高后序列还是从1开始,那么插入新记录时就会尝试插入id=1,与已有的id冲突,直接报主键重复。需要在导入后手工调整序列的当前值。瀚高里序列名通常是“表名_字段名_seq”,可以这样处理:
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));这条命令的意思是:将序列的值设置为当前表里最大id,再继续递增时就不会碰到已有值了。如果是多张自增表,建议写一个脚本批量执行,避免漏掉某张表。
3. SQL语法差异:从MySQL思维切到瀚高思维
如果数据迁移是体力活,那SQL语法改造就真的是脑力活了。MySQL和瀚高都是关系型数据库,但两者的语法风格差异非常大。下面我把实际工作中最常遇到的差异集中列出来,这些内容我建议你直接转给负责改代码的同学看。
3.1 最基础也最容易翻车的三个点:引号、大小写、关键字
先看引号问题。MySQL里反引号用来表示表名和字段名,双引号可以用来包裹字符串,单引号也可以。例如SELECT * FROMorderWHERE status = "active"这种写法在MySQL里完全能跑。但瀚高继承的PG体系里,反引号不是默认的标识符引用符号,直接使用会报语法错误;双引号只用来包标识符,字符串必须用单引号。也就是说,原来字符串用的是双引号,到了瀚高要么换成单引号,要么就用美元符包围。这个问题在MyBatis的XML里尤其隐蔽,因为XML转义和SQL引号混在一起,很容易看漏。
大小写问题也很微妙。MySQL在Windows系统下表名通常不区分大小写。瀚高里表名和字段名在没有双引号包裹时会被自动转为小写,所以你建表时写了CREATE TABLE UserInfo(...),默认生成的是小写表名userinfo。反过来,如果你在查询里写FROM UserInfo,它也会转成小写去找userinfo,通常没问题。但如果你用双引号包裹了大小写混合的表名,比如FROM "UserInfo",那就只能精确匹配"UserInfo",大小写敏感。这种不一致很容易导致本地联调正常、测试环境报“表不存在”的诡异问题。
关键字冲突也要留意。MySQL的表或字段叫order、group、system、condition是很常见的事。瀚高里很多词是保留字或非保留关键字,直接使用会报语法错误。保险的做法是建表时尽量避开这些词,如果绕不开,在SQL里用双引号包住标识符。我建议在代码库搜索所有包含保留字的SQL,逐一修改,别等到跑起来再处理。
3.2 高频函数替换对照表
函数是重灾区。下面整理一份我在实际项目中经常用到的替换对照表,压测过的那些都验证过可用性,但不同瀚高版本可能有细微差异,遇到报错先查官方手册。
| MySQL写法 | 瀚高推荐写法 | 说明 |
|---|---|---|
| IFNULL(a, b) | COALESCE(a, b) | 函数名不同,行为基本一致 |
| IF(cond, a, b) | CASE WHEN cond THEN a ELSE b END | 没有IF函数,只能用CASE表达式 |
| NOW() / SYSDATE() | NOW() / CURRENT_TIMESTAMP | 都可以用,但注意返回类型是否带时区 |
| DATE_FORMAT(d, '%Y-%m-%d') | TO_CHAR(d, 'YYYY-MM-DD') | 日期格式化函数差异大,格式符完全不一样 |
| GROUP_CONCAT(x SEPARATOR ',') | STRING_AGG(x, ',') | 名称和顺序都不一致,注意结果排序规则 |
| SUBSTRING_INDEX(s, d, n) | SPLIT_PART(s, d, n) | 拆分字符串时用 |
| UUID() | gen_random_uuid() | 需要先CREATE EXTENSION IF NOT EXISTS pgcrypto |
| LIMIT offset, count | LIMIT count OFFSET offset | LIMIT后面的参数顺序相反,很多人在这里栽跟头 |
| INSERT ... ON DUPLICATE KEY UPDATE | INSERT ... ON CONFLICT(col) DO UPDATE SET ... | 语法差别非常大,要重写 |
| REPLACE INTO tbl ... | INSERT ... ON CONFLICT ... DO UPDATE | REPLACE INTO是MySQL专用语法,直接不支持 |
| FIND_IN_SET(a, b) | 改成数组或LIKE逻辑 | 尽量在应用层处理,数据库层没有等价函数 |
以UUID为例说一下。MySQL里可以直接用UUID()生成UUID字符串,但瀚高里默认没有这个函数。如果你需要在应用里生成UUID主键,首选全局唯一的UUID方式:先在瀚高里执行CREATE EXTENSION IF NOT EXISTS pgcrypto;,之后就可以使用SELECT gen_random_uuid();返回一个UUID类型值。如果你在建表时把主键类型定为了VARCHAR(36),那么插入时要显式转一下字符串,例如CAST(gen_random_uuid() AS VARCHAR),否则类型匹配会报错。另外要注意,uuid-ossp扩展里还有一个uuid_generate_v4()函数,功能和gen_random_uuid()类似,但如果你只安装了pgcrypto就不能用前者,两者别混着来。
GROUP BY的严格模式也必须提前适应。MySQL在关闭ONLY_FULL_GROUP_BY以后,允许SELECT出不在GROUP BY里的列,很多老项目都依赖这一点。但瀚高体系下GROUP BY非常严格,SELECT列表里的非聚合列必须全部出现在GROUP BY中,否则直接报错。这个差异会炸出一大片统计查询,特别是那些图省事的报表SQL。改造思路有两种:要么把漏加的字段补进GROUP BY,要么用聚合函数包一层,比如MAX、MIN,确保每个字段都有确定的来源。
3.3 存储过程、视图、触发器的改造思路
存储过程是迁移里成本最高的部分。MySQL的存储过程语法和PG/瀚高的PL/pgSQL差异非常大,基本没有自动转换的工具。比如MySQL里用DELIMITER定义结束符,用BEGIN...END包整个逻辑,参数不带IN/OUT的时候默认为IN;而瀚高里需要定义CREATE OR REPLACE FUNCTION或者PROCEDURE,用$$包住函数体,还要声明返回类型。
MySQL的一个典型存储过程:
DELIMITER // CREATE PROCEDURE get_user(IN uid INT) BEGIN SELECT * FROM sys_user WHERE id = uid; END //改造到瀚高里,如果只是查一条数据并返回结果集,简单做法是写成返回TABLE的函数:
CREATE OR REPLACE FUNCTION get_user(uid INT) RETURNS TABLE(id BIGINT, username VARCHAR) AS $$ BEGIN RETURN QUERY SELECT sys_user.id, sys_user.username FROM sys_user WHERE id = uid; END; $$ LANGUAGE plpgsql;如果你数据库里的存储过程特别多,一次性改造不现实,那就排优先级:先改核心业务链路上的,再把边角功能的存储过程往后排。改造过程中,建议把存储过程里的动态SQL、游标、异常处理逻辑单独抽出来测试,这三块最容易因为语法差异而失败。特别注意MySQL里的DECLARE CONTINUE HANDLER FOR NOT FOUND和PG里的WHEN NO_DATA_FOUND异常处理是完全不同的写法。
视图的转换相对简单一些,但也要小心。MySQL视图里常见的IFNULL、DATE_FORMAT、反引号、函数差异都会在创建视图时报出来。我的习惯是把所有视图的创建SQL收集起来,在瀚高里一个个执行,报错就改,直到全部创建成功。创建成功后,再针对关键视图做一个查询测试,确保查出来的列名和类型与应用期望一致。
触发器建议能不用就不用。MySQL触发器通常用于审计日志、更新时间字段自动维护、级联统计等场景。迁移到瀚高后,语法要改成PG风格,例如用EXECUTE FUNCTION而不是EXECUTE PROCEDURE,事件和时机也略有差异。如果应用可以通过代码逻辑替代触发器,我建议尽量在应用层做,毕竟数据库上的触发器迁移成本高、测试成本更高,而且多一个触发器就多一份性能损耗。
4. 应用侧改造与Tomcat部署问题
数据库切换不只是改几个SQL,整个应用侧的配置都要跟着变。这一步牵扯到JDBC驱动、连接池、Tomcat数据源配置、war包部署等等。如果你用的是Spring Boot,改造会相对集中;如果是传统Tomcat部署war包的架构,那要改的地方就比较分散,问题也更容易出在“改了这个忘了那个”上。
4.1 JDBC驱动、连接串和数据源配置
应用连接MySQL时,常见的连接信息是jdbc:mysql://127.0.0.1:3306/mydb,驱动类是com.mysql.cj.jdbc.Driver。切换到瀚高后,需要替换为瀚高提供的JDBC驱动,通常连接串是jdbc:highgo://127.0.0.1:5866/mydb,驱动类是com.highgo.jdbc.Driver。端口号要以你实际安装瀚高时的配置为准,默认常见的是5866,但我不排除其他自定义端口,你可以在瀚高的服务配置或连接工具里确认。
如果项目使用Spring的XML配置数据源,那么改造大概是这样的:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.highgo.jdbc.Driver" /> <property name="url" value="jdbc:highgo://127.0.0.1:5866/mydb" /> <property name="username" value="youruser" /> <property name="password" value="yourpassword" /> <property name="validationQuery" value="SELECT 1" /> </bean>如果你用的是Druid,还需要关注Druid针对PG系数据库的wall-filter规则。Druid的防火墙默认支持MySQL语法,但切到瀚高后,可能会拦截一些PG系原生的SQL写法,比如ON CONFLICT、RETURNING等。遇到这种情况,要么升级Druid版本并配置对应的dbType,要么把wall-filter的拦截等级调低。我遇到过一次线上问题,POST请求偶尔报SQL被拦截,排查半天发现就是Druid的wall把ON CONFLICT当成非法语法拒绝了。
如果是Spring Boot项目,多数人用的是HikariCP连接池。HikariCP对数据库类型不算太敏感,配置driverClassName和jdbcUrl即可。但如果你是JPA/Hibernate项目,一定要把方言换成PG体系的方言,例如:
spring: datasource: driver-class-name: com.highgo.jdbc.Driver url: jdbc:highgo://127.0.0.1:5866/mydb username: youruser password: yourpassword jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect方言不换的话,Hibernate在生成分页SQL、序列策略、类型映射时都会沿用MySQL的逻辑,轻则性能异常,重则直接启动失败。这一点在JPA项目里特别关键,别漏。
4.2 连接池与Tomcat数据源配置
传统的Tomcat项目,经常会在META-INF/context.xml或Tomcat的conf/context.xml里配置JNDI数据源。以前连接MySQL的配置长这样:
<Resource name="jdbc/mydb" auth="Container" type="javax.sql.DataSource" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/mydb" username="root" password="123456" maxTotal="30" maxIdle="10" maxWaitMillis="10000" validationQuery="SELECT 1" />切换到瀚高后,需要把driverClassName、url、username、password全部替换。驱动jar包也要放到Tomcat的lib目录或应用的WEB-INF/lib目录下。有一点要特别注意:如果是不同应用共用同一个Tomcat实例,驱动jar最好放在Tomcat的lib目录里,避免每个war包各自带一份驱动,造成版本冲突。如果你只在一个应用里切换数据库,把驱动放到WEB-INF/lib反而更省心,升级驱动版本时也不影响Tomcat上的其他项目。
数据源配置中的validationQuery统一用SELECT 1就够了,MySQL和瀚高都能执行。连接池的最大连接数要根据业务量重新估,不建议沿用MySQL时期的参数。我见过一个项目,MySQL时期maxActive=100,迁移到瀚高后没改参数,结果压测时瀚高数据库连接数直接打满,数据库CPU飙升。瀚高这类PG系数据库对连接数的消耗策略和MySQL不完全一样,建议从原来的连接数往下调一些,配合应用层的连接池监控逐步调整。
4.3 Tomcat部署war包与常见启动异常
Tomcat部署war包这一块,很多问题不在数据库本身,而是新旧环境切换后Tomcat的配置、目录权限、端口占用一起炸。最常见的几个现象我列一下。
第一,Tomcat启动一闪而过。这种情况九成是JAVA_HOME没有配置,或者配置的Java版本与Tomcat要求不一致,跟数据库迁移没有直接关系,但会出现在你切换测试环境时。处理方法很简单:打开命令行,直接运行catalina.bat run(Windows)或catalina.sh run(Linux),这样错误日志会直接打印到控制台,比看一闪而过的窗口靠谱得多。
第二,部署war包后访问返回404。war包放进webapps后,Tomcat会在启动时自动解压。如果老是404,先看webapps目录下有没有生成同名的目录。如果war包没有解压,可能是war包本身损坏、磁盘空间不足、或者Tomcat的临时目录没有写入权限。如果解压了但还是404,检查一下项目访问路径是否正确:假设war包名为demo.war,那么默认访问地址是http://ip:8080/demo/,直接访问根路径有时会找不到资源。
第三,启动时日志报数据源初始化失败。这类错误基本都指向数据库连接没通。常见的报错有ClassNotFoundException: com.highgo.jdbc.Driver,说明驱动jar没放对位置,或者jar包没打进去;还有Cannot create JDBC driver of class '' for connect URL,说明连接串里的驱动类和URL格式不匹配。这些错误通常会在Tomcat启动到一半时抛出来,但应用本身不一定崩,而是启动后很多功能不可用。
第四,端口占用导致启动失败。Tomcat默认会有三个端口:8005(Shutdown端口)、8080(HTTP端口)、8009(AJP端口)。MySQL时代的机器上可能已经跑着别的Tomcat实例,切换部署时新老Tomcat端口冲突非常常见。处理办法是打开conf/server.xml,把三个端口分别改掉,确保和现有服务不冲突。改完后重启,再执行netstat或lsof确认端口已在监听。
4.4 启动起来之后还要检查的东西
应用能启动不代表迁移完成,我建议启动后做一轮接口冒烟测试,重点测这几类功能:登录接口(涉及用户表和时间字段)、列表分页接口(涉及LIMIT/OFFSET语法)、报表统计接口(涉及GROUP BY和聚合函数)、带日期条件的查询(涉及日期格式和时区)、涉及自动生成主键的新增操作(验证序列是否已对齐)。这些功能在迁移前最好先写成一个测试用例清单,迁移后对着清单逐项勾选。
如果是Hibernate/JPA项目,还要看一眼控制台日志里有没有打印出错误的SQL方言警告。Hibernate启动时会根据方言生成数据库schema检查语句,如果方言不对,日志里通常会看到类似dialect或sequence相关的警告。出现这类警告时,哪怕应用没崩,也要尽早处理,不然后面分页查询、主键生成策略会陆续出问题。
5. 常见问题与排查实录
最后这部分,我把在迁移和部署过程中真正遇到过、也排查过的问题整理成一个速查表,并挑两个印象最深的现场案例详细说说。这里的经验都是花时间换来的,你可以直接当成排查手册用。
5.1 问题速查表
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 建表报错AUTO_INCREMENT无法识别 | MySQL自增语法未转换 | 改为BIGSERIAL或IDENTITY |
| 建表报错COMMENT语法错误 | MySQL字段注释语法与瀚高不同 | 建表后再单独执行COMMENT ON COLUMN |
| 插入数据报主键重复 | 序列当前值没有跳到已有数据最大值 | 执行setval重置序列 |
| 查询报错42P01 relation does not exist | 表名大小写不匹配 | 检查应用SQL里有没有大小写混合表名,或是否少了schema前缀 |
| 查询报错function ... does not exist | MySQL函数没有对应瀚高函数 | 按函数替换表改造SQL |
| 查询报错column ... must appear in the GROUP BY clause | GROUP BY严格模式差异 | 补GROUP BY字段或改用聚合函数 |
| Tomcat启动一闪而过 | JAVA_HOME配置或端口冲突 | 命令行运行catalina run查看日志 |
| 部署war后404 | war包未解压或访问路径不对 | 检查webapps目录,确认访问路径带项目名 |
| 数据源初始化失败 | 驱动jar缺失或连接串错误 | 确认驱动jar位置、driverClass、URL格式 |
| 中文乱码 | 字符集或JDBC编码配置不一致 | 统一数据库UTF-8,连接串添加characterEncoding=utf-8 |
| 批量插入很慢 | 事务提交过于频繁 | 分批提交,或使用COPY命令导入 |
5.2 我印象最深的两个现场排查
一个是对账脚本怎么也跑不对。当时业务方反馈月底对账数据差几万条,我排查发现对账SQL用了一个字段做GROUP_CONCAT拼接,在MySQL里GROUP_CONCAT默认按出现顺序拼接,但到了瀚高,STRING_AGG默认并不保证顺序一致,结果导致拼接出来的明细对不上。后来在SQL里加了对排序字段的ORDER BY子句才解决。这个问题的教训是:凡是依赖结果顺序的函数,迁移后都要明确指定排序规则,不要依赖数据库的默认行为。
另一个是Tomcat启动后应用反复重启。后来发现是另一个运维同事修改了Tomcat的JVM参数,把堆内存调得太小,应用加载JDBC驱动和数据源的过程中触发OOM,Tomcat自动重启。这不是数据库本身的问题,但在切换数据库的敏感期特别容易混淆。排查这类问题一定要先看Tomcat的catalina.out日志,找到OOM或OutOfMemoryError之类的关键字,再往JVM参数方向查,而不是一头扎进数据库。
最后再分享一个我个人的经验:整个迁移项目里,真正难的不是某一个点,而是把“数据库转换、SQL兼容、应用配置、部署运维”这条链路的每个环节都验证到位。建议在项目里面预留一个专门的环境,只用来做迁移验证,不要和日常开发环境混在一起。这样出了问题也好定位,不会因为环境干扰而分不清到底是代码问题还是数据库问题。按照上面这个顺序一步一步走,该验证的验证,该改的改,最后上线时的心态会稳很多。