xxl-job适配PostgreSQL实战:从源码改造到SQL方言兼容
2026/9/8 5:22:33 网站建设 项目流程

简介: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}、函数IFNULLDATE_FORMAT,加上建表时用到的AUTO_INCREMENTENGINE=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 写法备注
TINYINTSMALLINT状态位字段常用
INTINTEGER直接替换
BIGINTBIGINT无需调整
DATETIMETIMESTAMP默认值要特别注意
TEXT/MEDIUMTEXTTEXT日志内容类字段
BLOB/LONGBLOBBYTEA存储序列化对象时用
AUTO_INCREMENTGENERATED BY DEFAULT AS IDENTITYPG 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 );

字段清单以官方脚本为准,我这里想强调的是几个转换习惯:idBIGSERIAL或者GENERATED BY DEFAULT AS IDENTITY都可以,我习惯用前者,简单直接;trigger_status这种状态字段用SMALLINTTINYINT对应更准确;所有字符串长度保持和官方一致,尤其是job_descexecutor_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_TIMESTAMPPG 也支持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 里,但改造的时候一定要全局搜索IFNULLDATE_FORMATGROUP_CONCAT这些关键词,逐个判断。特别是日志查询和报表统计模块,通常会用到日期格式化,漏一个就是运行时报错。另外,PG 的字符串拼接用的是||,如果你在 mapper 里看到 MySQL 的CONCAT,也要顺手替换成||——虽然 PG 也支持CONCAT,但统一风格更利于后续维护。最稳妥的办法是在适配完成后,把调度中心的所有页面点一遍,让每个查询都真实跑一遍。

3.4 并发写与唯一冲突:注册表相关 SQL 的坑

xxl-job 的注册中心逻辑会频繁向xxl_job_registry表写入注册信息。注册信息通常带有唯一性约束,比如registry_keyregistry_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_infoxxl_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_FORMATIFNULL等函数未替换全局搜索日期和空值函数,替换成 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_TRIGGERSQRTZ_CRON_TRIGGERS这些,那多半是 Quartz 的问题,优先检查 QRTZ 表结构是否用了官方 PG 脚本;如果报错信息里有xxl_job_logxxl_job_info这些表,那就是 mapper SQL 的问题,优先查 Mapper XML。这两条路分开排查,效率会高很多,不会在无关代码里打转。

最后再说一个维护层面的体会。改源码适配数据库这件事,最忌讳直接在官方主干上裸改。我这次把所有改动集中到一个 fork 分支里,每次官方的版本更新后,把旧分支的改动打成补丁再应用过去,几分钟就能知道哪些文件和官方冲突了、适配逻辑有没有被波及。对被迫离开 MySQL 生态的团队来说,xxl-job 适配 PostgreSQL 真不是大手术,难点全在那些藏在 Mapper XML 里的 MySQL 方言。希望这篇记录能帮你把排查路径缩短一大截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询