TBSchedule源码学习笔记-任务处理

上回说到每个线程组会创建自己的com.taobao.pamirs.schedule.taskmanager.TBScheduleManager实例来管理线程组,一个JVM中该实例的个数与结合调度机数目分配給JVM的数目一致。TBScheduleManager实例中会计算调度任务的启动时机(与控制台界面设置保持一致)。实际开发一个调度任务按框架要求需要实现com.taobao.pamirs.schedule.IScheduleTaskDealMulticom.taobao.pamirs.schedule.IScheduleTaskDealSingle接口。接着上边的启动过程来。
之前有说道:

这里使用的“开启服务”和“暂停服务”,分别使用了com.taobao.pamirs.schedule.taskmanager.TBScheduleManager类的public void resume(String message) throws Exception方法以及public void pause(String message) throws Exception方法

那么就接着打开看看public void resume(String message) throws Exception 方法,毕竟他是控制了一个线程组的启动。每当到达调度时间(开始执行时间)都会调用该方法。


/**
     * 处在了可执行的时间区间,恢复运行
     * @throws Exception 
     */
    public void resume(String message) throws Exception{
        if (this.isPauseSchedule == true) {
            if(log.isDebugEnabled()){
                log.debug("恢复调度:" + this.currenScheduleServer.getUuid());
            }
            this.isPauseSchedule = false;
            this.pauseMessage = message;
            if (this.taskDealBean != null) {
                if (this.taskTypeInfo.getProcessorType() != null &&
                    this.taskTypeInfo.getProcessorType().equalsIgnoreCase("NOTSLEEP")==true){
                    this.taskTypeInfo.setProcessorType("NOTSLEEP");
                     = new TBScheduleProcessorNotSleep(this,
                            taskDealBean,this.statisticsInfo);
                }else{
                    this.processor = new TBScheduleProcessorSleep(this,
                            taskDealBean,this.statisticsInfo);
                    this.taskTypeInfo.setProcessorType("SLEEP");
                }
            }
            rewriteScheduleInfo();
        }
    }   

大佬们说常用SLEEP模式,所以这里主要看对SLEEP 模式的处理,可见这里判断任务设置如果为sleep模式,则创建一个com.taobao.pamirs.schedule.taskmanager.TBScheduleProcessorSleep<T>实例。这里创建这个实例赋值给了TBScheduleManager的一个全局变量,这个processor被拿去做了些什么,难道是利用他启动了一些线程?毕竟我这设置了好多的线程项,应该是在这里selectTask并对数据分片的吧?所以先去看processor被拿去干什么用了。找了一圈发现以下几个操作

/**
     * 当服务器停止的时候,调用此方法清除所有未处理任务,清除服务器的注册信息。
     * 也可能是控制中心发起的终止指令。
     * 需要注意的是,这个方法必须在当前任务处理完毕后才能执行
     * @throws Exception 
     */
    public void stop(String strategyName) throws Exception{
        //.........//
            this.processor.stopSchedule();
        //.........//
    }
/**
     * 清除内存中所有的已经取得的数据和任务队列,在心态更新失败,或者发现注册中心的调度信息被删除
     */
    public void clearMemoInfo(){
        //.........//
                this.processor.clearAllHasFetchData();
        //.........//
    }
/**
     * 超过运行的运行时间,暂时停止调度
     * @throws Exception 
     */
    public void pause(String message) throws Exception{
        //.........//
                this.processor.stopSchedule();
        //.........//
    }

好吧好像都是一些要求释放资源的操作,并没有开启所以到这里有一种不详的预感“是不是又在构造函数里开线程了?!”,带着疑问打开TBScheduleProcessorSleep类的代码。嗯,看到了关键字 startThread 看来在这个类里启动线程无疑了。

/**
     * 创建一个调度处理器 
     * @param aManager
     * @param aTaskDealBean
     * @param aStatisticsInfo
     * @throws Exception
     */
    public TBScheduleProcessorSleep(TBScheduleManager aManager,
            IScheduleTaskDeal<T> aTaskDealBean, StatisticsInfo aStatisticsInfo) throws Exception {
        //线程组管理器
        this.scheduleManager = aManager;
        this.statisticsInfo = aStatisticsInfo;
        //任务配置信息
        this.taskTypeInfo = this.scheduleManager.getTaskTypeInfo();
        //通过控制台设置的bean
        this.taskDealBean = aTaskDealBean;
         //加载调度任务(实现IScheduleTaskDealMulti或IScheduleTaskDealSingle的区别)
        if (this.taskDealBean instanceof IScheduleTaskDealSingle<?>) {
            if (taskTypeInfo.getExecuteNumber() > 1) {
                taskTypeInfo.setExecuteNumber(1);
            }
            isMutilTask = false;
        } else {
            isMutilTask = true;
        }
        if (taskTypeInfo.getFetchDataNumber() < taskTypeInfo.getThreadNumber() * 10) {
            logger.warn("参数设置不合理,系统性能不佳。【每次从数据库获取的数量fetchnum】 >= 【线程数量threadnum】 *【最少循环次数10】 ");
        }
        //任务管理中"线程数" 配置,本例设置是5 ,所以会启动5个线程。
        for (int i = 0; i < taskTypeInfo.getThreadNumber(); i++) {
            //
            this.startThread(i);
        }
    }

构造方法中确实启动了n个线程,n为任务管理中设置的线程数,所以这里利用线程做了些什么呢?看一下startThread(int index)方法的源码。

private void startThread(int index) {
        Thread thread = new Thread(this);
        threadList.add(thread);
        String threadName = this.scheduleManager.getScheduleServer().getTaskType()+"-" 
                + this.scheduleManager.getCurrentSerialNumber() + "-exe"
                + index;
        thread.setName(threadName);
        thread.start();
    }

发现这里的thread 传入的参数是this,this当然是自己了(com.taobao.pamirs.schedule.taskmanager.TBScheduleProcessorSleep<T>),所以线程会执行这个类的run方法。
在看run的实现代码行之前,先看全局变量,毕竟一个线程组会初始化一个TBScheduleProcessorSleep对象,假如设置了线程数为5,那么会启动5个线程,这5个线程之间必然会共享或同步一些数据,至少要共享selectTask所返回的数据供线程execute。所以看一下这个TBScheduleProcessorSleep类的全局变量。

    /**
    * 哦?这个是什么,mark
*/
    final  LockObject   m_lockObject = new LockObject();
    /**
    * 哦?这个又是什么?mark
*/
    List<Thread> threadList =  new CopyOnWriteArrayList<Thread>();
    /**
     * 任务管理器
     */
    protected TBScheduleManager scheduleManager;
    /**
     * 任务类型
     */
    ScheduleTaskType taskTypeInfo;

    /**
     * 任务处理的接口类
     */
    protected IScheduleTaskDeal<T> taskDealBean;
    /**
     * 当前任务队列的版本号
     */
    protected long taskListVersion = 0;
    final Object lockVersionObject = new Object();
    final Object lockRunningList = new Object();
    /**
    * 哦?这个又又是什么?mark
    */
    protected List<T> taskList = new CopyOnWriteArrayList<T>();

    /**
     * 是否可以批处理
     */
    boolean isMutilTask = false;

    /**
     * 是否已经获得终止调度信号
     */
    boolean isStopSchedule = false;// 用户停止队列调度
    boolean isSleeping = false;
    /**
    * 一些统计数据
*/
    StatisticsInfo statisticsInfo;

除了一些公共配置信息,目前存疑问的就是以下几个,线程间共享这些数据的作用,还有如何同步的?selectTask所查出的数据如何避免集群间重复读取和使用?

final  LockObject   m_lockObject = new LockObject();
List<Thread> threadList =  new CopyOnWriteArrayList<Thread>();
protected List<T> taskList = new CopyOnWriteArrayList<T>();

注意java.util.concurrent.CopyOnWriteArrayList是线程安全的,CopyOnWrite并发容器用于读多写少的并发场景。既然是一个线程安全的并发容器,这样就可以不用关心线程间threadList和taskList的数据一致性啦。
带着疑问打开run方法(吐槽源码格式有点乱)

 @SuppressWarnings({"rawtypes", "unchecked", "static-access"})
    public void run() {
        try {
            //开始执行时间
            long startTime = 0;
            while (true) {
                //这个方法有锁,锁在m_lockObject 实例上。
                this.m_lockObject.addThread();
                Object executeTask;
                while (true) {
                    //如果发起了停止的请求,释放当前线程
                    if (this.isStopSchedule == true) {//停止队列调度
                        this.m_lockObject.realseThread();
                        this.m_lockObject.notifyOtherThread();//通知所有的休眠线程
                        synchronized (this.threadList) {
                            this.threadList.remove(Thread.currentThread());
                            if (this.threadList.size() == 0) {
                                this.scheduleManager.unRegisterScheduleServer();
                            }
                        }
                        return;
                    }

                    //加载调度任务(实现IScheduleTaskDealMulti或IScheduleTaskDealSingle的区别)
                    if (this.isMutilTask == false) {
                        executeTask = this.getScheduleTaskId();
                    } else {
                        executeTask = this.getScheduleTaskIdMulti();
                    }
                    //如果没有数据打破循环
                    if (executeTask == null) {
                        break;
                    }

                    try {//运行相关的程序
                        startTime = scheduleManager.scheduleCenter.getSystemTime();
                        if (this.isMutilTask == false) {
                            if (((IScheduleTaskDealSingle) this.taskDealBean).execute(executeTask, scheduleManager.getScheduleServer().getOwnSign()) == true) {
                                addSuccessNum(1, scheduleManager.scheduleCenter.getSystemTime()
                                                - startTime,
                                        "com.taobao.pamirs.schedule.TBScheduleProcessorSleep.run");
                            } else {
                                addFailNum(1, scheduleManager.scheduleCenter.getSystemTime()
                                                - startTime,
                                        "com.taobao.pamirs.schedule.TBScheduleProcessorSleep.run");
                            }
                        } else {
                            if (((IScheduleTaskDealMulti) this.taskDealBean)
                                    .execute((Object[]) executeTask, scheduleManager.getScheduleServer().getOwnSign()) == true) {
                                addSuccessNum(((Object[]) executeTask).length, scheduleManager.scheduleCenter.getSystemTime()
                                                - startTime,
                                        "com.taobao.pamirs.schedule.TBScheduleProcessorSleep.run");
                            } else {
                                addFailNum(((Object[]) executeTask).length, scheduleManager.scheduleCenter.getSystemTime()
                                                - startTime,
                                        "com.taobao.pamirs.schedule.TBScheduleProcessorSleep.run");
                            }
                        }
                    } catch (Throwable ex) {
                        if (this.isMutilTask == false) {
                            addFailNum(1, scheduleManager.scheduleCenter.getSystemTime() - startTime,
                                    "TBScheduleProcessor.run");
                        } else {
                            addFailNum(((Object[]) executeTask).length, scheduleManager.scheduleCenter.getSystemTime()
                                            - startTime,
                                    "TBScheduleProcessor.run");
                        }
                        logger.warn("Task :" + executeTask + " 处理失败", ex);
                    }
                }
                //当前队列中所有的任务都已经完成了。
                if (logger.isTraceEnabled()) {
                    logger.trace(Thread.currentThread().getName() + ":当前运行线程数量:" + this.m_lockObject.count());
                }
                if (this.m_lockObject.realseThreadButNotLast() == false) {
                    int size = 0;
                    Thread.currentThread().sleep(100);
                    startTime = scheduleManager.scheduleCenter.getSystemTime();
                    // 装载数据
                    size = this.loadScheduleData();
                    if (size > 0) {
                        this.m_lockObject.notifyOtherThread();
                    } else {
                        //判断当没有数据的是否,是否需要退出调度
                        if (this.isStopSchedule == false && this.scheduleManager.isContinueWhenData() == true) {
                            if (logger.isTraceEnabled()) {
                                logger.trace("没有装载到数据,start sleep");
                            }
                            this.isSleeping = true;
                            Thread.currentThread().sleep(this.scheduleManager.getTaskTypeInfo().getSleepTimeNoData());
                            this.isSleeping = false;

                            if (logger.isTraceEnabled()) {
                                logger.trace("Sleep end");
                            }
                        } else {
                            //没有数据,退出调度,唤醒所有沉睡线程
                            this.m_lockObject.notifyOtherThread();
                        }
                    }
                    this.m_lockObject.realseThread();
                } else {// 将当前线程放置到等待队列中。直到有线程装载到了新的任务数据
                    if (logger.isTraceEnabled()) {
                        logger.trace("不是最后一个线程,sleep");
                    }
                    this.m_lockObject.waitCurrentThread();
                }
            }
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }
    }

貌似这个方法还挺长…..,但是也不难发现是通过以下代码段查询任务数据的。

public void run() {
    //......//
    if (this.isMutilTask == false) {
                        executeTask = this.getScheduleTaskId();
                    } else {
                        executeTask = this.getScheduleTaskIdMulti();
                    }
    }
    //......//
}

                    public synchronized Object getScheduleTaskId() {
                                 if (this.taskList.size() > 0)
                                     return this.taskList.remove(0);  // 按正序处理
                                 return null;
                               }

                               public synchronized Object[] getScheduleTaskIdMulti() {
                                   if (this.taskList.size() == 0){
                                     return null;
                                   }
                                   int size = taskList.size() > taskTypeInfo.getExecuteNumber() ? taskTypeInfo.getExecuteNumber()
                                            : taskList.size();

                                   Object[] result = null;
                                   if(size >0){
                                       result =(Object[])Array.newInstance(this.taskList.get(0).getClass(),size);
                                   }
                                   for(int i=0;i<size;i++){
                                     result[i] = this.taskList.remove(0);  // 按正序处理
                                   }
                                   return result;
                               }

发现取数的操作和taskList相关,所以this.taskList 即是通过selectTask 方法查询出的任务数据。即该数据会存放在taskList属性中由线程间共享。这里明摆着只是操作移除taskList中的数据给execute方法使用,所以taskList方法中的数据从哪来?仔细寻找映入眼帘的关键代码在这。

public void run() {
     //......//
    while(true){
         //......//
        while(true){
             //......//

        }
        //......//
        size = this.loadScheduleData();
        //......//
    }
}
protected int loadScheduleData() {
        try {
           //在每次数据处理完毕后休眠固定的时间
            if (this.taskTypeInfo.getSleepTimeInterval() > 0) {
                if(logger.isTraceEnabled()){
                    logger.trace("处理完一批数据后休眠:" + this.taskTypeInfo.getSleepTimeInterval());
                }
                this.isSleeping = true;
                Thread.sleep(taskTypeInfo.getSleepTimeInterval());
                this.isSleeping = false;

                if(logger.isTraceEnabled()){
                    logger.trace("处理完一批数据后休眠后恢复");
                }
            }
            //哦?这个操作点开看满复杂的。所以有必要详细研究下。
            List<TaskItemDefine> taskItems = this.scheduleManager.getCurrentScheduleTaskItemList();
            // 根据队列信息查询需要调度的数据,然后增加到任务列表中
            if (taskItems.size() > 0) {
                List<TaskItemDefine> tmpTaskList= new ArrayList<TaskItemDefine>();
                //拷贝一份数据
                synchronized(){
                    for (TaskItemDefine taskItemDefine : taskItems) {
                        tmpTaskList.add(taskItemDefine);
                    }
                }
                //调用
                List<T> tmpList = this.taskDealBean.selectTasks(
                        taskTypeInfo.getTaskParameter(),
                        scheduleManager.getScheduleServer().getOwnSign(),
                        this.scheduleManager.getTaskItemCount(), tmpTaskList,
                        taskTypeInfo.getFetchDataNumber());
                //更新上一次查询数据的时间。
                scheduleManager.getScheduleServer().setLastFetchDataTime(new Timestamp(scheduleManager.scheduleCenter.getSystemTime()));
                if(tmpList != null){
                    //重要:将任务项配置放到线程组里来。
                   this.taskList.addAll(tmpList);
                }
            } else {
                if(logger.isTraceEnabled()){
                       logger.trace("没有获取到需要处理的数据队列");
                }
            }
            addFetchNum(taskList.size(),"TBScheduleProcessor.loadScheduleData");
            return this.taskList.size();
        } catch (Throwable ex) {
            logger.error("Get tasks error.", ex);
        }
        return 0;
    }

以上代码中已经可以看见数据来源确实是调用了任务实现的selectTask,并把数据刷新到共享变量中。上面List<TaskItemDefine> taskItems = this.scheduleManager.getCurrentScheduleTaskItemList(); 这个疑问涉及到集群间数据同步,一系列zk操作,日后整理。
从实际的使用经验看,taskItemDefine对象应该是按任务项个数来分配的,而selectTask的时候会使用传入的taskItemDefine对象作为查询条件决定数据的查询规模。当然如果不正确实现任务处理接口的化,所有的分片都白扯,例如直接无视taskItemDefine参数。

TaskItem任务项
是对任务进行的分片划分。例如:
1、将一个数据表中所有数据的ID按10取模,就将数据划分成了0、1、2、3、4、5、6、7、8、9供10个任务项。
2、将一个目录下的所有文件按文件名称的首字母(不区分大小写),
就划分成了A、B、C、D、E、F、G、H、I、J、K、L、M、N、O、P、Q、R、S、T、U、V、W、X、Y、Z供26个队列。
3、将一个数据表的数据ID哈希后按1000取模作为最后的HASHCODE,我们就可以将数据按[0,100)、[100,200) 、[200,300)、[300,400) 、
[400,500)、[500,600)、[600,700)、[700,800)、[800,900)、 [900,1000)划分为十个任务项,当然你也可以划分为100个任务项,最多是1000个任务项。
任务项是进行任务分配的最小单位。一个任务项只能由一个ScheduleServer来进行处理。但一个Server可以处理任意数量的任务项。
例如任务被划分为了10个队列,可以只启动一个Server,所有的任务项都有这一个Server来处理;也可以启动两个Server,每个Sever处理5个任务项;
但最多只能启动10个Server,每一个ScheduleServer只处理一个任务项。如果在多,则第11个及之后的Server将不起作用,处于休眠状态。
4、可以为一个任务项自定义一个字符串参数由应用自己解析。例如:”TYPE=A,KIND=1”

把loadScheduleData()和之前的executeTask = this.getScheduleTaskId(); 连起来,继续回到run方法的两层循环

public void run() {
     //......//
    while(true){
         this.m_lockObject.addThread();
         //......//
        while(true){
             //......//
            executeTask = this.getScheduleTaskId();

            //如果没有数据打破循环
            if (executeTask == null) {
                break;
            }
            //否则调用execute方法消耗数据
        }
        //......//
        if (this.m_lockObject.realseThreadButNotLast() == false) {
            int size = 0;
            Thread.currentThread().sleep(100);
            startTime =scheduleManager.scheduleCenter.getSystemTime();
            // 装载数据
            size = this.loadScheduleData();
            if (size > 0) {
                this.m_lockObject.notifyOtherThread();
            } else {
                //判断当没有数据的是否,是否需要退出调度
                if (this.isStopSchedule == false && this.scheduleManager.isContinueWhenData()== true ){                      
                    if(logger.isTraceEnabled()){
                           logger.trace("没有装载到数据,start sleep");
                    }
                    this.isSleeping = true;
                    Thread.currentThread().sleep(this.scheduleManager.getTaskTypeInfo().getSleepTimeNoData());
                    this.isSleeping = false;

                    if(logger.isTraceEnabled()){
                           logger.trace("Sleep end");
                    }
                }else{
                    //没有数据,退出调度,唤醒所有沉睡线程
                    this.m_lockObject.notifyOtherThread();
                }
            }
            this.m_lockObject.realseThread();
        } else {// 将当前线程放置到等待队列中。直到有线程装载到了新的任务数据
            if(logger.isTraceEnabled()){
                   logger.trace("不是最后一个线程,sleep");
            }
            this.m_lockObject.waitCurrentThread();
        }

        //......//
    }
}

再把锁的代码加回来,能发现对锁的操作:
1.在开启线程后每一次循环在LockObject对象中记录一个线程数,并在最后释放线程数,释放后阻塞当前线程,直至释放到最后一个线程,然后去调用selectTask里面加载数据。
2.如果查询到数据就唤醒所有等待的线程。否则判断是否需要暂停调度,如果不需要退出调度会继续唤醒线程。各线程然后会继续执行1的操作。

这样就能保证在一个线程组里只有一个线程可以做selectTask操作。而根据代码上看taskItemDefine貌似是在集群中随机分配的,这样就能保证正确实现任务处理接口的情况下,针对每个任务数据在集群中是平均分配的(因为selectTask方法使用taskItemDefine作为查询参数,对数据进行分片)

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页