前阵子接了个任务,把 XXL-JOB 任务调度平台从 MySQL 平滑迁移到人大金仓数据库 KingbaseES。说实话,刚接到这种需求时心里多少有点底,因为 XXL-JOB 本身的设计是支持多数据库的,但真正动手以后才发现,从 MySQL 方言切到金仓方言,坑比想象中多得多。这篇文章把这次适配的完整过程拆开讲一遍,从表结构脚本改造、驱动配置、MyBatis 方言修正,到登录不上、时间不对、分页失败这些实战问题,都会聊到。不管你是被公司信创需求推着走,还是单纯想让调度系统跑在国产数据库上,这份记录应该能帮你少走不少弯路。
1. 适配前先想清楚:XXL-JOB 到底依赖 KingbaseES 的哪些能力
1.1 为什么必须做数据库适配,而不是直接换平台
很多人一听到“适配人大金仓”第一反应是:XXL-JOB 不是支持 MySQL、PostgreSQL、Oracle 吗?它天然不就支持金仓吗?这个想法对了一半。KingbaseES 确实同时兼容 PostgreSQL 和 Oracle 语法,但它不是原封不动的 PostgreSQL,尤其当你选择了不同的兼容模式时,行为差异会很具体。
而 XXL-JOB 的 admin 管理端在存储层做了大量工作,它需要数据库提供这几种能力:
- 任务信息、调度日志、注册节点、用户权限这些业务表的读写。
- 调度锁表的行级锁能力,也就是
SELECT ... FOR UPDATE,多个调度中心实例靠它抢锁。 - 分页查询能力,调度日志列表、任务列表都依赖分页 SQL。
- 事务和自增主键,批量插入和更新必须在一个事务里可靠执行。
- 一些聚合函数和日期函数,调度报表图表要按天统计。
这些需求在 KingbaseES 的 PostgreSQL 兼容模式下基本都能满足,但前提是你得把建表 SQL、JDBC 驱动、连接参数、部分 MyBatis XML 里的 SQL 方言都调成金仓认识的写法。
1.2 适配工作拆开来看,其实就四层
我习惯在动手前把改动面列出来,避免改到一半发现漏了某个环节。这次适配分解下来是四块:
- 表结构脚本:官方提供的
tables_xxl_job.sql默认是 MySQL 方言,需要转成 KingbaseES 兼容的建表语句。 - 驱动与连接:把 MySQL 驱动换成金仓的 JDBC 驱动,改数据源 URL 和连接池配置。
- MyBatis Mapper 中的 SQL 方言:XXL-JOB 的 mapper XML 里有部分 SQL 带有明显的 MySQL 痕迹,比如
LIMIT offset, size、IF()这种写法,在 PostgreSQL 兼容模式下跑不通,得改。 - 初始化数据与权限:admin 用户密码、调度锁记录等种子数据要重新灌入,还要注意大小写敏感问题。
很多人只盯着第一层,结果驱动也换了、库也建了,启动后功能一用就崩,基本都崩在后三层的细节上。
1.3 版本和模式选择,直接决定后续工作量
先说版本。我这里用的是 XXL-JOB 2.4.1 和 KingbaseES V8(即 KingbaseES V8R6 的常见发行版),JDK 1.8,Spring Boot 2.7 这套组合。不同小版本的表结构会有一点差异,但改造思路是通用的。
再说金仓的数据库兼容模式。KingbaseES 安装时可以初始化成 Oracle 模式、PostgreSQL 模式或 MySQL 模式。XXL-JOB 的 SQL 逻辑里大量使用序列、行级锁、标准分页这些能力,我强烈建议直接用PostgreSQL 兼容模式。原因后面会讲,比如 MySQL 模式虽然能兼容 XXL-JOB 的分页 SQL,但会让字符串比较默认变成不区分大小写,直接导致登录口令校验出问题,这种暗坑比改 SQL 还难受。
2. 环境准备:用 Docker 快速拉起一个可用的 KingbaseES
2.1 拉镜像和启动容器,别在这一步就卡住
上手阶段问题最多的是“金仓怎么装”。如果你只是做适配验证,直接用 Docker 最省事,不用担心系统和依赖。人大金仓官方镜像的拉取命令是docker pull kingbase/kingbase这种形态,实际以你本地能访问到的镜像仓库为准。启动容器时建议把端口映射到主机的54321,因为这是金仓的默认端口,和 PostgreSQL 的 5432 不一样,后面配 JDBC URL 时要用到这个端口。
docker run -d \ --name kingbase \ -p 54321:54321 \ -e ENABLE_CREATE_DATABASE=true \ -e SYSTEM_PASSWORD=123456 \ kingbase/kingbase这里有个细节值得注意:金仓超级用户默认叫SYSTEM,不是postgres,密码通过SYSTEM_PASSWORD指定。容器起来后,先确认日志里出现“database system is ready to accept connections”再继续,别拿了个没启动好的容器折腾半天。
2.2 建业务库和账号,权限分配要一步到位
首次连接用SYSTEM账号进去,创建一个独立的业务库和专用账号,不要让任务调度系统直接用超级用户跑。金仓里建库建账号的语法和 PostgreSQL 很像,下面这段可以照搬:
CREATE USER xxljob WITH PASSWORD 'xxljob@2024'; CREATE DATABASE xxl_job OWNER xxljob ENCODING 'UTF8'; GRANT ALL PRIVILEGES ON DATABASE xxl_job TO xxljob;创建数据库时明确指定UTF8编码,后续任务描述、告警邮箱这些字段里如果有中文,不容易出现乱码。另外注意,如果初始化时没有开启ENABLE_CREATE_DATABASE,容器默认可能连CREATE DATABASE都不让执行,这种情况要么重建容器,要么手动用createdb命令操作。
2.3 先验证驱动连通性,再进应用改造
环境准备阶段最好就把 JDBC 连通性验证掉,而不是等改完代码才发现驱动不对。金仓 V8 对应的 JDBC 驱动类是com.kingbase8.Driver,URL 格式是jdbc:kingbase8://localhost:54321/xxl_job,这一点和 MySQL 的com.mysql.cj.jdbc.Driver完全不同。你可以写一个最朴素的 JDBC 测试类,加载驱动、拿连接、执行SELECT 1,确认三件事:驱动 jar 能被加载、密码认证通过、目标库能连通。
连接通了,环境准备就算完成,接下来进入核心的脚本改造环节。
3. 核心改造:表结构与初始化数据的方言迁移
3.1 官方建表脚本到底长什么样,为什么不能直接用
XXL-JOB 官方的表结构脚本放在xxl-job-admin/src/main/resources/tables/tables_xxl_job.sql里,默认是为 MySQL 准备的。你会看到大量这类写法:
CREATE TABLE `xxl_job_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `job_group` int(11) NOT NULL, `job_desc` varchar(255) NOT NULL, `add_time` datetime DEFAULT NULL, ... `trigger_status` tinyint(4) NOT NULL DEFAULT '0', `trigger_last_time` bigint(20) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `I_trigger_status` (`trigger_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这份脚本你直接扔给 KingbaseES 执行,大概率会报错。反引号不是金仓的引用符号,ENGINE=InnoDB和DEFAULT CHARSET这种表选项在金仓里不存在,int(11)这种显示宽度设置也没有意义。所以要动手改的不只是几个单词,而是整套建表方言。
3.2 MySQL 方言到 KingbaseES 的替换规则,背下这一张表就够
我把自己在改造中实际用到的替换规则整理成了一个对照表,你照着改基本不会错:
| MySQL 写法 | KingbaseES(PostgreSQL 兼容模式)写法 | 说明 |
|---|---|---|
`id` int(11) NOT NULL AUTO_INCREMENT | id SERIAL PRIMARY KEY | 自增主键用序列实现 |
bigint(20) | BIGINT | 长度参数直接去掉 |
tinyint(4) | SMALLINT | 也可以用BOOLEAN,但为贴近原逻辑用 SMALLINT |
datetime | TIMESTAMP | 时间类型对应到金仓的 TIMESTAMP |
mediumtext | TEXT | Glue 脚本源码用大文本类型即可 |
`表名`反引号 | 不带引号,或用双引号 | 建议统一小写不带引号 |
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | 删除 | 金仓建表不用这些选项 |
KEYxxx(字段) | CREATE INDEX 索引名 ON 表名(字段) | MySQL 的 KEY 实际是索引,语法不同 |
DEFAULT CURRENT_TIMESTAMP | DEFAULT now() | 函数名不同 |
以xxl_job_info为例,改完应该是这个效果:
CREATE TABLE xxl_job_info ( id SERIAL PRIMARY KEY, job_group INT NOT NULL, job_desc VARCHAR(255) NOT NULL, add_time TIMESTAMP, update_time TIMESTAMP, author VARCHAR(64), alarm_email VARCHAR(255), schedule_type VARCHAR(50) NOT NULL DEFAULT 'NONE', schedule_conf VARCHAR(128), misfire_strategy VARCHAR(50) NOT NULL DEFAULT 'DO_NOTHING', executor_route_strategy VARCHAR(50), executor_handler VARCHAR(255), executor_param VARCHAR(512), executor_block_strategy VARCHAR(50), executor_timeout INT NOT NULL DEFAULT 0, executor_fail_retry_count INT NOT NULL DEFAULT 0, glue_type VARCHAR(50) NOT NULL, glue_source TEXT, glue_remark VARCHAR(128), glue_updatetime TIMESTAMP, child_jobid VARCHAR(255), trigger_status SMALLINT NOT NULL DEFAULT 0, trigger_last_time BIGINT NOT NULL DEFAULT 0, trigger_next_time BIGINT NOT NULL DEFAULT 0 ); CREATE INDEX idx_job_info_trigger_status ON xxl_job_info(trigger_status); CREATE INDEX idx_job_info_schedule_type ON xxl_job_info(schedule_type);一个容易被忽略的点是索引命名。MySQL 中各表的索引名可以重复,但 KingbaseES 里索引名在同一模式下是全局唯一的,所以我建议给每个索引加上表名前缀,比如idx_job_info_trigger_status,避免撞名。
3.3 初始化数据:锁记录和默认管理员不能少
表结构建好后,还要灌入初始化数据。XXL-JOB 的调度锁机制依赖一张xxl_job_lock表,属于典型的行锁控制。MySQL 脚本里会预先插入一行锁记录:
INSERT INTO xxl_job_lock (lock_name) VALUES ('schedule_lock');这行数据必须存在,调度中心启动后每次抢占调度锁都是对这个lock_name对应的行做SELECT ... FOR UPDATE,没数据就意味着所有调度线程永远竞争不到锁,任务不会触发,这个问题很隐蔽。
另外,管理后台需要初始管理员。XXL-JOB 的用户表里默认账号是admin,密码字段存的是 MD5 值,123456 的 MD5 是e10adc3949ba59abbe56e057f20f883e,插入时照抄即可:
INSERT INTO xxl_job_user (id, username, password, role, permission) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);说到这必须提醒一下:如果金仓初始化的是 PostgreSQL 兼容模式,字符串比较默认区分大小写,这段插入没问题;但如果初始化成了 MySQL 模式,后面登录校验时大小写会被忽略,极易出现密码对但就是登不进去的诡异现象。这一点我在踩坑实录里再细聊。
4. 驱动与配置改造:让 admin 服务真正“认账”金仓
4.1 引入 KINGBASE JDBC 驱动,注意依赖坐标
表结构就绪,下一步改 admin 服务的依赖。把 MySQL 驱动从pom.xml里去掉,换成金仓的 JDBC 驱动。金仓 V8 驱动的 Maven 坐标不是中央仓库直接有的,常见做法是把驱动 jar 安装到本地仓库,或者以系统依赖方式引入。如果你公司私服里有,直接引用即可;没有的话,从安装包里找到kingbase8-8.6.0.jar这类 jar 文件,执行:
mvn install:install-file \ -Dfile=kingbase8-8.6.0.jar \ -DgroupId=com.kingbase8 \ -DartifactId=kingbase8 \ -Dversion=8.6.0 \ -Dpackaging=jar然后在pom.xml里声明依赖就好。
4.2 数据源配置的四个关键参数,一个都不能错
驱动引入后,改application.properties。XXL-JOB admin 默认的数据源配置是 MySQL 的,你要把四行核心配置换掉:
spring.datasource.url=jdbc:kingbase8://localhost:54321/xxl_job spring.datasource.username=xxljob spring.datasource.password=xxljob@2024 spring.datasource.driver-class-name=com.kingbase8.Driver注意这里有两个细节。第一,URL 里的端口是54321,不是常见的 5432;第二,driver-class-name是com.kingbase8.Driver,中间有个8,别把它和 PostgreSQL 的org.postgresql.Driver搞混。写反了或者写漏了,报错信息会很有迷惑性。
4.3 连接池的探活语句和超时参数最好一并调
XXL-JOB 用的是 HikariCP 连接池,默认配置对数据库切换后可能出现“连接假死”的情况。我在适配时把连接池探活参数一起加上:
spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=10 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000connection-test-query=SELECT 1这行很重要。部分数据库驱动在连接被网络断开后不会立刻感知,加了探活语句可以让连接池及时剔除失效连接,避免调度到一半突然连不上库。金仓对这种标准探活 SQL 完全支持。
4.4 还要顺手处理的 MyBatis 方言问题
数据源配好后,不加处理直接启动,大概率会在某些页面崩掉。原因是 XXL-JOB 自带的 mapper XML 里有不少 MySQL 专属语法,最常见的是这两个:
- 分页写法:
LIMIT #{offset}, #{pagesize},PostgreSQL 兼容模式不认这种写法,会报语法错误。 - 条件聚合:比如统计调度日志时用
SUM(IF(handle_code=200, 1, 0)),IF()是 MySQL 函数,金仓的 PostgreSQL 模式不认。
对应的改造分别是把LIMIT #{offset}, #{pagesize}改成LIMIT #{pagesize} OFFSET #{offset},把IF(条件, 真值, 假值)改成标准的CASE WHEN 条件 THEN 真值 ELSE 假值 END。
以XxlJobLogMapper.xml里的分页为例,改前改后对比是这样:
<!-- 改造前,MySQL 写法 --> LIMIT #{offset}, #{pagesize} <!-- 改造后,KingbaseES 兼容写法 --> LIMIT #{pagesize} OFFSET #{offset}聚合函数也一样:
<!-- 改造前 --> SUM(IF(handle_code=200, 1, 0)) as triggerDayCountSuc <!-- 改造后 --> SUM(CASE WHEN handle_code=200 THEN 1 ELSE 0 END) as triggerDayCountSuc这类 SQL 在 XXL-JOB 的 mapper 文件里不止一处,建议全局搜索几个关键字:LIMIT、IF(、DATE_FORMAT、NOW(),逐个检查。如果图省事只改了数据源配置,第一眼可能一切正常,等点开调度日志或报表页就会原地爆发。
4.5 编译打包与启动验证
代码和配置改完后,执行mvn clean package -DskipTests,然后启动 admin 服务。观察启动日志,重点确认这三点:
- 数据源初始化成功,没有“Cannot load driver class”之类的报错。
- Flyway 或初始化脚本正常执行(如果开启了)。
- 调度线程开始运行,日志里出现注册执行器的扫描记录。
启动正常后,先用浏览器打开管理后台,用 admin / 123456 登录。登录通了,再创建执行器、新建任务、手动执行一次,验证整个调用链路。千万别跳过这一步,后面排查大部分问题都靠这个基础链路。
5. 实战踩坑实录:这些坑,我替大家先踩了
5.1 大小写敏感导致“密码正确但登录失败”
这是最典型的暗坑,也正是“kingbase mysql模式字符串不区分大小写”这个热搜词的来源。KingbaseES 在 MySQL 兼容模式下,默认对字符串比较不做大小写区分。也就是说,XXL-JOB 登录时执行WHERE username='admin' AND password='E10ADC...',数据库会把密码和用户名都当成不区分大小写去匹配,看起来好像是“密码随便输都能进”,实际上更麻烦的是它会在不同场景下表现不一致,干扰判断。
我当时为了偷懒把数据库初始化成 MySQL 模式,结果管理后台登录时,明明密码是对的,日志里却反复出现查询结果不一致。后来把模式切成 PostgreSQL 兼容模式,问题立刻消失。
这里给你一个明确建议:不要为了兼容官方 SQL 而选择 MySQL 模式。XXL-JOB 的 SQL 方言在 PostgreSQL 模式下改造成本可控,而字符串大小写语义被悄悄改变的问题会坑到你怀疑人生。如果你的环境必须用 MySQL 模式,那要在登录校验逻辑里显式加上大小写敏感函数(比如LOWER(username)=LOWER(?)这种),否则别碰这个模式。
5.2 自增主键与序列问题:插入时主键冲突
把AUTO_INCREMENT改成SERIAL之后,理论上主键由序列自动生成。但我在个别版本里遇到过插入日志时报主键冲突的情况。原因往往是表结构是从 MySQL 直接迁移过来的,数据里已经有很大的主键值,而序列的起始值还停在 1,于是新插入的数据主键撞上了已有数据。
解决办法是手动把序列同步到当前表的最大 ID:
SELECT setval(pg_get_serial_sequence('xxl_job_log', 'id'), (SELECT MAX(id) FROM xxl_job_log));如果你在建表时用的是GENERATED BY DEFAULT AS IDENTITY这种标准语法,也可以调整IDENTITY的起始值。总之,迁移已有数据时,一定要把序列当前值同步到最大主键值,否则跑一段时间后必然爆炸。
5.3 默认时间字段为 NULL,导致调度时间显示异常
add_time、update_time这类字段在官方 MySQL 脚本里是datetime DEFAULT NULL,转成TIMESTAMP后保留了“默认为空”的语义。问题是 XXL-JOB 某些版本的代码在插入数据时并不显式写入这两个时间字段,依赖数据库的默认值。如果建表时默认值被删了,或者写成了DEFAULT NULL,就会出现任务创建时间全是空的情况。
我建议在建表时把这几个时间字段显式设成默认值:
add_time TIMESTAMP NOT NULL DEFAULT now(), update_time TIMESTAMP NOT NULL DEFAULT now(),这样至少不会出现时间字段为空导致的排序、统计异常。如果你把调度日志表的时间字段也改了,记得一起确认。
5.4 分页查询 500 错误,一个逗号的差距
XXL-JOB 的调度日志查询是典型的分页场景,页面每翻一页就执行一次分页 SQL。默认的LIMIT #{offset}, #{pagesize}在 MySQL 下没问题,在 KingbaseES 的 PostgreSQL 兼容模式下直接报语法错误。这个错误在启动阶段不一定暴露,往往是你点“调度日志”菜单后瞬间出现。
改成LIMIT #{pagesize} OFFSET #{offset}之后,问题解决。这里要特别留神:所有 mapper 里的分页 SQL 都要改,不只是日志表这一处。搜索LIMIT关键字时,把所有出现LIMIT #{xxx}, #{yyy}的地方一次性改成标准写法。
5.5 连接空闲一段时间后,调度突然卡死
这个问题和数据库本身关系不大,更多的是连接池配置。XXL-JOB 调度中心如果长时间空闲,数据库端的连接可能已经被动断开,但连接池不知道。下一次调度触发时,拿到的是已经失效的连接,SQL 执行失败,任务会一直不触发。
罪魁祸首是没配探活语句。在连接池配置里加上spring.datasource.hikari.connection-test-query=SELECT 1,并把空闲超时时间设得合理一点,这个问题就消失了。另外金仓对长时间空闲连接的保持能力和 MySQL 略有差异,所以探活语句几乎是必须的。
5.6 索引名冲突,导致初始化脚本执行失败
前面提到过 KingbaseES 一个模式下的索引名全局唯一,MySQL 则允许不同表之间有同名索引。如果你的初始化脚本是拿官方 MySQL 脚本逐句翻译的,很可能在xxl_job_log和xxl_job_info里都建了名为I_trigger_time的索引,这时候第二张表建索引就会失败。
处理方式不复杂,把索引名全部改成带表名前缀的形式,比如idx_xxl_job_log_trigger_time、idx_xxl_job_info_trigger_time,一劳永逸。
6. 上线前检查清单与调优建议
6.1 调度稳定性清单,逐项确认再上生产
我在这次适配的收尾阶段列了一个自查清单,每一项都是踩过坑才总结出来的,供你直接抄作业:
| 检查项 | 操作建议 |
|---|---|
| 数据库兼容模式 | 统一用 PostgreSQL 兼容模式,避免大小写语义不一致 |
| 自增序列起始值 | 确认所有SERIAL序列已同步到表内最大 ID |
| 时间字段默认值 | 关键时间字段必须DEFAULT now(),避免空值 |
| mapper 分页 SQL | 全局搜索LIMIT #{xxx}, #{yyy},全部改成LIMIT ? OFFSET ? |
IF()函数 | 全局搜索并改成CASE WHEN标准写法 |
| 连接池探活 | 配置connection-test-query=SELECT 1 |
| 日志清理任务 | 确认调度日志清理任务能正常执行,避免日志表无限膨胀 |
| 备份策略 | 对xxl_job_*系列表做定期备份,锁表数据尤其重要 |
6.2 调度慢和连接数激增的排查思路
如果上线后发现调度频率不高但数据库连接数很高,多半是连接池的空闲连接没释放,或者慢查询堆在数据库端。可以先看金仓这边的sys_stat_activity视图,确认活跃查询和锁等待情况。XXL-JOB 默认的调度频率通常不高,不至于打爆连接池。
另外,XXL-JOB 的调度日志表和日志清理逻辑要特别关注。日志清理任务用的是DELETE ... WHERE id < ? LIMIT ?这种分批删除方式,如果数据库端这个 SQL 走不了索引,删除会异常缓慢,进而占用大量连接。建议给xxl_job_log的trigger_time字段建索引,这能同时提升日志查询和清理的效率。
6.3 关于备份,别漏了锁表
数据库备份别只备份业务数据。xxl_job_lock表虽然只有一行,但少了它调度直接停摆,所以备份的时候要把所有xxl_job_*表一并纳入。恢复演练时也要验证这行锁记录被正确恢复,否则恢复出来的系统只能看不能调度。
我个人的习惯是给 KingbaseES 配一个定时逻辑备份任务,把整个xxl_job库每晚导出一份 SQL 文件,保留最近 7 天。调度系统核心数据量不大,这种轻量策略完全够用。
说到底,XXL-JOB 适配 KingbaseES 这件事,难度不在代码层面,而在于你对两个数据库方言差异的敏感度。把 SQL 脚本、驱动、连接池、mapper 这些细节逐一核对清楚,就能稳稳跑起来。最后再分享一个小技巧:改造过程中每改完一个文件就立刻做一次编译和启动验证,别攒着一口气改完再启动,否则报错时你很难定位是脚本问题、配置问题还是 SQL 方言问题。我这次就是分阶段验证,虽然过程多启动了几次服务,但整体推进反而更顺。