简介:xxl-job 2.4.1 的 PostgreSQL 适配版,是一份经过源码改造的分布式任务调度平台资源,主要解决官方版本默认依赖 MySQL 的问题。面向需要在 PostgreSQL 数据库环境部署该调度平台,或者想要扩展数据库支持、进行二次开发的开发者与运维工程师。整个资源包为 RAR 压缩格式,共 257 个文件,体积仅 1.8MB。其中包含大量 Java 源码,用于调度核心逻辑;XML 配置和映射文件用于 MyBatis 等持久层;还有 JS、CSS、FTL 等前端页面文件,构成完整的任务调度台界面;Properties 与 Dockerfile 等则提供环境配置和容器化部署参考。随包附带 MySQL 和 PostgreSQL 两套建库脚本,并预留了数据库切换的配置文件,读者既可以快速在两种数据库之间迁移,也可以对照官方 2.4.1 源码,分析适配改动点,理解任务调度、执行器注册等表结构差异。目前已有 1401 人下载学习,适合有一定分布式任务调度基础、需要落地到 PostgreSQL 环境的读者使用。
1. xxl-job 2.4.1 适配 PostgreSQL:官方没做的事,值得你自己动手
某团队在做数据库整体迁移,目标是把调度中心从 MySQL 切到 PostgreSQL,结果发现 xxl-job 2.4.1 的官方源码虽然能把 JDBC 驱动挂上,但建表脚本、MyBatis 的 SQL 方言全按 MySQL 写的,换库后要么启动就报错,要么任务日志查不出来。问题的根源在于:xxl-job 2.4.1 官方对 PostgreSQL 只停留在「驱动能连」的层面,并没有做 SQL 方言适配。本文要做的,就是把 xxl-job 2.4.1 适配 PostgreSQL 的完整过程讲清楚:哪些源码文件要动、建表脚本怎么重写、mapper 里的 MySQL 写法怎么改、参数怎么设、上线前要验证哪些链路。
这篇笔记适合两类人:一类是自己维护调度中心、想把数据库统一成 PostgreSQL 的运维或后端开发者;另一类是想把官方源码改造成私有分支、但不想踩方言坑的二次开发者。本质上,这不是一个配置项能解决的问题,而是一次小规模的源码改造,我会把我在适配过程中遇到的坑和验证方法全部交代清楚,照着做就能把调度中心稳在 PostgreSQL 上。
2. 先别急着改代码:源码体检与方言差异清单
2.1 改源码前先做源码级体检:先确认 2.4.1 里哪些文件是 MySQL 方言
拿到 xxl-job 2.4.1 源码后,我习惯先做一次全量扫描,不急着改任何一行。需要关注的就两类位置:一是 SQL 初始化脚本,二是 MyBatis 的 mapper XML。官方源码的 admin 模块里,SQL 脚本目录下放的是 MySQL 版本的建表与种子数据;而 mapper XML 分散在 xxl-job-admin 的 dao 层里,每个 mapper 对应一张核心表。先跑一遍 grep 把明显的 MySQL 关键词全部列出来,再决定改动范围:
grep -rn "AUTO_INCREMENT\|ENGINE=InnoDB\|`.*`" --include="*.sql" . grep -rn "NOW()\|DATE_ADD\|DATE_SUB\|LIMIT #{.*}\, #{.*}" --include="*.xml" .第一条命令用于扫 SQL 脚本中的 MySQL 专属语法:反引号、AUTO_INCREMENT、ENGINE 定义。第二条命令用于扫 mapper XML 里的时间函数和分页语法。这一步的价值在于:它能帮你估算改动量。我实测下来的体感是,核心 mapper 里真正需要动的地方并不算多,大概集中在任务信息查询、日志清理和注册中心相关的几个 XML 中,但每一处都必须改对,否则线上跑一段时间才会暴露。
至于到处都是的NOW(),先不用急着全局替换——xxl-job 兼容多个数据库,有些 XML 里的写法本身是跨库安全的,真正的坑是那些 MySQL 独占语法。把它们一条条列出来,后续动手时心理就有底了。注意扫描结果里如果出现 qrtz 开头的表,那是历史遗留的 Quartz 相关的脚本,2.4.1 默认走的是自研调度线程,多数场景不需要碰它,先跳过别浪费时间。
2.2 PG 与 MySQL 在调度场景的 5 个关键 SQL 差异
把扫描结果和实际业务结合,我总结了 xxl-job 2.4.1 里最容易翻车的 5 个 SQL 差异。第一个是分页语法。MySQL 写LIMIT #{offset}, #{pagesize},PostgreSQL 只认LIMIT #{pagesize} OFFSET #{offset},这个不改直接报语法错误,属于启动期就能炸的问题。
第二个是模糊查询的大小写敏感性。MySQL 在默认排序规则下 LIKE 不区分大小写,PostgreSQL 的 LIKE 是严格区分大小写的,直接用同一个模糊搜索条件,用户搜「Order」和「order」结果完全不同,任务多了以后会让人误以为搜索功能坏了。
第三个是时间函数差异。MySQL 的DATE_ADD、DATE_SUB、NOW()在 PG 里都不存在或语义不同,PG 里对应的是CURRENT_TIMESTAMP加interval表达式。官方在日志清理这类周期性任务里会用到时间运算,这部分不改会直接运行时抛异常。
第四个是自增主键。MySQL 的int AUTO_INCREMENT在 PG 里要换成BIGSERIAL,或者显式建序列并设置默认值nextval()。这里有个隐藏问题我会在第 5 章专门讲:用序列还是用 BIGSERIAL,直接影响 MyBatis 能否回填自增 ID。
第五个是批量插入返回主键的行为差异。xxl-job 在新增任务后需要立刻拿到自增 ID 去组装后续逻辑,MySQL 用useGeneratedKeys很顺,PG 在序列方案下偶尔会取不到,这个坑我建议在使用 BIGSERIAL 的前提下同时保留SELECT lastval()的兜底思路。
2.3 适配方案选型:全量改 mapper 还是加方言层
体检完,接下来要决定改造路线。常见做法是直接在官方源码里改 mapper XML,这也是大多数团队的选择,但它有个代价:以后官方出 2.4.2、2.5,你的改动得手动再并一遍。另一个思路是给框架加一层方言适配,比如把 SQL 通过拦截器按数据库类型动态改写,但 xxl-job 的 mapper 里写的是完整 SQL 文本,拦截改写成本高、正则容易误伤,不建议在 2.4.1 上硬做。
我一般会选择直接改源码,同时把改动点统一打上标记,让后续合并上游可追溯。比如在每个改动处加一行注释-- [PG-ADAPT],后面升级时用 grep 就能快速找回自己改过的地方。这样既避免了方言层设计的复杂度过高,又不至于让维护彻底失控。方案的取舍原则很简单:改动集中在 XML 和 SQL 脚本里,代码逻辑层几乎没有 Java 要动,加方言层属于过度设计。
3. 把官方源码改成 PG 原生实现:依赖、建表脚本与 MyBatis 改造
3.1 替换驱动与数据源:pom.xml 和 application.properties 怎么改
第一步是替换驱动依赖。官方 admin 模块默认引入的是 MySQL 驱动,需要改成 PostgreSQL 的 JDBC 驱动。以 Maven 工程为例,在 xxl-job-admin 的 pom.xml 里找到数据库驱动依赖,整体替换。这里注意版本号不要随手写死,建议在父 pom 的 properties 里统一管理:
<!-- 官方默认依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- 适配后替换为 --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency>逻辑说明:这一步是让 admin 模块的 Spring Boot 数据源能驱动 PostgreSQL 连接。官方代码里对驱动的类名是通过配置加载的,所以 Java 源码不需要动,只要把依赖换成 PG 驱动,再改配置即可。
与此同时,application.properties 里的数据源配置要整体切换。PG 的 JDBC URL 格式与 MySQL 完全不同,driver-class-name 也要换:
# MySQL 原配置 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # PostgreSQL 变更 spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/xxl_job?currentSchema=public&stringtype=unspecified&TimeZone=Asia/Shanghai spring.datasource.username=postgres spring.datasource.password=postgres spring.datasource.driver-class-name=org.postgresql.Driver参数说明:stringtype=unspecified是我强烈建议加上的,它让 PG 驱动对字符串参数的类型推断更宽松。xxl-job 的 mapper 里大量使用动态 SQL,MyBatis 把参数以 varchar 类型传给 PG,PG 在判断字段类型时经常报「column is of type bigint but expression is of type character varying」这类错误,加上这个参数能绕开绝大多数此类问题。TimeZone显式指定时区,避免任务日志里的时间比本地时间差 8 小时。改完这两处,数据库连接层面就通了。
3.2 重写初始化脚本:把 AUTO_INCREMENT 换成 BIGSERIAL 序列
连接通了还不够,数据库里的表压根没建。官方的表结构脚本是 MySQL 方言,直接丢到 PG 里执行会报错。最头疼的不是字段类型——varchar、datetime 这些 PG 都能兼容,真正的硬骨头是自增主键和索引定义。以核心表xxl_job_info为例,官方写法改造前后对比:
-- MySQL 官方写法(不能直接在 PG 执行) CREATE TABLE `xxl_job_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `job_group` int(11) NOT NULL, `job_desc` varchar(255) NOT NULL, `glue_type` varchar(50) NOT NULL, `add_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_job_group` (`job_group`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- PostgreSQL 改写版本 CREATE TABLE xxl_job_info ( id BIGSERIAL NOT NULL, job_group int NOT NULL, job_desc varchar(255) NOT NULL, glue_type varchar(50) NOT NULL, add_time timestamp NULL, update_time timestamp NULL, PRIMARY KEY (id) ); CREATE INDEX idx_job_group ON xxl_job_info (job_group);逻辑说明:BIGSERIAL会自动创建序列并作为 id 默认值,这是 PG 里最接近 MySQL 自增主键的写法。timestamp NULL显式允许为空,避免老数据里时间字段为空时插入失败。索引定义从 MySQL 表内的KEY语法改成独立的CREATE INDEX。需要注意的是,xxl-job 的表不只一张,xxl_job_log、xxl_job_registry、xxl_job_group、xxl_job_user等都要按同样的规则改写一遍,不能偷懒只处理主表。
另一处需要重点处理的是种子数据。官方脚本里会给默认用户、默认执行器组等插入初始数据,这部分 SQL 基本是跨库兼容的,唯一要注意的是如果表用了 BIGSERIAL,插入时不要显式给 id 赋值,否则后续序列的计数器会跟不上,导致新增数据时报主键冲突。如果官方脚本里对某些表显式指定了 id,建议要么删掉 id 列,要么插入后手动执行一次序列重置语句:SELECT setval('xxl_job_info_id_seq', (SELECT max(id) FROM xxl_job_info))。
3.3 逐文件改 MyBatis mapper:分页、时间函数、模糊查询
建表脚本只是地基,真正的适配工作集中在 mapper XML。这里按优先级排列,第一处必改的是分页查询。官方任务信息的分页 SQL 用了 MySQL 的LIMIT #{offset}, #{pagesize}写法,在 PG 下执行直接报语法错误。以任务信息分页为例:
<!-- MySQL 写法 --> <select id="pageList" resultType="XxlJobInfo"> SELECT * FROM xxl_job_info <where> <if test="jobGroup != null and jobGroup != ''"> AND job_group = #{jobGroup} </if> </where> ORDER BY id DESC LIMIT #{offset}, #{pagesize} </select> <!-- PostgreSQL 写法 --> <select id="pageList" resultType="XxlJobInfo"> SELECT * FROM xxl_job_info <where> <if test="jobGroup != null and jobGroup != ''"> AND job_group = #{jobGroup} </if> </where> ORDER BY id DESC LIMIT #{pagesize} OFFSET #{offset} </select>逻辑说明:MyBatis 传入的offset和pagesize含义不变,只是 PG 的 SQL 语法要求把偏移量放到 OFFSET 后。我这里没有改动 Java 层,调用方传参方式完全兼容,这是最稳妥的改法。
第二处是模糊查询。xxl-job 的任务名搜索用的是LIKE CONCAT('%', #{jobDesc}, '%'),PG 支持CONCAT,但LIKE默认大小写敏感。改法有两种:一是把LIKE换成ILIKE,二是在字段上套LOWER。我推荐直接用ILIKE,改动最小:
<!-- 原 MySQL 写法 --> AND job_desc LIKE CONCAT('%', #{jobDesc}, '%') <!-- PostgreSQL 改写 --> AND job_desc ILIKE CONCAT('%', #{jobDesc}, '%')第三处是时间运算。日志清理接口需要根据保留天数删除过期日志,官方写法多半是DATE_SUB(NOW(), INTERVAL #{days} DAY)这类 MySQL 专属语法。PG 下要改成CURRENT_TIMESTAMP - (#{days} || ' days')::interval。注意||在 PG 里是字符串拼接运算符,::是类型转换,这两者和 MySQL 的习惯完全不同,写的时候要小心别混用。
第四处是注册中心相关的 SQL。执行器地址列表的注册、心跳更新的 SQL 里,如果出现了ON DUPLICATE KEY UPDATE这类 MySQL 专属语法,PG 必须改成标准的INSERT ... ON CONFLICT DO UPDATE,逻辑等价但语法完全不同。我建议把 xxl-job-admin 模块下所有 mapper XML 都过一遍,凡是 MySQL 独占语法,一个不留。
3.4 第四步:Timestamp 与日志查询的时区口径统一
SQL 改完,还隐藏着一个不容易察觉的问题:时间字段的读取口径。xxl-job 的任务日志里记录的是调度时间、执行时间、结束时间,这些时间由服务端通过 Java 写入。MySQL 在连接串里指定serverTimezone就能和服务器时间对齐,PG 的 JDBC 驱动默认取 JVM 时区,如果你没有在连接串里显式加TimeZone参数,同一个任务在日志里显示的时间和数据库实际存储的时间可能对不上,排查问题时会很迷惑。
我一般会在 JDBC URL 上显式加TimeZone=Asia/Shanghai,并在xxl_job_log等涉及时间字段查询的 mapper 里,统一使用CURRENT_TIMESTAMP作为默认值。这里多说一句:xxl-job 的部分时间字段在 Java 层赋值,数据库层的DEFAULT只影响新插入未显式赋值的情况,所以比时区更关键的是连接会话的时区,而不是表字段的默认值。改完这四处,数据库层面的适配基本完成,可以进入编译和回归验证阶段。
4. 编译、初始化与功能回归:适配做完不等于能上线
4.1 编译期容易炸的三个点
很多人在这一步以为万事大吉,直接mvn clean package一把梭,结果编译期就挂了。我遇到的第一个坑是 Maven 依赖冲突,官方 admin 模块里可能还有别的模块间接引用了 MySQL 驱动,光改一处 dependency 不够,编译时 MyBatis 初始化扫描到旧驱动类直接报类找不到。建议全局搜索mysql关键词,把间接引用一并清理。
第二个坑是 JDK 版本与依赖树不匹配。xxl-job 2.4.1 官方要求 JDK 8+,但 PG 驱动的高版本可能需要更高 JDK。如果团队还在用 JDK 8,建议在 dependencyManagement 里锁一个兼容 JDK 8 的 PG 驱动版本,别追新。第三个坑是资源过滤问题:mapper XML 里的 SQL 改动如果带有特殊字符(比如<![CDATA[ ... ]]>),Maven 在复制资源时可能过滤错乱,导致打包后的 XML 和源码不一致。出现这种诡异情况时,把 target/classes 下的 XML 解开看一眼即可确认,不用玄学猜。
mvn clean package -Dmaven.test.skip=true -pl xxl-job-admin -am参数说明:-pl指定只构建 admin 模块及它依赖的模块,-am让 Maven 同时构建依赖项。这一步的作用是缩短编译反馈循环,不用每次都全量打包。如果这一步过了,说明依赖和资源层面没问题,可以进初始化验证。
4.2 初始化验证:脚本执行、种子数据与默认账号
编译通过后,先别急着启动。把改写好的 PG 建表脚本在目标数据库上完整执行一遍,这一步不能跳过。执行时要重点观察三条信息:有没有报错、有没有 warning、种子数据是否插入成功。PG 的psql默认在出错时继续执行后面的语句,所以即使报错也可能只有部分输出,建议加-v ON_ERROR_STOP=1让错误直接中断:
psql -h 127.0.0.1 -U postgres -d xxl_job -v ON_ERROR_STOP=1 -f schema_pg.sql psql -h 127.0.0.1 -U postgres -d xxl_job -v ON_ERROR_STOP=1 -f data_pg.sql脚本执行完后,确认核心表数量、默认用户是否存在。官方脚本里会给默认管理员账号,而该账号的密码是经过某种哈希存储的,在不同数据库下的兼容性通常没问题,但登录失败时优先查这张用户表的数据,而不是怀疑账号密码。种子数据插入后,建议立刻用序列重置语句把所有表的自增序列对齐,这能避免后续新增操作偶发主键冲突,省得线上出幺蛾子。
4.3 回归清单:调度、触发、日志、注册中心四条链路
数据库初始化完成、服务能启动,并不代表适配成功。我的习惯是列一张四个链路的回归清单,从上到下依次验证:调度链路、触发链路、日志链路、注册中心链路。
调度链路验证的是新增任务后定时触发是否正常。在 admin 里新建一个测试任务,调度类型选固定速度或 Cron,观察执行记录有没有产生。这里如果分页 SQL 没改对,任务列表可能显示不出来,容易误判成调度器没启动。触发链路验证的是手动执行一次任务,检查执行日志的回写是否成功。很多隐藏的 ID 回填问题在这一步会现出原形。
日志链路验证的是任务日志查询、详情查看、日志清理三个子功能。尤其要验证清理功能,因为DATE_SUB这类时间运算的改动只在执行清理时才触发,平时根本测不出来。注册中心验证启动两台执行器,在 admin 里看执行器地址列表是否自动注册、心跳是否正常更新。这个链路最依赖ON CONFLICT这类 SQL 改写,不实际跑起来很难发现语法问题。四条链路全绿,才敢说适配基本完成。
5. xxl-job 2.4.1 PG 适配避坑指南:4 个翻车点与修复办法
5.1 坑一:任务模糊搜索时有时无,大小写导致的结果缺失
现象:任务列表里搜「Order」,之前能搜到的「order」任务不见了;切到 PG 后同样的关键词结果少了近一半,且不是 bug,重启后依旧如此。
原因:MySQL 在 utf8mb4 默认排序规则下 LIKE 不区分大小写,PG 的 LIKE 严格区分。官方 mapper 用的是 LIKE 拼接搜索,没做任何大小写处理,所以适配后所有模糊搜索行为都变了。
解决:把任务相关 mapper 中的LIKE替换为ILIKE。这是 PG 原生的不区分大小写匹配,语义上和 MySQL 默认行为对齐。注意ILIKE在 PG 9.x 之后的版本都支持,不用考虑老版本兼容问题。改完后记得同步检查任务名称搜索、执行器地址搜索等相关查询。
5.2 坑二:分页列表报错,LIMIT 两个参数写法的语法差异
现象:启动没问题,但进入任务管理页面直接报 500,日志里有syntax error at or near ","。
原因:官方 mapper 里写的是 MySQL 方言LIMIT #{offset}, #{pagesize},PG 在解析逗号形式的分页时直接报语法错误,这是纯方言问题,不是代码逻辑问题。
解决:把所有LIMIT #{offset}, #{pagesize}统一改成LIMIT #{pagesize} OFFSET #{offset}。注意别只改任务信息一个 mapper,登录日志、调度日志等所有分页查询都要同步改。改完后用grep再扫一遍,确保没有遗漏任何一个逗号分页写法。这一步要照做,因为分页查询在任务、日志、执行器列表里都有,漏一个就会在某个页面突然报错。
5.3 坑三:时间函数报错,日志清理任务一执行就失败
现象:调度任务本身能跑,但日志清理相关功能一旦触发,数据库连接直接抛异常,日志里报函数不存在。
原因:官方在清理逻辑里使用DATE_ADD、DATE_SUB这类 MySQL 时间函数,PG 没有对应内建函数,调用即报错。这个功能平时不触发,所以很多人在联调阶段发现不了,一上线跑个几天才炸。
解决:把时间运算改成 PG 的CURRENT_TIMESTAMP - (#{days} || ' days')::interval写法。注意||在 PG 里是字符串拼接,MySQL 里默认也是,但语义在数字与字符串混合时有差异,最好用CONCAT(#{days}, ' days')显式拼接,再做一个::interval类型转换。改完后用 pgAdmin 或 psql 手执一条 SQL 验证语法能跑,再通过 admin 界面实际触发一次清理,确认执行成功。
5.4 坑四:新增任务后 ID 回填失败,自增主键获取不到
现象:新增任务报「主键冲突」或「无法获取自增 ID」,日志里出现执行器执行失败,但表里数据已经插入。
原因:官方建表用的是AUTO_INCREMENT,我改成 PG 时如果用的是「先建序列再把 nextval 设为默认值」的方案,MyBatis 的useGeneratedKeys在 PG 驱动下不一定能从序列中取回插入的 ID。另一个可能是我在插入种子数据时显式指定过 id,导致序列计数器和表内已有数据不同步。
解决:表结构统一用BIGSERIAL,这是 PG 里最接近 AUTO_INCREMENT 的方案,MyBatis 的useGeneratedKeys能正确取到自增 ID。如果已经用序列方案建了表,在种子数据插入后手动执行SELECT setval('xxl_job_info_id_seq', (SELECT max(id) FROM xxl_job_info))重置序列。如果还取不到,兜底方案是在插入 SQL 后追加一条SELECT lastval()手动查回 ID,但这是最后的手段,优先用 BIGSERIAL。
6. 让改动可维护:把 PG 适配做成 patch 而不是 fork
适配跑通了只是开始,更现实的问题是以后官方版本升级怎么办。直接 fork 一个私有分支改源码,短期内最省事,但长期维护成本极高,官方每个版本打 tag 你都要手动合并一次,冲突会越来越重。我比较推荐的方式是:以官方 2.4.1 的 tag 为基础,单独维护一个适配分支,改动全部集中在两个目录——SQL 脚本和 mapper XML,并把所有改动点做统一标记。
git clone <官方仓库> xxl-job-pg cd xxl-job-pg git checkout -b adapt/pg 2.4.1 # 修改 SQL 脚本和 mapper XML 文件 # 在所有改动处标注 [PG-ADAPT] git commit -am "adapt xxl-job 2.4.1 to postgresql" git diff 2.4.1 > xxl-job-2.4.1-pg-adapt.patch这样做的价值在于:官方出新版本时,只需要把对应版本的 patch 打上去,逐个确认上下文是否仍然匹配。我在实际维护中还会在 README 里写一张改动清单,注明每个文件改了什么、为什么改、验证了哪些场景。这张清单比任何注释都管用,三个月后再看代码时能准确回忆起当时的决策。至于上游若在未来真正支持了 PG,这张 patch 也能帮助我们快速排查官方哪些需求已经被原生实现覆盖。
最后给一个个人习惯:在把适配后的调度中心接到生产环境前,我会先用 docker 起一个全新的 PG 实例,用PGPASSWORD=xxx psql -c "select 1"验证连通性,再按回归清单把四条链路完整跑一遍。这套动作和改法本身一样重要,毕竟 PostgreSQL 版本、时区、驱动版本的细微差异都可能让同样的 SQL 表现不同。适配不是一锤子买卖,把它当成工程资产来管理,后续升级才不会翻车。希望这篇适配笔记能帮你把 xxl-job 2.4.1 顺利落到 PostgreSQL 上。
本文还有配套的精品资源,点击获取