要想让计划任务“坑”死你的系统,你一定要这样写

时间:2021-05-11 05:17:40

别拿计划任务不当干粮,小心分分钟干掉你的系统,想看看怎么样狗带最惨,请参考下面的手册

获取大量的数据逐条处理

许多计划任务是用于统计或者批处理的,经常需要遍历某个列表

比如:

//查找所有将要过期的用户,逐个发送邮件

       Iterable<UserEntity> users = userDAO.findExpireUser();
       for (UserEntity user : users) {

      //发送逻辑
                Thread.sleep(5000);
           }
       }

上面的这个例子里,coder可能认为同时过期的用户可能不多,不过你忽略的一个情况,如果系统里的用户是同一天批量创建的,他们中的大多数会在同一天过期,于是就可能出现几万,几十万用户都需要发邮件的壮观场面。。。。上面这个例子里,一次来了两百万。

正确做法:

取数据要分页取,分页处理,处理了立即释放,除非你非常确定不会增长的列表,都不许用listAllgetAll,因为生产环境的行为永远出乎你的意料。

Iterable<UserEntity> users = userDAO.findExpireUserbyPage(i,pagesize);
       for (UserEntity user : users) {

       //发送逻辑
                Thread.sleep(5000);
           }
       }

从团队方面来讲,凡是DAO里面有listAllgetAll,无条件search的都是问题高发区,团队应该限制对这些方法的调用。

 

 

在循环里Sleep一会儿

还是上面的例子,每次发送邮件等待5秒,oh my god,双杀来了。假如有200w用户同时过期,还不算发送逻辑本身的时间,那是一五二十,二五一十,对不起我的手指头不够数,大概是115.7天的时间,关键是在这大概4个月的时间里,users这个列表妥妥的无法被回收,轻松吃掉一两个G内存呢,想想有点小激动呢,我亲爱的OutOfMemory可等不了4个月,分分钟在来的路上呢。

 

正确做法:

在循环里千万不要sleep或者调用太费时的任务,比如上面的逻辑,应该把邮件放到发送列表,然后立刻返回,这些耗时的操作,比如发送邮件这些事情,应该交给另外的进程去完成。术业有专攻,不要越俎代庖

 

Iterable<UserEntity> users = userDAO.findExpireUserbyPage(i,pagesize);
       for (UserEntity user : users) {

       //发送逻辑
                MailBox.put(new ExpireMail(user.mail))
           }
       }

被直觉带节奏

产品:请写一个schedule统计出所有用户每天的登录次数

程序猿的设计:分页获取所有用户,查找该页用户今天的登录日志条数

UserEntity users=GetAllUser(pagepageSize);

For(UserEntity user in users){

List<Log>  logs=getLoginLogForToday(user.username);

System.out.println(“username:”+user.username+” logintimes:”+logs.size);

}

上面的需求、设计、代码看起来简洁有效,但是你肯定就上当了,这个实现是个坑,因为用户不会都登录,设想一个网站有1000万用户,每天登录的用户可能就100w人次,按照上面的实现,一定会循环1000万次,但是如果反过来,从日志入手,就只用循环100w

 

正确做法:

找到循环最少的路径,不要被语言表面的意思所干扰

 

1次循环就能完成?不,让我多来几次

比如每分钟我们要把登录日志取出来,分析下有没有异常情况,比如有没有ip重复登录啦,有没有应用访问量超标啊,有没有人在危险的地区登录啊,然后把危险情况产生预警,通知管理员。于是产生了这样的代码

IpLoginAnalyzer implements Analyzer{

Public void process(){

List<log> logs=getLog(Now,lastCheckTime)

。。。分析逻辑

}

}

AppVisitBarrierAnalyzer implements Analyzer{

Public void process(){

List<log> logs=getLog(Now,lastCheckTime)

。。。分析逻辑

}

}

DangerLocationAnalyzer implements Analyzer{

Public void process(){

List<log> logs=getLog(Now,lastCheckTime)

。。。分析逻辑

}

}

。。。。。

Foranalyzer in AnalyerList{

Analyzer.process()

}

 

乍一看很顺眼,每个analyzer干自己的事情,但是具体执行起来,会多次查询和循环日志,考虑下假如有100个分析器,就会遍历日志100次,加上执行process本身需要时间,非常可能造成这分钟的还没有执行完,下次的分析又开始了,一波波最终叠加成海啸,让应用歇菜。

 

正确做法:

日志分析,如果至少要遍历一遍,那么最好也就只遍历一遍,写个遍历器,负责逐条遍历,把每条交给各个处理器处理,一次遍历解决所有问题。

List<log> logs=getLog(Now,lastCheckTime)

For(log in logs){

Foranalyzer in AnalyerList{

if(analyzer.canProcess(log))

analyzer.process(log)

}

}

 

可能你会问,如果每个处理器处理的日志类型不同怎么办呢,比如有的处理器处理登录,有的处理创建用户,有的要处理所有日志,其实只要有处理所有日志的情况,其他的处理就可以一起做了,反正都要循环一次,就不要再浪费cpu循环多次了,对开发团队来说,日志处理应该有统一的框架,有统一的人审核,处理类似数据,执行频率相同的要归并,否则各自为政就会出现上面的情况。