对项目中所有定时任务进行错误日志收集

一、需求描述

工作中遇到一个任务,对定时任务里开启的线程池中执行的异步任务进行错误日志监控,保证定时任务能正常运行,且出先异常时,运维人员能快速通过界面找到出现问题的任务。

下面是定时任务的部分伪代码(省略具体业务):

class Test2 {
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));

    @Scheduled(cron = "0 0 6 * * ?")
    public void testScheuled() {
        //模拟业务:获取一个List
        List<String> strArr = new ArrayList<>();
        for (String str : strArr) {
            //启动异步任务
            threadPool.execute(() -> {
                //处理主要业务
                // ...
            });
        }
    }

	//.....省略其他定时任务.....
}

上述代码,主要列出了逻辑流程,具体业务代码省略,同样这个类的定时任务也有很多,这里只列出一个,便于演示。

代码解析:一个定时任务,有一个for循环,循环多次向线程池提交异步任务。

需求

逻辑:这里我们要做的事情就是对for循环里所提交的任务进行监控,若任务执行过程中出错了就需要记录下该异常,将异常信息记录(一个定时任务只需记录其中一个出错的线程异常即可),并且将该定时任务执行状态改为失败;若所有任务都执行成功了,那就无需记录任何信息,但要将该定时任务的执行状态改为成功。

运维页面:需要展示的信息有定时任务名称、定时任务code(唯一标识)、错误原因(可为空)、执行状态(成功或失败)、收集时间

二、代码实现(不考虑代码的美观)

针对上述需求,我们初步做了功能实现,实现起来也很简单:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));

@Scheduled(cron = "0 0 6 * * ?")
    public void testScheuled() {
        Map<String, String> errMap = new ConcurrentHashMap<>(2);

        //模拟业务:获取一个List
        List<String> strArr = new ArrayList<>();
        for (String str : strArr) {
            //启动异步任务
            threadPool.execute(() -> {
                try {
                    //处理主要业务
                    // ...
                } catch (Exception e) {
                    if (errMap.get(ERRMSG) == null) {
                        errMap.put(ERRMSG, ERRMSG);
                        //设置监控结果 
                        setCheckResult("自检码", "监控结果为False", e.getMessage(), 1, 1);
                    }
                } finally {
                    if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
                        errMap.put(SUCCESS, SUCCESS);
                        //设置监控结果
                        setCheckResult("自检码", "监控结果True", "", 1, 1);
                    }
                }
            });
        }
    }
    
    private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
       //....
       //写入数据库
	   //.....
    }

代码解释:对比原代码和新代码的变化,显然就是在线程的run方法内加了try…catch…finally…并在其中做了相关的错误信息记录,具体的错误信息处理已经封装成一个方法了,这里不重点介绍其中实现,也就是记录数据库,再查寻展示于界面。(错误信息处理,根据个人需求,不同场景需求下,实现方式也不一样,因此不做过多介绍)

Map<String, String> errMap = new ConcurrentHashMap<>(2);//用于判断是否出现过异常的标识
简单介绍一下,为什么要用map,而不是用一个局部变量做错误标志?
不用普通变量,因为使用了lambada表达式,所以是无法访问到的。
ConcurrentHashMap,可以被lambada表达式内所访问,且线程安全。

具体为何Map可以被访问而普通变量却不能访问,本人也还没有时间深究。

至此,代码写完了,功能也完美实现了,也完全预防了以下问题:

  1. 全部线程都出错时,出现多次向数据库写入错误信息的情况,通过map控制后,捕捉到一个线程出错后就仅写入一次,其他忽略。
  2. 全部成功时,出现多次向数据库写入成功信息的情况,同样的通过map控制后,也只写入了一次。
  3. 某次执行出现失败并记录了数据库为F,修复后又如何更新数据库为S?要知道,定时任务每次重新执行,map都是重新创建的,因此这个问题可以忽略。

任务完成了,高高兴兴去交任务,没想到被打回来了,理由:实现代码繁琐,代码侵略性大,重复代码多,项目定时任务较多,每个定时任务加一遍,累死个人。

三、代码优化(装饰器模式+模板方法模式)

看着屏幕上的那么多重复代码,被打回来也是正常,我还是太菜了…hahahahahah

参考了几个设计模式,我又对我的代码进行了下面的改正:
核心业务代码如下:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));


    @Scheduled(cron = "0 0 6 * * ?")
    public void testScheuled() {
        Map<String, String> errMap = new ConcurrentHashMap<>(2);
        //模拟业务:获取一个List
        List<String> strArr = new ArrayList<>();
        for (String str : strArr) {
            //启动异步任务
            threadPool.execute((new ExecutorTask("自检码", "监控结果True", "", 1, 1,errMap){
                @Override
                public void task() {
                    //处理主要业务
                    // ...
                }
            }));
        }
    }

非核心业务代码:


abstract class ExecutorTask implements Runnable{
    private String code;
    private int dateValue;
    private int field;
    private Map<String, String> errMap;
    public ExecutorTask(String code,int dateValue,int field,Map<String, String> errMap){
        this.code = code;
        this.dateValue = dateValue;
        this.field = field;
        this.errMap = errMap;
    }

    public abstract void task();
    public void run(){
        try {
            task();
        } catch (Exception e) {
            if (errMap.get(ERRMSG) == null) {
                errMap.put(ERRMSG, ERRMSG);
                setCheckResult(code, EnumType.CheckResult.F, e.getMessage(), dateValue, field);
            }
        } finally {
            if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
                errMap.put(SUCCESS, SUCCESS);
                setCheckResult(code, EnumType.CheckResult.S, SPACE_STRING, dateValue, field);
            }
        }
    }

    private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
       //....
       //写入数据库
	   //.....
    }
}

经过一顿操作,成功的把非核心业务代码提取处理,对原定时任务的代码入侵减少了,实现起来没那么繁琐了,再加入定时任务时,也只需要重点关注核心逻辑即可。显然成功抽离了大部分非核心代码。

简单解释一下ExecutorTask 类
ExecutorTask 实现了runable接口,并对其中的run方法做了实现,而run方法中对task方法进行catch异常操作,然后将task交由子类实现,而实现的代码便是咱们的核心业务代码。
这其中借鉴了23种设计模式种的几种设计模式,本人也有些分不太清,感觉和适配器模式、装饰器模式、模板方法模式都挺像的,这些大家都可以去了解下。

至此,本人又兴高采烈的去交任务了,然而又打回来了,理由如下:

  1. 核心代码里的这行Map<String, String> errMap = new
    ConcurrentHashMap<>(2);别人看了会怎么想?这是什么东西,为啥要出现在我的代码里?
  2. 还有这行 threadPool.execute((new ExecutorTask(“自检码”, “监控结果True”, “”, 1,
    1,errMap),凭啥保证别人会乖乖给你new一个ExecutorTask,又乖乖的传入后面几个鬼参数,而且现在都流行lambada表达式,这个内部类又是什么鬼?

这些问题,我也想过,本以为可以蒙混过去,也是想试探一下组长的态度,看来是我想多了…hahahahahha…针对这几个问题,我回到座位又陷入了漫长漫长的沉思…

四、代码再优化(装饰器模式+模板方法模式+适配器)

看着这几个问题,确实都是个问题,想了一下,也许可以对线程池做一下封装,于是又得到了下面的代码:

核心业务代码:

MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));

    @Scheduled(cron = "0 0 6 * * ?")
    public void testScheuled() {
        //开启监控,这三个参数是记录错误信息需要的,根据不同的定时任务,传入相应的值
        threadPool.beginMonitor("自检码",1, 1);

        //模拟业务:获取一个List
        List<String> strArr = new ArrayList<>();
        for (String str : strArr) {
            //启动异步任务
            threadPool.execute(()->{
                //...
                //核心业务逻辑
                //...
            });
        }
    }

非核心业务代码:

class MyThreadPoolExecutor{
    private ThreadPoolExecutor poolExecutor;
    private String code;
    private int dateValue;
    private int unit;
    private boolean monitor = false;
    private Map<String, String> errMap;
    public MyThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue) {
        this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory());
    }

    public void beginMonitor(String code, int dateValue, int unit){
        this.code = code;
        this.dateValue = dateValue;
        this.unit = unit;
        this.monitor = true;
        this.errMap = new ConcurrentHashMap<>(2);
    }

    public void execute(Runnable run){
        if (monitor){
            ExecutorTask executorTask = new ExecutorTask(code,dateValue,unit,errMap,run);
            poolExecutor.execute(executorTask);
        }else {
            poolExecutor.execute(run);
        }
    }
}

class ExecutorTask implements Runnable {
    private String code;
    private int dateValue;
    private int unit;

    private Runnable task;
    private Map<String, String> errMap;

    public ExecutorTask(String code, int dateValue, int unit, Map<String, String> errMap,Runnable task) {
        this.code = code;
        this.dateValue = dateValue;
        this.unit = unit;

        this.task = task;
        this.errMap = errMap;
    }

    public void run() {
        try {
            task.run();
        } catch (Exception e) {
            if (errMap.get(ERRMSG) == null) {
                errMap.put(ERRMSG, ERRMSG);
                setCheckResult(code, EnumType.CheckResult.F, e.getMessage(), dateValue, unit);
            }
        } finally {
            if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
                errMap.put(SUCCESS, SUCCESS);
                setCheckResult(code, EnumType.CheckResult.S, SPACE_STRING, dateValue, unit);
            }
        }
    }

    private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
        //....
        //写入数据库
        //.....
    }
}

由上面的代码可以看到,主要核心的业务代码已经清净了不少,与原代码相比,几乎没什么区别,已经是完美优化了。
与原代码的主要区别:对线程池做了一层封装,并通过对execute的功能做了增强。

看着这份代码,心想着,改了一遍又一遍,推翻推翻再推翻,终于干净,于是我又想赶快把这份代码交上去,看他怎么说…
刚要挪开脚步,突然想起前两次都被推翻,这次要谨慎点,再仔细检查一遍看看有啥bug没~~~~~~~

果不其然,这次我自己发现了问题,唉~~~

在自定义的线程池里,那几个成员变量是公共资源,每个定时任务就是一条线程,每个都会调用一次beginMonitor方法,这就导致了那几个成员变量出现线程安全问题。

难道无解了吗?如何保证那几个成员变量的线程安全呢?

五、ThreadLocal解决线程安全问题

晚上,心思重重的我再次打开电脑,盯着这份代码思考,偶尔百度下~~~无解。
关电脑睡觉,躺床上依旧思考…难道又回归到原点了吗?不对,等等等等…ThreadLocal…

立马爬起来,才凌晨而已,还挺早哦…不急着睡,经过三两下捣腾,搞定。
核心业务代码不变:

    MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));

    @Scheduled(cron = "0 0 6 * * ?")
    public void testScheuled() {
        //开启监控
        threadPool.beginMonitor("自检码",1, 1);

        //模拟业务:获取一个List
        List<String> strArr = new ArrayList<>();
        for (String str : strArr) {
            //启动异步任务
            threadPool.execute(()->{
                //...
                //核心业务逻辑
                //...
            });
        }
    }

对线程池的成员变量改为ThreadLocal线程变量:


class MyThreadPoolExecutor{
    private ThreadPoolExecutor poolExecutor;
    ThreadLocal<MonitorInfo> monitorInfo = new ThreadLocal();;
    ThreadLocal<Map> errMap = new ThreadLocal();

    public MyThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue) {
        this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory());
    }

    public void beginMonitor(MonitorInfo info){
        monitorInfo.set(info);
        errMap.set(new ConcurrentHashMap<>(2));
    }

    public void execute(Runnable run){
        if (monitorInfo.get() != null){
            ExecutorTask executorTask = new ExecutorTask(monitorInfo.get(),errMap.get(),run);
            poolExecutor.execute(executorTask);
        }else {
            poolExecutor.execute(run);
        }
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class MonitorInfo{
    private String code;
    private int dateValue;
    private int unit;
}

class ExecutorTask implements Runnable {
    private MonitorInfo info;
    private Runnable task;
    private Map<String, String> errMap;

    public ExecutorTask(MonitorInfo info, Map<String, String> errMap,Runnable task) {
        this.info = info;
        this.task = task;
        this.errMap = errMap;
    }

    public void run() {
        try {
            task.run();
        } catch (Exception e) {
            if (errMap.get(ERRMSG) == null) {
                errMap.put(ERRMSG, ERRMSG);
                setCheckResult(EnumType.CheckResult.F, e.getMessage(), info);
            }
        } finally {
            if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
                errMap.put(SUCCESS, SUCCESS);
                setCheckResult(EnumType.CheckResult.S, SPACE_STRING, info);
            }
        }
    }

    private void setCheckResult(String checkResult, String checkErr,MonitorInfo info) {
        //....
        //写入数据库
        //.....
    }
}

ThreadLocal为线程变量,是线程安全的,每个定时任务运行时,都是一个线程,因此存入于ThreadLocal内的值也都是线程安全的,避免了互相干扰。
(但是,再我以为问题解决了后,突然想到了一个严重的问题,每个子任务都是一个新的线程,所以子任务线程,是无法共享monitorInfo 里面值的。看样子这个问题暂时无解了)

这个问题暂时放下了,时光匆匆,我离职了,又入职了,又离职了,又入职了…
·····
····
··

六、一年后

两年后的今天,翻到了这个遗留问题,博客也一直是私密的,想着再回顾一下当时的场景,看以我如今的水平能不能解决,抱着尝试的心态就干了起来。
·······
····
···
··
nice!!解决了。

定时任务:

@Component
public class ScheduledTask4 {
    MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(6,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(200));

    @Scheduled(fixedDelay= 10000)
    public void scheduledTask2() throws InterruptedException {
        MonitorInfo monitor = threadPool.getMonitor("自检码1", 1, 1);
        for (int i = 0;i<10;i++) {
            threadPool.execute(monitor,() -> {
//                System.out.println(Thread.currentThread().getName()+"**********自检码1********"+ LocalDateTime.now());
            });
        }
    }

    @Scheduled(fixedDelay= 10000)
    public void scheduledTask3() throws InterruptedException {
        MonitorInfo monitor = threadPool.getMonitor("自检码2", 1, 1);
        for (int i = 0;i<10;i++) {
            threadPool.execute(monitor,() -> {
//                System.out.println(Thread.currentThread().getName()+"**********自检码2********"+ LocalDateTime.now());
            });
        }
    }

    @Scheduled(fixedDelay= 10000)
    public void scheduledTask4() throws InterruptedException {
        MonitorInfo monitor = threadPool.getMonitor("自检码3", 1, 1);
        for (int i = 0;i<10;i++) {
            threadPool.execute(monitor,() -> {
//                System.out.println(Thread.currentThread().getName()+"**********自检码2********"+ LocalDateTime.now());
                    int j = 10/0;
            });
        }
    }
}

自定义线程池:

class MyThreadPoolExecutor{
    private ThreadPoolExecutor poolExecutor;

    public MyThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue) {
        this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory());
    }

    public MonitorInfo getMonitor(String name, int a, int b){
        return new MonitorInfo(name, a, b,new ConcurrentHashMap<>());
    }

    public void execute(MonitorInfo monitor,Runnable run){
        if (monitor != null){
            ExecutorTask executorTask = new ExecutorTask(monitor,run);
            poolExecutor.execute(executorTask);
        }else {
            poolExecutor.execute(run);
        }
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class MonitorInfo{
    private String code;
    private int dateValue;
    private int unit;
    private Map<String,String> errMap;
}

class ExecutorTask implements Runnable {
    private Runnable task;
    private MonitorInfo monitor;


    public ExecutorTask(MonitorInfo monitor,Runnable task) {
        this.task = task;
        this.monitor = monitor;
    }

    public void run() {
        Map<String, String> errMap = null;
        try {
            errMap = monitor.getErrMap();
            task.run();
            System.out.println("线程"+Thread.currentThread().getName()+"处理了"+monitor.getCode());
        } catch (Exception e) {
            if (errMap.get(ERRMSG) == null) {
                errMap.put(ERRMSG, ERRMSG);
//                setCheckResult(EnumType.CheckResult.F, e.getMessage(), info);
                System.out.println("线程"+Thread.currentThread().getName()+"处理了"+monitor.getCode()+"出现异常。");
            }
        } finally {
            if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
                errMap.put(SUCCESS, SUCCESS);
//                setCheckResult(EnumType.CheckResult.S, SPACE_STRING, info);
            }
        }
    }

    private void setCheckResult(String checkResult, String checkErr,MonitorInfo info) {
        //....
        //写入数据库
        //.....
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值