项目背景
在将若依(Ruoyi)框架的任务调度模块从其他数据库迁移到OpenGauss时,遇到了Quartz调度器与数据库兼容性问题。OpenGauss作为华为推出的开源关系型数据库,虽然兼容PostgreSQL协议,但在实际使用Quartz时仍遇到了一些特殊问题。
适配步骤
- 初始化SQL语句
注:提供的初始化SQL语句已经将下方提到的长度报错信息解决
mysql
## Quartz
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS CASCADE;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE CASCADE;
DROP TABLE IF EXISTS QRTZ_LOCKS CASCADE;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_TRIGGERS CASCADE;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS CASCADE;
DROP TABLE IF EXISTS QRTZ_CALENDARS CASCADE;
-- 1、存储每一个已配置的 jobDetail 的详细信息
CREATE TABLE QRTZ_JOB_DETAILS (
sched_name varchar(120) NOT NULL,
job_name varchar(200) NOT NULL,
job_group varchar(200) NOT NULL,
description varchar(250) NULL,
job_class_name varchar(250) NOT NULL,
is_durable varchar(5) NOT NULL,
is_nonconcurrent varchar(5) NOT NULL,
is_update_data varchar(5) NOT NULL,
requests_recovery varchar(5) NOT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, job_name, job_group)
);
-- 2、 存储已配置的 Trigger 的信息
CREATE TABLE QRTZ_TRIGGERS (
sched_name varchar(120) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
job_name varchar(200) NOT NULL,
job_group varchar(200) NOT NULL,
description varchar(250) NULL,
next_fire_time bigint NULL,
prev_fire_time bigint NULL,
priority integer NULL,
trigger_state varchar(16) NOT NULL,
trigger_type varchar(8) NOT NULL,
start_time bigint NOT NULL,
end_time bigint NULL,
calendar_name varchar(200) NULL,
misfire_instr smallint NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, job_name, job_group) REFERENCES QRTZ_JOB_DETAILS(sched_name, job_name, job_group)
);
-- 3、 存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
sched_name varchar(120) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
repeat_count bigint NOT NULL,
repeat_interval bigint NOT NULL,
times_triggered bigint NOT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
);
-- 4、 存储 Cron Trigger,包括 Cron 表达式和时区信息
CREATE TABLE QRTZ_CRON_TRIGGERS (
sched_name varchar(120) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
cron_expression varchar(200) NOT NULL,
time_zone_id varchar(80),
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
);
-- 5、 Trigger 作为 Blob 类型存储
CREATE TABLE QRTZ_BLOB_TRIGGERS (
sched_name varchar(120) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
blob_data bytea NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
);
-- 6、 以 Blob 类型存储存放日历信息
CREATE TABLE QRTZ_CALENDARS (
sched_name varchar(120) NOT NULL,
calendar_name varchar(200) NOT NULL,
calendar bytea NOT NULL,
PRIMARY KEY (sched_name, calendar_name)
);
-- 7、 存储已暂停的 Trigger 组的信息
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
sched_name varchar(120) NOT NULL,
trigger_group varchar(200) NOT NULL,
PRIMARY KEY (sched_name, trigger_group)
);
-- 8、 存储与已触发的 Trigger 相关的状态信息
CREATE TABLE QRTZ_FIRED_TRIGGERS (
sched_name varchar(120) NOT NULL,
entry_id varchar(95) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
instance_name varchar(200) NOT NULL,
fired_time bigint NOT NULL,
sched_time bigint NOT NULL,
priority integer NOT NULL,
state varchar(16) NOT NULL,
job_name varchar(200) NULL,
job_group varchar(200) NULL,
is_nonconcurrent varchar(5) NULL,
requests_recovery varchar(5) NULL,
PRIMARY KEY (sched_name, entry_id)
);
-- 9、 存储少量的有关 Scheduler 的状态信息
CREATE TABLE QRTZ_SCHEDULER_STATE (
sched_name varchar(120) NOT NULL,
instance_name varchar(200) NOT NULL,
last_checkin_time bigint NOT NULL,
checkin_interval bigint NOT NULL,
PRIMARY KEY (sched_name, instance_name)
);
-- 10、 存储程序的悲观锁的信息
CREATE TABLE QRTZ_LOCKS (
sched_name varchar(120) NOT NULL,
lock_name varchar(40) NOT NULL,
PRIMARY KEY (sched_name, lock_name)
);
-- 11、 Quartz集群实现同步机制的行锁表
CREATE TABLE QRTZ_SIMPROP_TRIGGERS (
sched_name varchar(120) NOT NULL,
trigger_name varchar(200) NOT NULL,
trigger_group varchar(200) NOT NULL,
str_prop_1 varchar(512) NULL,
str_prop_2 varchar(512) NULL,
str_prop_3 varchar(512) NULL,
int_prop_1 int NULL,
int_prop_2 int NULL,
long_prop_1 bigint NULL,
long_prop_2 bigint NULL,
dec_prop_1 numeric(13,4) NULL,
dec_prop_2 numeric(13,4) NULL,
bool_prop_1 varchar(1) NULL,
bool_prop_2 varchar(1) NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group)
);
COMMIT;
-
引入依赖
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>
问题一:数据库字段长度不匹配
错误现象
Quartz调度器在尝试保存任务时出现"value too long for type character varying(1)"错误
问题分析
Quartz的QRTZ_JOB_DETAILS表中布尔类型字段原本定义为varchar(1),但Quartz内部会存储"true"或"false"这样的字符串值,长度超过1个字符,导致插入失败。
解决方案
修改Quartz初始化SQL脚本,将所有布尔字段的长度从varchar(1)扩展为varchar(5)
mysql
-- 修改前的字段定义
is_durable varchar(1) NOT NULL,
is_nonconcurrent varchar(1) NOT NULL,
is_update_data varchar(1) NOT NULL,
requests_recovery varchar(1) NOT NULL,
-- 修改后的字段定义
is_durable varchar(5) NOT NULL,
is_nonconcurrent varchar(5) NOT NULL,
is_update_data varchar(5) NOT NULL,
requests_recovery varchar(5) NOT NULL,
修改涉及的表包括:
QRTZ_JOB_DETAILSQRTZ_FIRED_TRIGGERSQRTZ_SIMPROP_TRIGGERS
问题二:应用启动时的初始化问题
问题描述
应用启动时,@PostConstruct注解的init()方法会尝试从数据库加载所有定时任务并重新创建调度任务。如果数据库中已存在损坏或不兼容的数据,会导致应用启动失败。
解决方案
临时注释掉初始化代码,先确保应用能够正常启动:
// 临时注释掉,防止应用启动失败
//@PostConstruct
public void init() throws SchedulerException, TaskException
{
scheduler.clear();
List<SysJob> jobList = jobMapper.selectJobAll();
for (SysJob job : jobList)
{
ScheduleUtils.createScheduleJob(scheduler, job);
}
}
问题三:定时任务工具类优化
问题分析
在创建定时任务时,没有明确设置作业的持久化属性,可能导致Quartz使用默认值,与数据库表结构不兼容。
解决方案
在ScheduleUtils.createScheduleJob()方法中明确设置作业持久化属性:
public static void createScheduleJob(Scheduler scheduler, SysJob job)
throws SchedulerException, TaskException
{
Class<? extends Job> jobClass = getQuartzJobClass(job);
// 构建job信息
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(getJobKey(jobId, jobGroup))
.storeDurably(false) // 明确设置持久化属性
.build();
// ... 其余代码
}
关键改进
- 明确设置storeDurably(false),确保作业持久化属性有确定值
- 避免Quartz内部使用默认的长字符串值
- 确保数据库中存储的是明确的值(如"false")
问题四:数据类型转换错误
错误信息
org.quartz.JobPersistenceException: Couldn't obtain triggers for job: 不良的类型值 long : \x
错误分析
这个错误表明Quartz在从OpenGauss数据库读取触发器信息时,遇到了long类型数据转换问题。\x表示二进制数据,说明数据库返回的数据格式与Quartz期望的long类型不匹配。
可能原因
数据库方言不匹配:Quartz默认可能不支持OpenGauss数据库的特定数据类型处理bigint类型处理问题:OpenGauss的bigint类型与PostgreSQL的bigint处理方式可能存在差异bytea字段处理问题:job_data字段为bytea类型,可能在某些情况下被错误解析数据损坏:数据库中已存在损坏的触发器数据
完整解决方案
- 修改Quartz配置类
在ScheduleConfig.java中添加OpenGauss/PostgreSQL特定的配置:
java
@Configuration
public class ScheduleConfig
{
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource)
{
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
// quartz参数
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
prop.put("org.quartz.threadPool.threadPriority", "5");
// JobStore配置
prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
// 关键配置:添加PostgreSQL委托类支持OpenGauss
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
// 集群配置
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10");
prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
factory.setQuartzProperties(prop);
factory.setSchedulerName("RuoyiScheduler");
// 延时启动
factory.setStartupDelay(1);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
// 启动时更新已存在的Job
factory.setOverwriteExistingJobs(true);
// 设置自动启动
factory.setAutoStartup(true);
return factory;
}
}
- 清理数据库中损坏的数据
如果数据库中已存在损坏的触发器数据,需要先进行清理:
mysql
-- 1. 备份现有数据(建议操作前先备份)
CREATE TABLE QRTZ_TRIGGERS_BACKUP AS SELECT * FROM QRTZ_TRIGGERS;
CREATE TABLE QRTZ_SIMPLE_TRIGGERS_BACKUP AS SELECT * FROM QRTZ_SIMPLE_TRIGGERS;
CREATE TABLE QRTZ_CRON_TRIGGERS_BACKUP AS SELECT * FROM QRTZ_CRON_TRIGGERS;
CREATE TABLE QRTZ_JOB_DETAILS_BACKUP AS SELECT * FROM QRTZ_JOB_DETAILS;
-- 2. 查找并删除损坏的触发器数据
-- 注意:以下操作会删除数据,请确保已备份
DELETE FROM QRTZ_CRON_TRIGGERS
WHERE (sched_name, trigger_name, trigger_group) IN (
SELECT sched_name, trigger_name, trigger_group
FROM QRTZ_TRIGGERS
WHERE next_fire_time IS NULL
OR CAST(next_fire_time AS TEXT) LIKE '\x%'
);
DELETE FROM QRTZ_SIMPLE_TRIGGERS
WHERE (sched_name, trigger_name, trigger_group) IN (
SELECT sched_name, trigger_name, trigger_group
FROM QRTZ_TRIGGERS
WHERE next_fire_time IS NULL
OR CAST(next_fire_time AS TEXT) LIKE '\x%'
);
DELETE FROM QRTZ_TRIGGERS
WHERE next_fire_time IS NULL
OR CAST(next_fire_time AS TEXT) LIKE '\x%';
-- 3. 清理孤立的Job详情
DELETE FROM QRTZ_JOB_DETAILS
WHERE NOT EXISTS (
SELECT 1 FROM QRTZ_TRIGGERS
WHERE QRTZ_TRIGGERS.job_name = QRTZ_JOB_DETAILS.job_name
AND QRTZ_TRIGGERS.job_group = QRTZ_JOB_DETAILS.job_group
);
启动

