XPlatform Job机制设计分析

来源:互联网 发布:nginx 反向代理 iis 编辑:程序博客网 时间:2024/05/01 02:18

XPlatform  Job机制设计分析

AuthorWenchu.cenwc 岑文初

Date2007-5-10

Emailwenchu.cenwc@alibaba-inc.com 

Job机制概述... 2

XPlatform Job解构... 2

平台服务使用的补充... 5

Quartz的简单介绍... 5

Quartz和平台的Job的比较... 10

XPlatfrom 新任务机制设计与实现... 11

新任务机制设计... 11

接口... 11

实现... 11

测试... 12

关于Quartz的一点补充... 13

 


Job机制概述

XPlatform提供了比较不错的Job机制,由于早期没有仔细分析内部的设计,所以一直没有对Job引起足够的重视,同时正好也看了Quartz的这个设计,最后将会把这两种Job机制设计做一个对比,当然都各有各的优点,也有各自的不足,在后面的时间中如果能够将两者优点结合起来,那无疑将会把XPlatform的任务机制设计的更完善和灵活。(异步执行的job部分尚未研究)

XPlatform Job解构

一.数据库设计

XPlatformJob实现是基于数据库存储来实现的,因此先介绍一下它的表结构设计,同时也是Job对象的设计。

一共是四张表xp_runtime_data, xp_job_sandbox, xp_recurrence_info, xp_recurrence_rule

1 数据库表结构图

xp_runtime_data是用来存储任务内容部分(类似于QuartzJob的含义)。

xp_recurrence_rule是用来配置fire job的规则(类似于Quartztrigger,不过更确切的说要加上后面提到的xp_recurrence_info才算是完整的trigger),里面包含的信息为频率,间隔,次数,结束时间。

xp_recurrence_info也是配置fire job的一部分信息以及一些与rule配合信息,例如recurrence_count字段就是用来标注当前的job已经被执行了多少次了。

xp_job_sandbox就是要执行的具体的任务记录表,表中记录了开始时间,job名称,服务名称,前面几张表的id。这张表中的信息每一条就代表一条被执行的任务,有些已经被执行了有些还未被执行,都通过表中的字段可以区分出来,后面涉及到流程的时候将会有比较详细的说明。

 

二.类结构关系

2 类结构关系图

 

JobManager:负责管理和维护所有的定制任务,任务内容从数据库中获得。

JobPoller:负责管理维护任务处理线程池,任务执行列表。

JobInvoker:负责获取任务并且执行任务。

 

3 任务后台执行的基本流程

 

需要注意的:

首先JobPooler内部有两个linkedList,一个用来作为任务执行线程池,另一个用来作为可执行任务列表。

配置任务执行线程池是配置model/components/service/config目录下的serviceengine.xml文件。以下说明一下参数的含义:红色的表示我们都没有使用的,其实很有用。

<thread-pool ttl="18000000"

                 wait-millis="750"

                 jobs="2"

                 min-threads="5"

                 max-threads="15"

                 poll-enabled="true"

                 poll-db-millis="20000"/>

ttl="18000000"//线程池中线程的生命周期,到了时间就会自动被结束。(毫秒单位)

jobs="2"//每个线程负责的job数量,根据这个参数会动态的扩展线程池当时的线程数量

min-threads="5"//最小线程数,初始化就在池中放入这些线程

max-threads="15"//最大线程数

poll-enabled="true"//是否允许线程池

poll-db-millis="20000"//JobPooler运行时的间隔时间(毫秒单位)

wait-millis="750"//是每个JobInvoker线程运行的间隔时间(毫秒单位)

failed-retry-min//失败后下一次执行的时间间隔,也就是当前执行的时间点+failed-retry-min为下一次执行的时间。(这个参数如果不填,默认30分钟)

run-from-pool//这个不能作为属性,要作为子element来配置,如果配置了多个,那么在执行查询符合条件可以执行的job的时候就需要在附加jobpoolId必须包含在这个列表中。十分适合分布式任务的处理。

 

JobManager从数据库中获取符合条件可以执行的任务,条件依据如下:

runTime小于等于当前时间,startDateTime is null ,cancelDateTime is null以及前面提到的run-from-pool参数配置情况。

 

平台服务使用的补充

这次查找问题的时候发现了一个很严重的问题,由于平台早期没有什么文档参考,业务配置都是参看老的配置范例,那么导致对于服务配置的错误使用。

Service会配置在每个模块自己的service目录下面,每个配置文件都会有多个service的配置。那么service的属性究竟有多少个呢,具体怎么使用呢,我这边重要的强调两个参数,是现在配置文件中多没有使用的。

平台的事务使用的是JTA事务,一方面考虑原来可能是不同的数据源,另外这里也可以处理一些非JDBC事务的情况。但是平台对于服务的事务配置十分灵活,不是对所有的事务统一一种配置,而是自己可以在服务定义的属性中自己配置。属性名称分别为:use-transactiontransaction-timout,前者是是否需要使用事务,后者是是否配置事务超时时间。

举一下例子,在我们的日程提醒的邮件部分,有一个邮件服务定义,里面这两项都没有配置。

那么后台将会怎么处理呢,首先获取数据库连接处理一些信息获取操作以及预先更新操作,然后调用发送邮件服务函数,最后在结束数据库处理,事务结束,如果中间出现异常,那么就回滚整个事务。当没有配置是否需要事务参与以及事务超时时间的话,那么默认就是需要事务处理,超时时间就是0,也就是无超时,当发送邮件时间无限拉长的话,那么首先就会死等,然后数据库连接就被占用,在平台数据库资源我想大家都知道十分宝贵,那么这样的情况发生,数据库连接资源耗尽,就很容易发生。

因此,需要说明的就是,首先如果服务中没有必要配置事务的情况,就不要配置事务,把use-transaction=”false”,特别是中间有些对外的操作或者十分耗时的操作,就算要使用事务,那么请一定要设置超时时间(单位为秒)。也就是说平台不允许在需要事务的情况下不配置超时时间。这点十分重要,需要引起所有的开发人员以及配置人员注意。

 

Quartz的简单介绍

Quartz是开源的一个项目,现在广泛用于各个项目的任务机制实现中。

 

下面是个人的初步学习的内容,有很多不一定正确后续需要进一步实践验证。

 

Quartz的类图

4 Quartz的类图

       Quartz内部主要就是这些类:

       Job接口只有一个方法需要实现,其中JobExecutionContext可以作为运行时参数传递的上下文。用户自己定义自己的JobImpl就可以了。

       JobDetail类用来保存Job的状态信息(Job的名称,Job所属组,Job实际调用的实现类)。

       Trigger配置激发Job运行的参数。

       Schedule就是将JobJob关联的Trigger放入到执行队列中,然后通过线程池内的线程动态处理Job

       其实在很多设计中可以将JobDetailTrigger合在一起,但是为了更好的重用Job定义以及灵活配置,将两者分开是理所当然的。

 

Quartz 结构是模块化的,如果要使它正常运行需要以下各个模块配合工作:

a.ThreadPool

b.JobStore

c.DataSources (if necessary)

d.The Scheduler itself

 

ThreadPool的介绍

       ThreadPool提供了线程池来执行任务。有些用户一般设置为5个可以应付100个左右的任务,但不是同时并发的情况下。任务一般情况下都是耗时比较短。还有用户设置10,15,50主要是根据各自的情况不同配置。如果线程不够,那么Quartz就会被阻塞,这就可能造成misfiremisfire可以配置策略来处理后续问题。

 

JobStore的介绍

       在上面提到的这些类的背后,其实还有很重要的一个内容,就是JobStoreJobStore负责保存所有关于scheduler的工作数据:job,triggers,calendars等。选择一个适合的JobStore来配置Scheduler十分重要。在前面提到过平台现在的任务机制是基于数据库方式的,而Quartz却是可以分为两种方式内存方式和数据库方式。

       RAMJobStore是最简单的JobStore,效率最高,他将所有信息放在RAM中,缺点就是信息会丢失。

JDBCJobStore是将所有信息都放入到数据库中。数据库中需要建好索引来提高效率。首先需要创建一系列的表,可以在docs/dbTables下面找到一些数据库脚本,所有的表都是QRTZ_开头,前缀可以替换,只需要配置文件中配置即可。需要配置一下事务处理的类型(org.quartz.impl.jdbcjobstore.JobStoreTX or org.quartz.impl.jdbcjobstore.JobStoreCMT)。配置数据源的范例文件在docs/config下面。主要是下面几个配置要注意:

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegateoracle也有自己的delegate//数据库驱动代理类

org.quartz.jobStore.tablePrefix = QRTZ_   //数据库表前缀

org.quartz.jobStore.dataSource = myDS    //数据源的配置

如果你的Scheduler十分忙,基本上总是线程池满负荷,那么将数据库连接数设置为线程池大小+1

       可以设置参数org.quartz.jobStore.usePropertiestrue,默认是false,如果JobDataMaps里面的所有的值都是字符串,那么就会以name-value对被保存在数据库中,不用序列化以后保存到Blob字段中,极大地提高效率。

 

需要说明的几点:

1.  Schedule中每一次如果符合了Job被激发的情况时,后台将会根据JobDetail提供的类名,生成一个类实例,然后执行这个Job实现类的默认执行方法,因此需要这个类起码要提供一个无参数的构造函数。

2.  JobDataMap,它分别在TriggerjobDetail两个对象中都有,都可以put参数,然后到job运行期的时候获取,两者的差别很简单一个是关联到这个Trigger的,一个是关联到jobDetail的。 jobDetail.getJobDataMap().put("jobSays", "Hello World!");可以通过这种方式将需要传入的参数放置到JobDetailJobDataMap当中,然后通过JobExecutionContext可以获得jobDetailJobDataMap,这样就可以在运行过程中获得传入的参数。(如果你使用持久化的JobStore的话,就需要注意存储在JobDataMap中的内容,因为存储进去的内容将会被序列化,这可能就会出现class的版本问题。)

3.  Job的实例可以分为statefulnon-stateful两类。无状态的job只有在被加入到scheduler的时候才会把JobDataMap存储起来。这就意味着在执行job的时候,修改了job data map的内容的时候修改信息将会被丢失,同时也不会体现在下一次执行的时候。stateful job正好相反,它的jobDataMap每次job被执行以后会被再次存储。但是stateful不能并发执行,也就是说如果一个线程去执行一个已经被执行的statefuljob,那么他就会被阻塞住。如果要使用状态job,那就实现statefulJob接口。

4.  Job的几个参数:

Durability:如果job是非Durability的话,那么在scheduler中如果没有任何trigger和这个job相关联,那么这个job将会被删除。

Volatility:如果jobvolatility的话,那么schedulerrestart的时候将不会把job持久化。

RequestsRecovery:任务如果被强制结束,例如宕机之类的,那么重新启动以后这个任务将会被重新执行。

JobListeners:一个job可以有一个或者多个JobListeners和他关联。当job执行的时候,listeners将会被通知。

5.  Calendar接口的介绍:

Calendar可以将Trigger的激发计划中不需要的时间点排除出去。例如可以创建一个trigger在每个工作日的早晨九点半激发job,但是除去所有的休息日。

 

6.  Trigger有优先级的设置(v1.6提供),默认是5,越大优先级越高,允许负数。TriggerUtils用来创建各种Trigger十分有效。

7.  两种常用的Trigger介绍:

SimpleTrigger:适用于只需要在特定时间执行一次或者在某个时间点以后固定间隔循环执行的任务。

public SimpleTrigger(String name, String group, Date startTime,Date endTime, int repeatCount, long repeatInterval)

repeatCountrepeatInterval都必须大于等于0。如果endTime被设置,那么将会覆盖repeatCount属性。

 

SimpleTrigger Misfire的策略:

MISFIRE_INSTRUCTION_FIRE_NOW

MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT

MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT

 

CronTrigger:类似于日历形式的激发任务。

按照顺序配置对应的日历字段:

Seconds

Minutes

Hours

Day-of-Month

Month

Day-of-Week

Year (optional field)

 

使用范例:

Wild-cards (the '' character) can be used to say "every" possible value of this field. Therefore the '' character in the "Month" field of the previous example simply means "every month". A '*' in the Day-Of-Week field would obviously mean "every day of the week".

 

All of the fields have a set of valid values that can be specified. These values should be fairly obvious - such as the numbers 0 to 59 for seconds and minutes, and the values 0 to 23 for hours. Day-of-Month can be any value 0-31, but you need to be careful about how many days are in a given month! Months can be specified as values between 0 and 11, or by using the strings JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC. Days-of-Week can be specified as vaules between 1 and 7 (1 = Sunday) or by using the strings SUN, MON, TUE, WED, THU, FRI and SAT.

 

The '/' character can be used to specify increments to values. For example, if you put '0/15' in the Minutes field, it means 'every 15 minutes, starting at minute zero'. If you used '3/20' in the Minutes field, it would mean 'every 20 minutes during the hour, starting at minute three' - or in other words it is the same as specifying '3,23,43' in the Minutes field.

 

The '?' character is allowed for the day-of-month and day-of-week fields. It is used to specify "no specific value". This is useful when you need to specify something in one of the two fields, but not the other. See the examples below (and CronTrigger JavaDOC) for clarification.

 

The 'L' character is allowed for the day-of-month and day-of-week fields. This character is short-hand for "last", but it has different meaning in each of the two fields. For example, the value "L" in the day-of-month field means "the last day of the month" - day 31 for January, day 28 for February on non-leap years. If used in the day-of-week field by itself, it simply means "7" or "SAT". But if used in the day-of-week field after another value, it means "the last xxx day of the month" - for example "6L" or "FRIL" both mean "the last friday of the month". When using the 'L' option, it is important not to specify lists, or ranges of values, as you'll get confusing results.

 

The 'W' is used to specify the weekday (Monday-Friday) nearest the given day. As an example, if you were to specify "15W" as the value for the day-of-month field, the meaning is: "the nearest weekday to the 15th of the month".

 

The '#' is used to specify "the nth" XXX weekday of the month. For example, the value of "6#3" or "FRI#3" in the day-of-week field means "the third Friday of the month".

 

Here are a few more examples of expressions and their meanings - you can find even more in the JavaDOC for CronTrigger

 

Example Cron Expressions

CronTrigger Example 1 - an expression to create a trigger that simply fires every 5 minutes

 

"0 0/5 * * * ?"

 

CronTrigger Example 2 - an expression to create a trigger that fires every 5 minutes, at 10 seconds after the minute (i.e. 10:00:10 am, 10:05:10 am, etc.).

 

"10 0/5 * * * ?"

 

CronTrigger Example 3 - an expression to create a trigger that fires at 10:30, 11:30, 12:30, and 13:30, on every Wednesday and Friday.

 

"0 30 10-13 ? * WED,FRI"

 

CronTrigger Example 4 - an expression to create a trigger that fires every half hour between the hours of 8 am and 10 am on the 5th and 20th of every month. Note that the trigger will NOT fire at 10:00 am, just at 8:00, 8:30, 9:00 and 9:30

 

"0 0/30 8-9 5,20 * ?"

 

8.  Quartz对于事件的监听提供了三个不同层次的接口:TriggerListenerJobListenerSchedulerListener

 

 

Quartz和平台的Job的比较

 

任务存储方式

分布式支持

模块化

运行期灵活度

性能

XPlatform Job

单一数据库方式

配置不同的PoolId可以让多台任务服务器各自执行同一数据库中的任务

由于Job设计是基于OFBIZ架构的,因此对于OFBIZ的架构依赖很强,同时由于早期的工厂类设计模式,使得Job比较难剥离

参数主要保存在数据库中,运行中再置入到上下文中,可供Job获取。

规则设置相对来说比较简单,可以支持单一时间激发的任务或者周期性任务。

使用了线程池和任务队列,比较类似于消费者生产者模式,但是在对于任务队列操作的部分,为了支持线程并发访问,都加了synchronized,但是都是方法级别的,粒度很大,加上又是单例模式,这样多线程的线程池实际上效率根本就没有体现出来。

Quartz

数据库方式和内存方式

支持,不过需要每一台集群的服务器的时间同步

开源项目,模块化较好

参数可以通过JobDataMap传递。

规则配置十分灵活,提供了很多方便的Trigger

线程池以及并发问题解决得很好,效率较高。

 


XPlatfrom 新任务机制设计与实现

新任务机制设计

      接口

根据平台现在的整体架构设计框架,首先在平台的统一接口定义包中制定了任务机制的接口类(com.alisoft.xplatform.spec.job)。内部接口类结构图如下:

5 新任务机制接口类图

       具体的类的方法说明都在源码的javadoc中有详细说明,这边只做简单的说明。

Job接口就是最基本的任务接口,用来让业务开发者编写自己的业务逻辑。

Trigger接口就是任务激发条件设置接口。

JobDetail类,是任务详细信息保存的对象类。

Scheduler接口就是放置和自动执行配置好的任务的计划对列。(描述的不一定准确,简单的说就是和他英文意思一样,计划安排)

 

实现

       当前为平台的任务接口实现了Quartz版本的实现,具体的实现类图如下:

同样具体的参数以及类方法说明在javadoc中。

6 任务机制的Quartz实现类图

         QuartzJob是实现了两个接口的一个抽象类,其中平台自己接口的执行方法已经定义好,其实就是调用QuartzJob的执行方法,而Quartz的执行方法是抽象方法,提供给业务人员实现。

       QuartzJobDetailQuartzSchedulerQuartzCronTriggerQuartzSimpleTrigger四个实现类,都采用了代理模式,并且都可以采用注入的方式来使用和测试。

       QuartzScheduler如果不采用注入的方式,那么可以使用iniQuartzScheduler方法来初始化内部的对象。

 

       测试

       测试类都放在(com.alisoft.xplatform.job.quartzImpl包中),不过都在test目录下。

7 单元测试类图

       QuartzSchedulerTest类是QurarzScheduler的单元测试类。EmailSchedulerTest是为业务部门开发作的一个开发Demo。另外分别有两个Job对应于这两个测试类。(关于怎么使用看看这两个类就很明确了,因此不加赘述)。

 

关于Quartz的一点补充

1.       Quartz可以通过配置文件来配置Secheduler的很多参数,包括使用哪一类的JobStore(内存的或者是数据库的),是否支持集群(原来以为不支持,其实老早支持了,不过有限制,集群的机器的时间必须相互同步,否则会出现很多问题)。

2.       Secheduler中使用是需要注意关于jobTrigger的使用。JobTrigger都有nameGroup,在一个Scheduler里面,不允许有同名和并且同组的JobTrigger存在。因此需要注意这些方面,首先addJob(JobDetail)schedulerJobJobDetail,Trigger)会把Job放入到任务执行对列中,那么就需要好好看看是否在做着两个操作的时候,同样的Job已经被放入到了对列中,对于Trigger也是同样SchedulerJob的两个方法都会把Trigger放入到对列中。deleteJob可以把Job从对列中移出,unscheduleJob可以将JobTrigger都移出。

3.       Job什么时候会失效并且被移出数据库存储或者内存存储?Job所关联的Trigger都被执行完毕了,那么对应的JobTrigger都会被删除。