性能文章>实战生产问题:真的别再使用TimerTask了!>

实战生产问题:真的别再使用TimerTask了!原创

1年前
7348212

问题背景

说说大概的场景吧。由于系统里面增加了权限的限制,不同用户拥有不同的数据权限。当前的方案是查询用户uid和对应的数据列表存放在本地内存里,并且需要定时捞取对应的关系数据存储在本地缓存LocalCacheMap中,key为uid,value为List。

接着其它接口则根据LocalCacheMap获取对应的数据权限进行相关的判断。

我们准备上线时,发现功能不正常,本来应该是能正常过滤权限数据的时候却没有生效?

但是其它同学在测试环境验证基本没问题的!所以就有很大的疑问了。

我都不相信这功能真的在测试环境上OK?对测试结果表示怀疑,所以我跟组内同学在测试环境验证一波,确实是没问题的。那么继续查看检验代码逻辑,查看是否有哪里不严谨,可能存在隐藏的bug。

在看代码的过程中也发现了日志打的太少了,重点的地方都不打下,起码还能知道从哪里跟踪,所以对于这点暂且不说,我也不想说重新补打日志,然后重新打包,发版排查。

为了一探究竟,此时必然还是需要使用强大的工具Arthas,之前就已经解决过我的生产问题。

排查解决问题

排查一:

此时借助Arthas,尝试调用获取缓存数据:

image.png

调用实例方法,获取到的结果为null,说明缓存中没有我要的数据,这就很奇怪了。然后我去验证拉去权限数据的接口,手动去调http接口,此时在服务器上使用curl手动调用接口,但是接口返回的443,所以我怀疑是不是调用的接口问题造成缓存数据为空?此时找了运维同学帮忙确认下是不是网络的问题,最后排查结果确实是网络没有放行,于是等待网络问题解决之后继续上线流程。

但是,网络问题虽然解决了,再次验证功能还是跟刚才一样,why?继续排查…

排查二:

此时,再次借助Arthas,调用了refresh方法,来手动触发缓存刷新操作。

image.png

执行刷新操作,返回null,这是正常的,因为refresh返回void,此次刷新耗时在1132ms。

接着,再调前面获取缓存数据的接口:

image.png

数据出来了!!!说明权限数据接口是正常的,拉取数据是正常,接着我们就在功能上进行验证,确实都正常了。

那么问题就出在刷新方法的调用上,是否没触发或者调用者已经没调用等等情况。查看了代码之后才发现发现是使用TimerTask来定时执行任务,定时更新缓存数据。

Timer timer = new Timer(false);
timer.schedule(new RefreshTask(), 10*1000L, 30*1000L);
private class RefreshTask extends TimerTask {
    @Override
    public void run() {
        reflesh();
    }
}
reflesh() {
    // 拉去权限数据
    // 更新缓存数据
}

就是这样来维护缓存数据。那么为什么它没执行?按道理启动之后都会每30秒执行一次才对,但是为什么没有呢?

罪魁祸首

想必很多人知道TimerTask会存在一个问题,就是定时调度执行的方法如果没有捕获处理异常的话,那么它就会终止,基本上不会再运行了。所以应该是这个问题造成的。

那么,应该要找到它抛异常的地方才能验证我们这个问题。所以从日志里面找,最终发现:

image.png

确实是在启动不久(10秒左右)的地方,抛了NPE异常,所以这也验证了我们的问题,罪魁祸首就是它了。

解决

ScheduledExecutorService executorService = 
        new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern(
                            "schedule-task-%d").build());;
executorService.scheduleWithFixedDelay(
           new RefreshTask() , 10*1000L, 30*1000L, TimeUnit.MILLISECONDS);

使用ScheduledThreadPoolExecutor来定时调度刷新缓存。比TimerTask的好处就是出现异常也会继续重新定时调度。

总结

这种问题,虽说不是特别难的问题,但经验不是很丰富的开发人员却在日常中常会犯的,也会影响正常特性上线,造成发版阻碍,影响功能上线。

针对此次“事件”,总结一下,以免下次再犯:

  1. 日志,日志,要打印,要打印好。

  2. 尽量别用TimerTask,别踩坑,如要用一定要捕获处理好异常,一般建议使用ScheduledExecutorService代替。(阿里规约)

  3. 要学会使用Arthas,在紧急“救火”中非常有用!

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr