简介:xxl-job 2.4.1已针对PostgreSQL完成源码级适配,解决官方版本默认依赖MySQL、无法直接使用PostgreSQL的问题。面向使用PostgreSQL作为业务数据库的Java开发与运维人员,在不改变原有功能的前提下,实现调度中心对MySQL和PostgreSQL两种数据库的灵活切换,通过配置文件即可选定目标库。压缩包共257个文件、1.8MB,以Java源码、JS脚本、XML配置、CSS样式、FreeMarker模板、properties配置为主,涵盖前后端核心代码、页面视图、样式文件及运行配置;同时包含两套建库SQL脚本,分别对应MySQL与PostgreSQL,便于初始化。已有1390人学习下载,适合正在迁移xxl-job或希望摆脱单一数据库依赖的团队参考。借助修改后的源码与说明,读者可快速掌握两种数据库兼容的关键改动点,并结合dockerfile、properties等配置快速搭建调度中心,省去从零适配的重复工作。 xxl-job 适配 postgresql 这件事,最初是公司数据库选型定下来的硬需求:所有中间件都必须跑在 PostgreSQL 上,而官方 xxl-job 2.4.1 默认只维护了 MySQL 和 Oracle 的适配。项目本身不算复杂,但你把官方源代码拉下来跑一遍就会发现,建表脚本、驱动依赖、MyBatis 的 SQL 映射文件全是以 MySQL 方言为基准写的。为了让调度中心统一落到 PG 上,我以 xxl-job 2.4.1 的官方源码为基础做了一版适配,所有改动都是直接改源码完成的。这篇文章把改造点、踩坑、验证过程完整写出来,适合正在做 MySQL 切 PostgreSQL、或者想把 xxl-job 落地到 PG 环境的后端开发参考。
1. 为什么非要改源码,而不是只换驱动
1.1 官方对数据库的支持其实很“偏科”
xxl-job-admin 的数据库访问层分为两块:一块是 Quartz 调度器自己的 QRTZ_* 表,另一块是 xxl-job 自己管理的业务表,比如 xxl_job_info、xxl_job_log、xxl_job_registry、xxl_job_lock 这些。官方在 db 目录下提供了 tables_xxl_job.sql(MySQL 版)和 tables_xxl_job_oracle.sql(Oracle 版),唯独没有 PostgreSQL 脚本。光这一点,就不是单纯换一个数据库驱动能解决的。
更麻烦的是 MyBatis 的 Mapper XML。2.4.1 的 SQL 里大量使用 MySQL 特有的分页写法LIMIT #{offset}, #{pageSize}、函数IFNULL、DATE_FORMAT,加上建表时用到的AUTO_INCREMENT、ENGINE=InnoDB这些语法,一旦数据源切到 PostgreSQL,SQL 解析阶段就会直接报错。很多人第一反应是把 pom 里的 mysql 驱动换成 postgresql 驱动,然后改一改连接串,结果启动后到处报语法错误,就是因为忽略了这一层。
1.2 三条路线的对比,以及我为什么选了改源码
当时我有三条路可以走。第一条:只改数据源配置,这是最省事的,但跑不起来,因为 Mapper XML 和初始化脚本都是 MySQL 方言,数据库网关类的中间件不会帮你自动翻译。第二条:引入 SQL 改写中间件或者数据库网关,统一做语法翻译。这个方案理论上可行,但调度中心对事务、行锁、Quartz JobStore 都有比较特殊的要求,网关层一旦处理不好,排查成本比改源码还高,所以我果断放弃。第三条就是直接改官方代码,把建表脚本、驱动依赖、Mapper XML 全部改成 PG 兼容的写法。
选第三条不是因为“不会别的方案”,而是这个改动范围其实很小。xxl-job-admin 的数据库访问层结构非常清晰,核心 SQL 集中在 mybatis-mapper 目录下,绝大多数业务逻辑不依赖具体数据库。也就是说,只要把表结构建对、SQL 写法兼容 PG,整个调度平台就能正常工作。改动集中在几个文件里,后续升级维护也方便。
1.3 整体改动地图
开始动手之前,我先把所有要动的点列了个清单,避免改着改着漏掉哪块:
xxl-job-admin/pom.xml:把 mysql 驱动依赖替换为 postgresql 驱动。application.properties:修改 driver-class-name、jdbc url、用户名密码。db/tables_xxl_job.sql:新增 PostgreSQL 版本的建表脚本。mybatis-mapper/*.xml:处理分页、函数、并发写相关的 SQL 兼容问题。
后面所有实操都是围绕这份清单展开的。建议你也先做这个动作,因为改源码最怕的不是技术难度,而是改到一半才发现某个 SQL 没适配,又得回头翻。
2. 建表脚本的 PostgreSQL 化
2.1 QRTZ 系列表:直接用 Quartz 官方脚本
xxl-job-admin 里带了 Quartz 调度器,因此需要 QRTZ_JOB_DETAILS、QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS 这些表。这里我有一个强烈建议:不要从 MySQL 脚本手工转换 QRTZ 系列表,直接去 Quartz 官方源码或发行包里拿tables_postgres.sql。原因很简单,Quartz 官方为每种数据库都维护了对应版本的表结构脚本,PG 版本的字段类型、默认值、索引定义都已经验证过了。自己转换的话,很容易在 BLOB 转 BYTEA、字段默认值、索引命名这些细节上出错,而且 Quartz 的 JobStore 对不同表结构的兼容性很挑,表结构差一点,触发线程就会罢工。
拿到官方 PG 脚本后,把 QRTZ 表部分和后面的 xxl-job 业务表合并到一个初始化脚本里执行。QRTZ 表的脚本通常要跑在同一个 schema 下,表名前缀保持一致,建议沿用默认的 QRTZ_ 前缀,没必要自定义。
2.2 业务表字段类型转换对照
业务表部分还是要手工处理的。官方 MySQL 脚本里的常见类型和 PG 的对应关系,我整理成了一张表:
| MySQL 写法 | PostgreSQL 写法 | 备注 |
|---|---|---|
TINYINT | SMALLINT | 状态位字段常用 |
INT | INTEGER | 直接替换 |
BIGINT | BIGINT | 无需调整 |
DATETIME | TIMESTAMP | 默认值要特别注意 |
TEXT/MEDIUMTEXT | TEXT | 日志内容类字段 |
BLOB/LONGBLOB | BYTEA | 存储序列化对象时用 |
AUTO_INCREMENT | GENERATED BY DEFAULT AS IDENTITY | PG 10+ 推荐写法 |
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | 删除 | PG 不适用 |
COMMENT 'xxx' | COMMENT ON COLUMN table.col IS 'xxx' | 可保留也可省略 |
类型转换只是第一步,真正的坑在默认值。比如 MySQL 里DATETIME DEFAULT CURRENT_TIMESTAMP,PG 里可以写成TIMESTAMP DEFAULT CURRENT_TIMESTAMP,但如果有ON UPDATE CURRENT_TIMESTAMP这种 MySQL 语法,PG 不自持,需要在应用层维护更新时间,或者建触发器,但 xxl-job 大部分更新时间字段是 Java 代码里 set 的,所以直接去掉ON UPDATE部分就行。
2.3 一个示范:xxl_job_info 的 PG 建表
我看网上有些转换教程喜欢贴一整个几百行的完整脚本,实际阅读效果很差。这里我挑 xxl_job_info 表做示范,这个表是任务管理的核心,字段类型覆盖了大半转换规则:
CREATE TABLE xxl_job_info ( id BIGSERIAL PRIMARY KEY, job_group INTEGER 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, 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 INTEGER NOT NULL DEFAULT 0, executor_fail_retry_count INTEGER 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 );字段清单以官方脚本为准,我这里想强调的是几个转换习惯:id用BIGSERIAL或者GENERATED BY DEFAULT AS IDENTITY都可以,我习惯用前者,简单直接;trigger_status这种状态字段用SMALLINT比TINYINT对应更准确;所有字符串长度保持和官方一致,尤其是job_desc、executor_param这种可能存较长内容的字段。
2.4 不要漏掉索引
业务表转换时,索引也很容易漏。官方 MySQL 脚本里常常有这种写法:
KEY idx_I_trigger_log (job_group, trigger_status)这在 PG 里是不认识的,需要转换成标准的建索引语句:
CREATE INDEX idx_I_trigger_log ON xxl_job_log (job_group, trigger_status);如果一张表有多个索引,建议放在一张表的 CREATE TABLE 之后集中执行。注意 PG 的索引名在同一 schema 下不能重复,转换时最好检查一下是否和已有索引重名。
3. 代码层修改点:从依赖到 SQL 映射
3.1 数据库驱动与数据源配置
先改依赖。在xxl-job-admin/pom.xml里找到 mysql 驱动依赖,替换成 PostgreSQL 驱动。版本我建议和数据库 server 版本匹配,PG 12 到 PG 15 用 42.x 系列驱动都没有问题:
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency>然后是application.properties,重点看这几个配置:
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/xxl_job?TimeZone=Asia/Shanghai spring.datasource.username=xxl_job spring.datasource.password=xxl_job spring.datasource.driver-class-name=org.postgresql.Driver细心的朋友会发现我连接串里加了TimeZone=Asia/Shanghai。这个参数非常重要,后面会专门讲。如果和官方默认配置对比,除了 driver 变了,连接串从 MySQL 的characterEncoding=utf8换成了 PG 的TimeZone参数,其他结构保持一致。
3.2 分页 SQL:最容易报语法错误的地方
xxl-job-admin 的分页没有引入 PageHelper,日志查询、任务查询都是手写 SQL 分页。官方 mapper 里大量存在这种写法:
-- 修改前(MySQL 方言) SELECT * FROM xxl_job_log WHERE job_group = #{jobGroup} LIMIT #{offset}, #{pageSize} -- 修改后(PostgreSQL 方言) SELECT * FROM xxl_job_log WHERE job_group = #{jobGroup} LIMIT #{pageSize} OFFSET #{offset}MySQL 的LIMIT offset, size语义是先跳过 offset 行再取 size 行,PostgreSQL 官方支持的语法是LIMIT size OFFSET offset。这行不改,调度日志页面必然报syntax error at or near ","。我实际踩坑时就是先改了驱动和配置,启动没问题,一打开日志页面就报错,最后定位到这里。建议全局搜索LIMIT #{offset},和LIMIT ?,,把所有出现位置一次性替换干净,不要漏。
注意:
LIMIT #{pageSize} OFFSET #{offset}中的 offset 参数一定要传数值,不能传 null,否则 PG 会直接报OFFSET must not be null。
3.3 函数兼容:IFNULL、DATE_FORMAT 这类写法
除了分页,Mapper XML 里还有几个高频函数要在 PG 里换掉。我遇到的每个都列在这里:
| MySQL 写法 | PostgreSQL 写法 | 说明 |
|---|---|---|
IFNULL(a, b) | COALESCE(a, b) | 都返回第一个非空值 |
NOW() | CURRENT_TIMESTAMP | PG 也支持NOW(),但统一用CURRENT_TIMESTAMP更规范 |
DATE_FORMAT(t, '%Y-%m-%d %H:%i:%s') | TO_CHAR(t, 'YYYY-MM-DD HH24:MI:SS') | 格式化日期 |
GROUP_CONCAT(x) | STRING_AGG(x, ',') | 聚合拼接 |
这些函数不一定会全部出现在某一段 SQL 里,但改造的时候一定要全局搜索IFNULL、DATE_FORMAT、GROUP_CONCAT这些关键词,逐个判断。特别是日志查询和报表统计模块,通常会用到日期格式化,漏一个就是运行时报错。另外,PG 的字符串拼接用的是||,如果你在 mapper 里看到 MySQL 的CONCAT,也要顺手替换成||——虽然 PG 也支持CONCAT,但统一风格更利于后续维护。最稳妥的办法是在适配完成后,把调度中心的所有页面点一遍,让每个查询都真实跑一遍。
3.4 并发写与唯一冲突:注册表相关 SQL 的坑
xxl-job 的注册中心逻辑会频繁向xxl_job_registry表写入注册信息。注册信息通常带有唯一性约束,比如registry_key和registry_value的组合。如果官方 mapper 里存在 MySQL 风格的INSERT IGNORE或者REPLACE INTO,在 PG 里需要改成:
INSERT INTO xxl_job_registry (registry_group, registry_key, registry_value, update_time) VALUES (#{registryGroup}, #{registryKey}, #{registryValue}, CURRENT_TIMESTAMP) ON CONFLICT (registry_key, registry_value) DO UPDATE SET update_time = EXCLUDED.update_time;这里的关键点是ON CONFLICT必须依赖一个已存在的唯一索引或唯一约束,否则 PG 不知道冲突的是哪一行。建表脚本里要给对应的列加上唯一索引,这个顺序不能反。好的一面是,xxl-job 的调度锁逻辑使用的是xxl_job_lock表上的SELECT ... FOR UPDATE,这个语法在 PostgreSQL 原生支持,事务结束自动释放锁,不需要额外处理。多实例部署时,调度中心之间抢锁的行为和 MySQL 下表现一致,这一点实测下来很稳。
4. 实操流程:从源码到跑通的完整步骤
4.1 环境准备与建库建用户
我这次用的环境是 JDK 8、Maven 3.6、PostgreSQL 12。PG 10 以上基本都行,建议不要太老,因为新版特性如 identity 列在 PG 10 才正式完善。先创建数据库和账号:
CREATE USER xxl_job WITH PASSWORD 'xxl_job'; CREATE DATABASE xxl_job OWNER xxl_job ENCODING 'UTF8';然后导入整合好的 PG 版初始化脚本:
psql -h 127.0.0.1 -U xxl_job -d xxl_job -f tables_xxl_job_postgresql.sql执行完最好用\dt看一眼所有表是否创建成功,尤其是 QRTZ 系列和xxl_job_info、xxl_job_lock有没有缺。
4.2 修改配置与打包
拉取 xxl-job 2.4.1 源码,确认分支是 2.4.1 的 tag,然后依次执行前面说的三处修改:pom.xml、application.properties、Mapper XML。所有文件改完后,在项目根目录执行:
mvn clean package -DskipTests构建成功后,admin 端的 jar 包会在xxl-job-admin/target下生成。用java -jar启动,浏览器访问http://localhost:8080/xxl-job-admin,默认账号 admin、默认密码 123456。如果你和我一样用的是 server 版而不是嵌入式 Tomcat,把打出来的 war 丢到 servlet 容器里即可,但 2.4.1 直接跑 Spring Boot jar 是最省事的。
4.3 验证清单:不要只看登录成功就算完
登录后台只是第一步,真正的验证要看这几点:
- 任务管理:新增一个简单任务,配置 cron 表达式,手动执行一次,查看调度日志和返回结果。
- 日志清理:把日志清理相关页面跑一遍,确认不带分页问题的 SQL 没有触发报错。
- 调度报表:查看图表统计,如果 DATE_FORMAT 没改干净,统计页会直接 500。
- 执行器注册:配置一个执行器,确认调度中心能实时看到执行器的在线状态。
- 多实例锁:有条件的话部署两个 admin 实例,观察同一任务不会重复触发,验证
xxl_job_lock行锁在 PG 下有效。
我实际操作时,大部分时间花在了验证这一步。因为表结构、驱动、分页这些问题是一层一层浮现的,手动点页面比单纯看日志更能暴露遗漏点。建议你按上面的清单逐项过,不要急着上生产。
4.4 运行时观察 SQL 的技巧
排查阶段,建议把 MyBatis 的 SQL 日志打开,我通常会在logback.xml里加一段:
<logger name="com.xxl.job.admin.dao" level="DEBUG"/>这样后台执行的每条 SQL 都会打印出来,改一个点重启一次,能肉眼看到 SQL 是否还带 MySQL 痕迹。我踩过的一个坑就是改完 mapper 忘了清理 class 目录里的旧文件,导致实际运行的还是 MySQL 版 SQL,浪费了不少时间。
5. 常见问题与排查技巧实录
5.1 高频报错排查表
整个适配过程前后遇到不少报错,我把最有代表性的整理成表,方便大家直接对照:
| 报错现象 | 可能原因 | 解决办法 |
|---|---|---|
启动报relation "xxl_job_lock" does not exist | 建表脚本没执行,或部分表建失败 | 检查初始化脚本,逐个表确认 |
调度日志页报syntax error at or near "," | 分页 SQL 还是 MySQL 写法 | 把LIMIT #{offset}, #{pageSize}改成LIMIT #{pageSize} OFFSET #{offset} |
| 任务执行时间比预期差 8 小时 | 数据库时区和应用时区不一致 | JDBC URL 增加TimeZone=Asia/Shanghai |
| 调度报表页面 500 或空白 | DATE_FORMAT、IFNULL等函数未替换 | 全局搜索日期和空值函数,替换成 PG 写法 |
连接报password authentication failed | 数据库账号认证方式问题 | 确认pg_hba.conf的认证方式,或用新账号重建 |
| QRTZ 相关任务不触发 | QRTZ 表结构转换不完整 | 直接用 Quartz 官方tables_postgres.sql重建 QRTZ 表 |
5.2 容易被忽略的时区细节
时区这个问题我单独拿出来说。PostgreSQL JDBC 在没有显式指定时区时,会按数据库或操作系统的时区来处理时间。如果服务器是 UTC,而你写的 cron 执行时间按北京时间配置,日志里看到的时间就会差 8 小时。而且这种问题不会直接报错,只是让你觉得任务触发时间不对劲,非常隐蔽。解决办法就是在连接串里明确指定:
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/xxl_job?TimeZone=Asia/Shanghai同时建议把 PG 数据库本身的 timezone 也调一下,双保险。
5.3 定位是 xxl-job 的 SQL 还是 Quartz 的 SQL
适配过程中还有一个排查思路值得分享。xxl-job-admin 的数据库操作分两块,一块是通过 MyBatis 执行的 mapper SQL,另一块是 Quartz 内部通过 JobStore 执行的 SQL。如果报错信息里出现QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS这些,那多半是 Quartz 的问题,优先检查 QRTZ 表结构是否用了官方 PG 脚本;如果报错信息里有xxl_job_log、xxl_job_info这些表,那就是 mapper SQL 的问题,优先查 Mapper XML。这两条路分开排查,效率会高很多,不会在无关代码里打转。
最后再说一个维护层面的体会。改源码适配数据库这件事,最忌讳直接在官方主干上裸改。我这次把所有改动集中到一个 fork 分支里,每次官方的版本更新后,把旧分支的改动打成补丁再应用过去,几分钟就能知道哪些文件和官方冲突了、适配逻辑有没有被波及。对被迫离开 MySQL 生态的团队来说,xxl-job 适配 PostgreSQL 真不是大手术,难点全在那些藏在 Mapper XML 里的 MySQL 方言。希望这篇记录能帮你把排查路径缩短一大截。
本文还有配套的精品资源,点击获取