由于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。
这两个实现类都是为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个方法,分别用来获取锁、释放锁、以及判断当前锁对象在执行锁操作的时候是否需要数据库连接的支持。
从Semaphore的类结构可以看到他有两个不同的实现类:
- SimpleSemaphore:基于内存的锁机制
- DBSemaphore:基于数据库的锁机制
具体采用哪种类型的锁可以通过配置文件指定:
org.quartz.jobStore.lockHandler.class=SimpleSemaphore
两种锁机制的区别
基于内存的锁机制可以称之为“轻量级锁”,操作速度快、资源占用少、锁等待时间短,不需要底层数据库的支持。但是基于内存的锁机制不能实现跨应用的锁,在集群环境下基于内存的锁机制无法实现目标。
对比而言,基于数据库的锁是“重量级锁”,通过给数据库表(qrtz_lock)的某一行或者整张表加锁从而实现当前线程对资源的锁定。基于数据库的锁可以支持集群环境。
加锁与不加锁操作
JobStoreSupport中提供了两种数据库操作方法:
- executeInLock:加锁操作数据库
- executeWithoutLock:不加锁操作数据库
这是因为加锁操作数据库的时候会造成想要获取同一信息的其他线程的锁等待,轻则影响性能,重则造成操作超时从而影响任务的正常调度执行。所以,Quartz就提供了这两种数据库操作,只给那些对数据非常敏感的操作加锁,非必要的情况下就不加锁。
比如在注册job和trigger的时候就加锁,因为注册操作并不是一个数据库操作、而是一系列数据库操作,只有所有的注册操作完成之后,才能允许调度任务开始调度该作业,所以注册操作必须加锁执行。
而某些查询功能比如getTriggerState、retrieveTrigger等等就不需要加锁,所以采用不加锁方式访问数据库,无疑会提高性能、有效避免锁超时、提高应用性能。
Quartz的锁对象
从Quartz需要访问的资源来看,需要上锁的有两种:
- TRIGGER_ACCESS:也就是访问TRIGGER的时候需要上锁,这也比较容易理解,因为不管是作业的注册、还是调度执行,需要频繁操作TRIGGER
- 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:
- StdRowLockSemaphore:通过select ... for update实现行锁,Quartz默认使用
- 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!