技术开发 频道

如何处理任务调度程序的若干问题?


【IT168 开发管理】对于那些运行规则固定的静态任务(如每过30分钟更新缓存),我们当然可以通过Spring配置文件定义调度规则并在Spring容器启动运行调度。但事情并非总是这么简单,在实际的应用中,往往我们需要根据业务数据动态产生任务。举一个例子:在论坛系统中,当发现某些用户发表一些违反规则的内容时,做为一个惩罚手段,往往需要将该用户锁定一段时间。这种功能就需要通过操作业务功能动态地创建任务来完成:在锁定用户后,经过一段时间通过任务自动将其解锁。随着业务系统的运行,任务源源不断地产生,又源源不断地得到执行。

    在实际的应用系统中,任务的执行时间点往往是非常关键的,不允许执行点发生时间漂移(如电力控制系统)。此外,任务的执行往往是与日历相关的(如在本月15日上午10:00断路某个线程),所以Quartz往往是更适合的选择。 

    如何产生任务 

    在实际应用中,有两个不同的创建动态任务的方式: 

    1)业务流程产生型 
    2)扫描线程产生型。 

    前者表示在进行一个业务时,在业务操作过程中生产任务。而后者由一个扫描线程定时查看业务数据库的任务表,并根据业务数据产生任务。 

    在业务流程中产生 

    某个业务需要使用到任务,如果任务的执行点离业务操作的时间不会太长,这时可以直接在业务操作流程过程中就安排好任务。假设在电力传输管理系统中有一个功能,可以将一条传输线路在某段时间内停止供电。安排停电的时间点离业务的操作时间点一般不会太长,因此可以在用户执行线路停电安排的业务时,马上就向Scheduler中注册两个任务:一个是在某一时间点执行断电的任务,另一个任务是在某一时间点,执行恢复供电的任务。 

    当然,考虑到系统重启的情况,可能需要使用持久化任务,以便系统将已经安排的任务在系统重启后能够恢复。 

    由扫描任务周期性产生 

    但在有些情况下,不适合在业务流程时就产生任务,而应当通过定期扫描的功能,根据业务数据动态产生任务。 

    在论坛系统中,为了清除一些无效的注册帐号,可以定义这样的规则:如果账号注册后,在半年内都没有激活帐号,则将这些帐号删除。对于系统来说,引发这个潜在帐号清除任务的业务是用户注册功能,但是我们不应该在注册帐号后就安排一个清除帐号的任务。首先,在注册用户时,并不知道这个帐号在将来是否会满足清除的条件;其次,任务的执行时间点离当前点太远,现在就安排显然“为时过早”,不这将会产生过长的任务列队,打个不太恰当的比方,再离谱的炒房者也不会在打地基时就排队购房。 

    一般来说,像清除帐号这样的任务对执行时间点的要求并不会太高,所以只要安排一个以天为周期定时执行的任务,将当天满足条件的帐号删除即可。 

    现在,我们来考虑另一种比较极端的需求。假设我们对清除帐号的执行时间点有严格的要求,清除帐号的时间点只能精确地发生在一年之后的那个时间点上(356×24×60×60×1000毫秒之后)。这时,定期周期执行的清除任务就不再适用了。因为如果清除任务运行频率过小,就不能满足精确执行点的要求。如果频率过大,则会对业务数据库产生频繁的访问,对数据库的性能产生不良的影响。此外,在需要访问数据库的情况下,单纯从技术上角度上看,任务运行频率本身是受限的:你很难安排秒级的任务。 

    为了保证严格的执行时间点并尽量减小对数据库的影响,可以采用一个扫描任务,定期查询数据库,并为那些在小段时间后就要执行的潜在任务进行动态的安排,如下图所示:


通过扫描动态产生任务

    T0对应一个定时的任务,它负责周期性地扫描业务表,它查找出在未来的一个扫描周期内的将要执行的任务,创建并安排这些任务。通过这种方式可以带来三个好处: 

    (1)降低对数据库的影响:扫描任务自身的运行频率不必太高,如我们可以使扫描任务30分钟运行一次,将30分钟后的所有需要执行的任务都安排好。这样,扫描任务在30分钟之内只会进行一次数据库访问,对数据库的影响微乎其微。 

    (2)减小调度器中任务列队的长度:由于不是将所有潜在任务在很早以后就进行安排,而仅对一个扫描周期内的任务进行安排,调度器中任务列表的长度可以得到有效的控制。 

    (3)保证任务在精确的时间点执行:由于任务提前得到安排,任务的执行时间点可以得到很好的保证。当然,由于扫描任务本身执行的时间消耗,如果任务的运行时间点离扫描任务的周期点很近,它们的执行时间点的精确性可能受到影响。这时,你可以将安排任务的范围调大一些,比如安排两个周期后的所有任务,以取消这种任务执行点和扫描周期点过近的问题。不过,一般情况下,这种影响是可以接受的。 

    如果系统本身存在许多种需要通过扫描任务动态产生的任务,最好通过一张任务表来维护这些任务,而不要将它们分散在不同的业务表中。假设帐号清除任务的条件信息在用户表中,锁定用户任务的信息在锁定用户表中,商品过期任务的信息在商品表中。这时就需要分别为这些任务定义一个扫描任务,或者在扫描任务内遍历所有的相关的业务表。如果和任务有关的业务表非常多(比如100张),为了动态创建任务,扫描任务就会对数据库产生很大的影响。如果通过一张任务表记录所有潜在的任务,并在业务操作过程中动态维护这个任务表,则仅需要一个扫描任务,且扫描任务只需要查询这张任务表就可以了,这可以有效降低创建动态任务对数据库的影响。使用统一任务表的方式也是有代价的,任务有关的业务模块在业务功能之外还需要额外考虑维护任务表中的数据。比如在用户注册模块,用户注册完成后,就向任务表添加一条清除帐号的任务,在用户激活模块,则需要将清除该用户帐号的记录删除掉。

0
相关文章