
简介xxl-job适配PostgreSQL数据库的定制版源码包基于2.4.1版本修改官方源码面向需要在PostgreSQL环境下部署任务调度平台的后端开发者与运维人员。资源在保留MySQL支持的同时新增PostgreSQL数据库适配通过修改配置即可切换数据库类型并附带了两种数据库的建库脚本可直接完成表结构初始化。压缩包共257个文件核心为133个Java源文件与30个XML配置覆盖任务调度、Mapper映射及Spring集成逻辑另含35个JS、12个CSS及11个FTL等前端资源用于控制台界面附带SQL脚本、Dockerfile和properties文件兼顾容器化部署与本地运行需求整体仅1.8MB结构紧凑。关键改动集中在数据库方言适配与自动配置部分与原版代码结构保持一致便于对照官方代码理解差异并进行二次开发。已有1390人学习下载适合需要快速获得PostgreSQL版本支持或希望基于2.4.1做定制扩展的开发团队直接采用。 接手过 xxl-job 的都知道官方默认支持的关系型数据库就是 MySQL想要跑在 PostgreSQL 上不只是改个连接串那么简单。我这次用的是 xxl-job 2.4.1折腾了大概两天把调度中心 admin 的源码扒了一遍改了建表脚本、MyBatis 的 Mapper XML、驱动依赖和少量 Java 代码总算是让整套调度平台在 PostgreSQL 14 上稳下来了。这篇文章就把我的改造过程、改动的文件、关键 SQL 的差异处理方式以及启动和自测时踩过的坑全部整理出来给同样想把 xxl-job 迁移到 PG 的同学一个可以直接照抄的参考。1. 为什么非改源码不可官方版本对 PostgreSQL 基本是放弃的状态1.1 你以为改个配置就行实际启动就挂我一开始也抱着侥幸心理想着 xxl-job 的 DAO 层用的是 MyBatisSQL 写在 XML 里理论上只要把application.properties里的 datasource 切换成 PostgreSQL再把 JDBC 驱动加上应该就能跑。结果启动直接报错错误信息集中在表不存在和 SQL 语法错误上。因为 xxl-job 的调度中心在启动时会自动执行初始化 SQL从 classpath 下面读tables_xxl_job.sql来建表官方提供的脚本是纯 MySQL 方言包含ENGINEInnoDB、COMMENTxxx、AUTO_INCREMENT、反引号这些语法PostgreSQL 根本解析不了。也就是说不改源码的情况下你连表都建不出来后续就算手动建了表MyBatis XML 里那一堆 MySQL 专属函数也会继续报错。1.2 需要动刀的范围其实比想象中大很多人以为只改建表脚本就完事了其实不够。我自己梳理了一遍至少要覆盖这几块根目录doc/db/tables_xxl_job.sql官方建表脚本要全部改成 PG 语法XxlJobAdminConfig 中初始化数据库表的 Java 代码要能正确解析并执行 PG 版 SQLmybatis-mapper 目录下所有 XML 文件里涉及 MySQL 语法、函数、分页的 SQLpom.xml 中添加 PostgreSQL JDBC 驱动数据库连接池参数、driverClassName 等配置文件的调整。考虑到 2.4.1 版本里不少 Mapper 的 SQL 写得比较奔放直接改 XML 是最合理的路径不用大改 Java 代码逻辑。整体评估下来改造的工作量集中在 SQL 方言转换而不是业务逻辑这也是 xxl-job 设计得还算舒服的地方——调度模型和 DAO 解耦得比较干净。2. 改代码前先把三条数据库访问路径摸清楚2.1 启动时的自动建表逻辑在哪xxl-job-admin 的启动入口是XxlJobAdminConfig它实现了InitializingBean在afterPropertiesSet()里会调用初始化逻辑最终找到 classpath 下的tables_xxl_job.sql按分号切分后逐条执行。源码大致逻辑是读取 SQL 文件内容通过connection.createStatement()批量执行。这里有个关键点官方脚本里的分号切分是无脑 split所以 PG 版建表脚本里不能出现函数体、存储过程这类包含分号的结构最好全部用简单的CREATE TABLE IF NOT EXISTS注释也尽量不要加避免解析出错。2.2 MyBatis 方言与 XML 映射文件的差异xxl-job 的 DAO 层全部用 MyBatis XML 写 SQL涉及调度的核心表有XXL_JOB_QRTZ_TRIGGER_INFO、XXL_JOB_QRTZ_TRIGGER_LOG、XXL_JOB_QRTZ_TRIGGER_GROUP、XXL_JOB_QRTZ_REGISTRY、XXL_JOB_QRTZ_LOG_REPORT等。这些 XML 里大量使用了 MySQL 的分页写法LIMIT #{offset}, #{pagesize}、时间函数DATE_ADD()/DATE_SUB()、条件判断IFNULL()、字符串拼接CONCAT()这些在 PostgreSQL 里都不完全兼容必须要一条一条处理。2.3 实际要改的 Mapper 文件清单以下是我在 2.4.1 里实际改过的文件按重要程度排序XxlJobLogMapper.xml改动最大的一个分页、时间过滤、清理日志都有 MySQL 语法XxlJobInfoMapper.xml分页查询、调度状态更新、child_job_id字符串处理XxlJobRegistryMapper.xml注册表清理、过期判断XxlJobGroupMapper.xml分页查询XxlJobLogReportMapper.xml报表查询里的日期聚合逻辑XxlJobLogGlueMapper.xml少量分页和查询语句。Java DAO 接口本身不用动MyBatis 会自动绑定 XML改完 XML 重启即可生效。3. 逐文件适配 PostgreSQL 的完整改动记录3.1 官方建表脚本的转换处理官方tables_xxl_job.sql里每张表的结构基本都是同一个套路MySQL 风格非常明显。以任务信息表为例原始 DDL 长这样CREATE TABLE XXL_JOB_QRTZ_TRIGGER_INFO ( id int(11) NOT NULL AUTO_INCREMENT, job_group int(11) NOT NULL COMMENT 执行器主键ID, job_desc varchar(255) NOT NULL, add_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我改成 PostgreSQL 版本的时候主要处理了四类差异AUTO_INCREMENT 换成GENERATED BY DEFAULT AS IDENTITY或SERIAL我选的是GENERATED BY DEFAULT AS IDENTITY这种标准写法反引号全部去掉COMMENT子句去掉改成在表创建后用COMMENT ON语句单独加或者干脆不加MySQL 的datetime换成timestamp默认值写成CURRENT_TIMESTAMP。改造后的示例CREATE TABLE IF NOT EXISTS xxl_job_qrtz_trigger_info ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, job_group bigint NOT NULL, job_desc varchar(255) NOT NULL, add_time timestamp DEFAULT CURRENT_TIMESTAMP, update_time timestamp DEFAULT CURRENT_TIMESTAMP );注意PG 表名、字段名如果没加双引号会全部转成小写MyBatis 的 SQL 对大小写不敏感所以我在 XML 里也统一用了大写。这里更稳妥的做法是建表时就建小写表名XML 里也不加引号让 PG 的小写折叠生效省得之后写 SQL 时分不清大小写。3.2 分页 SQL 从LIMIT ?, ?到 OFFSET 的转换xxl-job 里凡是列表页SQL 几乎都长这样select idpageList resultTypecom.xxl.job.admin.core.model.XxlJobInfo SELECT * FROM XXL_JOB_QRTZ_TRIGGER_INFO WHERE 11 if testjobGroup ! null and jobGroup ! AND job_group #{jobGroup} /if ORDER BY id DESC LIMIT #{offset}, #{pagesize} /selectPostgreSQL 不支持LIMIT offset, size必须拆成LIMIT size OFFSET offset。我把所有LIMIT #{offset}, #{pagesize}统一替换成LIMIT #{pagesize} OFFSET #{offset}之所以用#{offset}而不是写成OFFSET #{offset}后仍然报错是因为部分 XML 里offset被当作保留字处理了需要加上 PG 的语义理解——在 PG 里 OFFSET 后面跟参数完全没问题不用担心。这种替换要非常仔细我把所有 Mapper XML 都搜了一遍确认没有遗漏。统计下来至少改了八个查询方法主要集中在 XxlJobInfoMapper 和 XxlJobLogMapper 里。3.3 日期函数与字符串函数的兼容性处理清单这部分是坑最多的。MySQL 和 PG 在时间计算、字符串拼接上的语法差异让不少 SQL 改起来极其费眼神。xxl-job 里执行日志清理的 SQL 大概是这样的delete idclearLog DELETE FROM XXL_JOB_QRTZ_TRIGGER_LOG WHERE trigger_time lt; DATE_ADD(NOW(), INTERVAL -#{clearBeforeNum} DAY) /delete日期运算在 PG 里要写成CURRENT_TIMESTAMP - INTERVAL 1 day * #{clearBeforeNum}或者用NOW() - make_interval(days : #{clearBeforeNum})。我采用的是第二种方式因为参数直接来自外部用make_interval更直观。改造后的写法DELETE FROM XXL_JOB_QRTZ_TRIGGER_LOG WHERE trigger_time lt; CURRENT_TIMESTAMP - make_interval(days : #{clearBeforeNum})还有一处是统计报表的查询MySQL 里用DATE_FORMAT(trigger_time, %Y-%m-%d)PG 里对应的是TO_CHAR(trigger_time, YYYY-MM-DD)。这个也好处理直接替换函数名和格式符即可。IFNULL在 PG 里是COALESCExxl-job 里好几个查询用到了比如获取下一个触发时间、处理调度日志的 handle_code我都改成了COALESCE语义一致。字符串拼接方面MySQL 的CONCAT(?, ?)在 PG 里同样支持CONCAT函数但要注意 PG 的||运算符在遇到 NULL 时会返回 NULL导致结果不对。xxl-job Mapper 里有个别地方直接用了#{jobGroup} || %这种如果参数为空会出问题我统一改成CONCAT(#{jobGroup}, %)稳妥很多。3.4 注册表清理和日志表清理的额外处理XxlJobRegistryMapper 里有一条清理过期注册信息的 SQL原始逻辑类似DELETE FROM XXL_JOB_QRTZ_REGISTRY WHERE update_time DATE_SUB(NOW(), INTERVAL 90 SECOND)改成 PGDELETE FROM XXL_JOB_QRTZ_REGISTRY WHERE update_time CURRENT_TIMESTAMP - INTERVAL 90 seconds注意这里 PG 的 interval 字符串必须带单引号且数字和单位之间要有空格写90s是不行的必须90 seconds或者90 second。XxlJobLogMapper 里还有一个根据执行器地址和任务Id查询日志的 SQL用到了ORDER BY trigger_time DESC LIMIT 1PG 也支持这种写法唯一要注意的是 LIMIT 后面不能像 MySQL 那样写0, 1这种。另外PG 对ORDER BY字段的隐式类型转换要求更高trigger_time如果建表时是timestamp没有问题如果是字符串存的就要先CAST(trigger_time AS timestamp)否则排序结果可能不符合预期。3.5 配置文件和 pom.xml 的调整这一步相对简单但是漏了就不行。pom.xml 中 xxl-job-admin 模块需要新增 PostgreSQL 驱动依赖我加的是官方驱动dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version42.7.1/version /dependency版本号自己选一个稳定版本即可建议 42.x 以上兼容 PG 14、15 和 16。application.properties里的改动更直白spring.datasource.urljdbc:postgresql://localhost:5432/xxl_job?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai spring.datasource.usernamepostgres spring.datasource.passwordyourpassword spring.datasource.driver-class-nameorg.postgresql.Driver需要注意 PostgreSQL 的连接参数currentSchema如果有需要也可以加上但默认用 public schema 就够了。连接池那块用的是 HikariCP不用额外改东西。4. 首次启动的初始化流程与异常排查4.1 建表脚本在什么时候执行xxl-job-admin 启动时会自动执行初始化脚本整个过程就是拿 classpath 下tables_xxl_job.sql的内容按;分割逐条执行。所以我在替换成 PG 版建表脚本后把脚本里所有的注释和空行都清理了一遍保证每条 SQL 是独立的、完整的避免出现解析异常。如果希望每次启动都不重复建表PG 的CREATE TABLE IF NOT EXISTS也能兜底执行第二次不会报错。4.2 我遇到的启动报错和解决对照我把这次适配过程中遇到的几个报错和对应的解决办法列成了一张表方便排查时直接对号入座。报错现象原因解决办法relation xxl_job_qrtz_trigger_info does not exist建表脚本没执行成功或表名大小写不一致确认初始化脚本已加载 PG 版本手动执行建表 SQL 排查错误syntax error at or near ENGINE建表脚本仍是 MySQL 方言重新替换官方建表脚本为 PG 语法LIMIT # {offset}, # {pagesize}附近的语法错误MySQL 分页写法不兼容改成LIMIT #{pagesize} OFFSET #{offset}function concat(timestamp with time zone, unknown) does not exist使用了 PG 不支持的隐式类型拼接显式转换比如CONCAT(CAST(trigger_time AS varchar), )operator does not exist: timestamp character varying表字段类型和查询参数类型不匹配统一参数类型必要时候在 XML 中做CASTinvalid input syntax for type bigint: 1,2child_job_id 这类字段是用逗号拼接的把字符串按逗号拆开后转数组或子查询处理4.3 老项目升级时的数据迁移心得如果本来就有 MySQL 环境下跑着的 xxl-job 调度数据在切换到 PG 之前建议先停调度、导出、再导入。任务信息表、日志表、执行器表这几张核心表直接导出 CSV 再导入 PG 即可但要注意自增 ID 的序列问题。PG 字段如果用了GENERATED BY DEFAULT AS IDENTITY或SERIAL导入之前要把序列值调整到当前最大 ID 之后否则后续新增任务会主键冲突。用setval直接调SELECT setval(pg_get_serial_sequence(xxl_job_qrtz_trigger_info, id), (SELECT MAX(id) FROM xxl_job_qrtz_trigger_info));这个操作一定不能省我一开始迁移完忘了调新增第一个任务就报主键重复的错排查了半天才反应过来。5. 适配完成后的调度链路自测清单5.1 必须覆盖的调度场景代码改完、服务能起来只算完成了一半。调度系统这种基础设施核心链路不验证一遍就上线等于埋雷。我自己整理了一个自测清单照着跑一遍基本能保证没有大问题新建执行器、编辑执行器、删除执行器新增 GLUE IDE 任务保存并在线编辑新增 Bean 模式任务手动执行一次配置 cron 表达式等待自动触发观察调度日志任务执行失败后查看失败回调、重试次数是否正常执行器断线后调度中心是否把注册实例标记为过期任务日志清理按天删除是否能正常执行报表页的每日调度统计曲线是否有数据。5.2 我实测下来最容易出问题的两个点第一个是失败日志的handle_code和handle_msg在 PG 里的类型处理。调度中心回调时写入的是小整数和长文本MySQL 下 lenient 一点不会报错PG 对类型要求比较严格如果表字段类型和写入类型不匹配会直接抛异常导致调度日志一直显示执行中。定位后我把handle_code字段改成integerhandle_msg改成text再也没有出过类型转换的问题。第二个是调度日志报表的日期分组查询。MySQL 的DATE_FORMAT可以根据%Y-%m-%d直接做字符串分组PG 的TO_CHAR也能做但如果字段本身是带时区的timestamptz分组的时区基准会受数据库时区影响。我的解决办法是在连接串里强制指定serverTimezoneAsia/Shanghai同时把日志表里触发时间字段建为timestamp without time zone避免时区错乱导致报表统计偏差。5.3 关于连接池和性能再说几句xxl-job 默认用的是 HikariCP连接池大小可以在application.properties里配置。调度中心的并发压力通常不会高到需要很大的连接池maximum-pool-size设成 20 足够。但 PG 数据库服务器的max_connections要提前确认如果 PG 实例还有其他业务共用连接数爆了反而是最尴尬的。另外PG 的自动清理机制和 MySQL 不太一样xxl-job 的调度日志表会越来越大建议定期清理。官方自带的控制台点清理日志就能触发DELETE但大表直接 DELETE 会比较慢有条件的话可以在 PG 里做分区表按月份分区或者定期用 TRUNCATE 归档后删除能省掉不少 I/O。最后分享一个小经验这套改造方案我已经在测试环境稳定跑了两周任务调度、日志回调、失败重试都正常。如果你也在做同样的事我的建议是不要试图用 ORM 的自动建表功能绕过 SQL 方言问题MyBatis 是不会帮你翻译 SQL 的老老实实把 XML 逐条过一遍最省心。另外就是建表脚本和 Mapper XML 改完之后先用一个最小任务跑通全链路再进行数据迁移这样即使有问题定位范围也会小很多。本文还有配套的精品资源点击获取