我称之为“事件驱动式定时任务调度”

474 阅读4分钟

背景

种种原因,平台要添加一个有着大量定时任务,且均为类似正则解析、查询ES那种IO密集或者CPU密集型的任务的功能。一旦出现解析死循环或者查询死循环,不及时处理掉就会导致CPU飙高或者内存溢出问题。

过程趣事

有人提出,所有编写定时任务的人都在线程中sleep循环执行!

e863a683d3b41270aae82284af67d47d.jpeg

(几十个任务定时执行,加不定期添加的延时任务)我可真是栓Q!

分析需求

开发A:需要间隔一定时间抓去ES数据,例如每隔5分钟抓去最近5分钟的数据

开发B:需要延时执行一些业务处理

项目负责人:好多定时任务,能不能不要一堆定时器不要让大家都自己实现五花八门。让大家专注业务逻辑。还有就是都是很吃资源的操作,万一死循环了,别打死设备。希望能处理异常任务

总结:定时任务延时任务超时任务中断处理有序顺序执行节约线程资源

设计结构

灵感来自Reactor模型,面向事件驱动。职能分离。

架构图.png

实现

可能里面的有些东西大家很眼熟,不要鄙视。问就是致敬。

6c20b530e73caf24884ddb5f39ff159e.jpeg

不废话,直接上源码

外部调用类

public  class DelayTimer {
    //来自netty的神秘力量,时间轮定时器。单线程,并且线程安全。
    static HashedWheelTimer timer = new HashedWheelTimer();
    //支持顺序执行任务的线程池
    static EagerThreadPoolExecutor executor;
    static {
        TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(40);
        executor = new EagerThreadPoolExecutor(
                4,
                Runtime.getRuntime().availableProcessors(),
                1,
                TimeUnit.SECONDS,taskQueue,new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"DelayTimer_"+r.hashCode());
            }},new ThreadPoolExecutor.AbortPolicy());
        taskQueue.setExecutor(executor);
    }
    //首次执行延时执行时间
    private long delay;
    //首次执行延时执行时间单位
    private TimeUnit delayTime;
    //任务:业务逻辑实现位置
    private DelayTimerTask task;

    private DelayTimer(){
    }

    public DelayTimer(DelayTimerTask timerTask, long delay, TimeUnit delayTime){
        this.task = timerTask;
        this.delay = delay;
        this.delayTime = delayTime;
    }

    public void start(){
        timer.newTimeout(task,delay,delayTime);
        System.out.println("添加任务结束");
    }
}

内部调度类

public abstract class DelayTimerTask implements TimerTask {

    // 每隔几秒执行一次
    private final long tick;
    //超时时间
    private final long timeOut;

    private final TimeUnit time;

    public DelayTimerTask(){
        this(0,TimeUnit.MILLISECONDS,3000);
    }


    public DelayTimerTask(long tick, TimeUnit time, long timeOut) {
        this.tick = tick;
        this.time = time;
        this.timeOut = timeOut;
    }

    @Override
    public void run(Timeout timeout) {
        try {
            Future<?> future = DelayTimer.executor.submit(()->{
                try {
                    doWork();
                }catch (InterruptedException e){
                    System.out.println("IO线程中断!");
                }
            });
            timeOutMonitor(timeout,future);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (tick!=0){
                rePut(timeout);
            }
        }

    }

    abstract void doWork() throws InterruptedException;

    private void timeOutMonitor(Timeout timeout,Future future){
        timeout.timer().newTimeout((timeout1)->{
            try {
                future.get(0,time);
                System.out.println("任务正常执行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                System.out.println("任务执行超时");
                /**
                *肯定有兄弟要问了,已经cancel中断了为啥还要kill线程。
                *这里就涉及到知识点:对线程执行cancel操作,
                *只有在线程run方法中有判断中断状态逻辑跳出执行的线程才会停止。
                *cancel操作仅修改了线程的中断标志位。不会立刻停止线程
                **/
                future.cancel(true);
                if (future.isCancelled())
                    killThread((FutureTask<?>) future);
                e.printStackTrace();
            }
        },timeOut,time);
    }

    private void rePut(Timeout timeout){
        if (timeout ==null){
            return;
        }
        Timer timer = timeout.timer();
        if (timeout.isCancelled()){
            return;
        }
        timer.newTimeout(timeout.task(),tick, time);
    }
    
    private void killThread(FutureTask<?> submit) {
        try {
            // 利用反射,强行取出正在运行该任务的线程
            Field runner = submit.getClass().getDeclaredField("runner");
            runner.setAccessible(true);
            Thread execThread = (Thread) runner.get(submit);
            execThread.stop();
            execThread = null;
        } catch (Exception e) {
            e.getMessage();
        }
    }
}

思考

如果线程还在队列里就超过执行时间了会怎么样?

答:线程池队列中的线程任务处于等待状态,当等待状态的线程收到中断标志时将结束任务。且因为业务与定时轮询执行的逻辑分离,不影响下一次执行该任务。

如果强制停止线程导致资源未释放问题怎么办?

1db4d4a7a59f73d5d084890801dc44cb.jpeg

答:成熟的业务兄弟该会自己回收好资源,不成熟的兄弟晚上来我家好好让你成熟一波!

为啥需求中没有谈到顺序执行,你反而加上了这个功能?

答:兄弟让我们回想一下线程池的源码。还记不记得当年大明湖畔的夏雨荷!呸!不是。

当线程池的核心线程与队列满了时,当未达到最大线程池则创建线程并将运行任务。这样以来队列里面的线程岂不是很有可能被超时。然后丢了一堆操作!所以这里使用顺序线程池。只有当线程池达到最大数时,才将任务堆放在队列中保证任务的有序执行。

补充

这里是一个简单的实现,更多细节的设计没有拿出来,本着顺便梳理一下技术点写了一点。如果兄弟们有疑问可以提出来。我好改进一下。