Quartz - JDBC-Based JobStore事务管理及锁机制

文章详细介绍了Quartz在基于JDBC的JobStore中如何进行事务管理和实现锁机制,包括JobStoreTX和JobStoreCMT两种事务管理实现,以及Semaphore接口和SimpleSemaphore、DBSemaphore两种锁的使用。在多任务并发或集群环境下,事务管理和锁机制对于保证任务正确执行至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由于JDBC-Based JobStore在进行job注册、trigger注册、任务调度及执行过程中需要操作数据库,而且会涉及到多张表,比如trigger注册的时候会根据不同情况写入triggers、simple_triggers或cron_triggers表,在执行任务的时候会读取和更新triggers、job_details、simple_triggers、cron_triggers、fired_triggers等。这些操作都有事务性要求:要么全部成功、要么全部失败,否则就会导致数据不一致,最终会影响到任务的正确调度和执行。

#### Quartz的事务管理

JDBC-Based JobStore有两个JobStore的最终实现类,一个是JobStoreTX,一个是JobStoreCMT,都继承自抽象类JobStoreSupport。

![image.png](/img/bVc6uPr)

这两个实现类都是为JDBC-Based JobStore提供事务管理能力的,其中JobStoreTX是自己实现事务管理的,事务的开启、commit、rollback都由JobStoreTX控制。

JobStoreCMT是依赖于容器来管理事务的,他把事务管理的职责交给了运行环境,他自己本身不做事务管理。比如可以交给Spring来进行事务管理。

具体使用哪一个JobStore是通过Quartz的配置文件指定的:

```

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX

```

配置为JobStoreCMT的情况下需要在配置文件中额外指定nonManagedTXDataSource:不受Quartz管理的事务的数据源。

两种事务管理机制都支持一个属性:dontSetAutoCommitFalse,字面含义是不允许设置数据库连接的autoCommit为false,实际含义就是不允许开启事务。这个参数的默认值为false:允许开启事务,在这个默认设置下,Quartz在获取到数据库连接后会设置其autoCommit为false,也就是相当于开启了事务,一般情况下这个参数也不需要修改。

但是如果是采用JobStoreCMT、交给容器管理事务的话,应该是可以设置dontSetAutoCommitFalse为true从而彻底交给容器来启用、commit、rollback事务的,这部分内容有待验证!

#### Quartz的锁机制

启用事务管理之后是不是就可以万无一失的确保任务的正确执行呢?在多任务并发、或者在cluster的环境下,并发任务可能存在同时访问同一条数据的可能,仅仅是事务管理还不足以确保任务的正确执行,还需要引入锁机制。

Quartz提供了一个叫Semaphore的接口来实现锁,Semaphore接口有obtainLock、releaseLock、requiresConnection 3个方法,分别用来获取锁、释放锁、以及判断当前锁对象在执行锁操作的时候是否需要数据库连接的支持。

![image.png](/img/bVc6vrO)

从Semaphore的类结构可以看到他有两个不同的实现类:

1. SimpleSemaphore:基于内存的锁机制

2. DBSemaphore:基于数据库的锁机制

具体采用哪种类型的锁可以通过配置文件指定:

org.quartz.jobStore.lockHandler.class=SimpleSemaphore

#### 两种锁机制的区别

基于内存的锁机制可以称之为“轻量级锁”,操作速度快、资源占用少、锁等待时间短,不需要底层数据库的支持。但是基于内存的锁机制不能实现跨应用的锁,在集群环境下基于内存的锁机制无法实现目标。

对比而言,基于数据库的锁是“重量级锁”,通过给数据库表(qrtz_lock)的某一行或者整张表加锁从而实现当前线程对资源的锁定。基于数据库的锁可以支持集群环境。

#### 加锁与不加锁操作

JobStoreSupport中提供了两种数据库操作方法:

1. executeInLock:加锁操作数据库

2. executeWithoutLock:不加锁操作数据库

这是因为加锁操作数据库的时候会造成想要获取同一信息的其他线程的锁等待,轻则影响性能,重则造成操作超时从而影响任务的正常调度执行。所以,Quartz就提供了这两种数据库操作,只给那些对数据非常敏感的操作加锁,非必要的情况下就不加锁。

比如在注册job和trigger的时候就加锁,因为注册操作并不是一个数据库操作、而是一系列数据库操作,只有所有的注册操作完成之后,才能允许调度任务开始调度该作业,所以注册操作必须加锁执行。

而某些查询功能比如getTriggerState、retrieveTrigger等等就不需要加锁,所以采用不加锁方式访问数据库,无疑会提高性能、有效避免锁超时、提高应用性能。

#### Quartz的锁对象

从Quartz需要访问的资源来看,需要上锁的有两种:

1. TRIGGER_ACCESS:也就是访问TRIGGER的时候需要上锁,这也比较容易理解,因为不管是作业的注册、还是调度执行,需要频繁操作TRIGGER

2. STATE_ACCESS:访问集群服务器状态表(qrtz_scheduler_state)的时候需要上锁,因为集群环境下多个服务器可能需要同时访问、更新状态表

#### 内存锁SimpleSemaphore

Quartz在非集群环境下的默认锁机制为内存锁SimpleSemaphore。

SimpleSemaphore提供一个锁容器(HashSet)locks,以及一个ThreadLocal变量lockOwners。

obtainLock方法:加锁操作前获取锁资源(TRIGGER_ACCESS或STATE_ACCESS)。首先检查lockOwners,如果锁资源已经被当前线程获取(在lockOwners中)则无需等待、直接返回。否则,检查locks中是否存在该资源,存在的话说明当前锁资源已经被其他线程获取,则当前线程需等待锁释放。一旦其他线程释放后,则当前线程将该锁资源存入locks,同时登记lockOwners。

releaseLock方法:加锁操作执行完成后释放锁资源。检查lockOwners如果当前线程已经锁定该资源的话,则将当前锁资源从lockOwners和locks中移除。

#### DBSemaphore基于数据库的锁

DBSemaphore是抽象类,有两个落地实现类StdRowLockSemaphore和UpdateLockRowSemaphore:

1. StdRowLockSemaphore:通过select ... for update实现行锁,Quartz默认使用

2. UpdateLockRowSemaphore:通过update 语句实现行锁,对于不支持通过select for update加锁的数据库,比如 MS SQLServer,需要采用UpdateLockRowSemaphore

DBSemaphore的原理其实也非常简单:当Quartz判断某一操作需要锁定资源的时候,首先区分一下需要行级锁还是表级锁,如果需要行级锁则通过select ... for update锁定qrtz_locks表中的指定行(TRIGGER_ACCESS或STATE_ACCESS),当需要表级锁的时候就使用insert语句锁定整张表。

obtainLock方法:其实就是执行上述行级锁或表级锁的操作,但由于数据库锁的开销比较大,所以在执行锁定之前首先通过lockOwners判断当前线程是否已经获得了锁,已经获得锁的话就不再执行sql语句去获得锁了,节约开销。

releaseLock方法:从lockOwners移除锁资源,数据库锁是不需要显式的操作去释放的,事务提交或回滚之后自然就释放了锁。

#### 小结

今天完成了Quratz的事务管理及锁机制的分析,Quartz的集群管理稍后分析。

Thanks a lot!

上一篇 [Quartz - JDBC-Based JobStore](https://segmentfault.com/a/1190000043453014)

下一篇 [Quartz - 集群Cluster的配置、failOver原理](https://segmentfault.com/a/1190000043471483)

### 实现Spring Boot中的Quartz集群 在Spring Boot项目中实现Quartz集群能够确保任务调度的高可用性和可伸缩性。为了达到这一目标,需遵循特定配置流程。 #### 1. Maven依赖设置 要在Spring Boot应用里启用Quartz并支持集群功能,首先应在`pom.xml`文件内加入必要的Maven依赖项: ```xml <dependencies> <!-- Spring Boot Starter Quartz --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <!-- 数据库驱动 (以MySQL为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> ``` 此部分操作使得应用程序具备了基本的任务调度能力以及连接到外部数据库的能力[^1]。 #### 2. 配置数据源与JDBC JobStore 为了让多个实例间共享相同的调度状态信息,必须采用持久化的存储方式保存这些元数据。通常情况下会选择关系型数据库作为后台存储介质,并通过配置`application.properties`或`application.yml`指定相应的参数: 对于`application.properties`: ```properties # DataSource configuration for MySQL database spring.datasource.url=jdbc:mysql://localhost:3306/quartz_db?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=password # Configure JDBC-based job store and clustering properties spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_ spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000 ``` 上述配置指定了使用基于JDBC的工作商店(`JobStore`),启用了聚簇模式(isClustered),并且设置了检查间隔(clusterCheckinInterval)[^4]。 #### 3. 创建自定义Job类 接下来创建具体的作业逻辑,即继承于`org.quartz.Job`接口的新类。此类负责处理每次触发时应执行的具体业务行为: ```java import org.quartz.*; import java.util.Date; public class MyCustomJob implements Job { @Override public void execute(JobExecutionContext context){ System.out.println("Executing custom job at " + new Date()); try{ Thread.sleep(5 * 1000); // Simulate long-running task }catch(Exception e){ throw new RuntimeException(e); } System.out.println("Finished executing custom job."); } } ``` 这段代码展示了怎样编写简单的定时任务处理器。 #### 4. 定义CronTrigger Bean 最后一步是在Spring上下文中注册一个或多个触发器bean,用于控制何时启动对应的作业实例。这里给出了一种利用cron达式的例子: ```java @Configuration public class SchedulerConfig { @Bean public JobDetail myCustomJob(){ return JobBuilder.newJob(MyCustomJob.class).withIdentity("myCustomJob").build(); } @Bean public CronTrigger cronTriggerForMyCustomJob(){ String cronExpression = "0/10 * * * * ?"; // Every ten seconds return TriggerBuilder.newTrigger() .forJob(myCustomJob()) .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) .build(); } } ``` 以上就是整个过程的大致描述,当部署多份相同的应用程序副本至不同服务器上之后,它们会自动形成一个协调工作的群体——这就是所谓的“Quartz Cluster”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值