前言
记得我在之前某一篇博文里讲到过一个案例:使用java的守护线程来模拟redis缓存的过期时间设定。
然后线程这个也是老生常谈的一个问题,守护线程也不陌生,在Jvm里就有大量的守护线程的使用。然后本文主要记录一下我在工作中使用守护线程完成业务逻辑,忽略了一点从而导致一个线上问题,进而记录排查这个过程:
基础知识:【小家java】Java里的进程、线程、协程 、Thread、守护线程、join线程的总结
业务背景
业务背景为一个供需关系模型:老师和学生
模型相当于:老师和学生都在指定一段时间里向系统签到,然后我们应用会定期的(比如20s一次)的把学生和老师撮合在一起,然后经过我们的黑匣子算法进行供需匹配,进而组成一个班级。
然后本场景的重点就在于这个每隔20s撮合一次。针对这个模型,我一下子想到的是两种方案:
- 方案一:被动式。也就是借助调度系统,设置一个定时器被动的、盲目的每20s就去老师、学生签到队列里看一次。有就都拿出来然后经过算法组班即可。
显然,该方法有它的优势,那就是简单粗暴的处理得非常的简单,也非常容易理解。但是弊端也很明显:主动扫描我们发现绝大部分请求可能都是浪费掉了的,1%利用上的可能都不到,会消耗掉系统大量的资源。比如我们只能全天扫,20s扫一次,说实话是个非常非常大的量了。
倘若你还记录一些请求日志之类的,我相信它会对你跟踪对应系统别的功能的日志信息带来极其的不便。而且这种频率的日志输出,也给网络流量、以及磁盘IO带来了不小的负担~
- 方案二:主动式。顾名思义,就是我有学生了主动告知你,然后你来个倒计时20s就去触发对应的组班动作即可。倘若在这20s期间有其余同学进来,那也会被一起成班嘛,这就是我们最希望的效果。
这种方式的优势:刚好就是弥补了上一种方式的不足,不用频繁的去耗费系统资源了,处理起来也更加的优雅。但是,它的实现方式就会稍微麻烦点,有两个最大的痛点吧。
第一:就是第一个人临界情况,需要考虑并发性。
第二:倒计时怎么倒?然后倒计时到了又怎么样触发对应的动作呢?
可能有人会想到,可以借助MQ中间件的延迟队列功能来做。
答:我也想过,但奈何那时候我们的技术栈还是ActiveMQ,显然是不支持的。另外说一句:即使MQ有延迟队列这样的功能,但一般MQ的官方都说了,如果是些重要的、敏感的数据,不推荐使用。
最终,我采用的是JDK的延迟队列+守护线程的方式去实现
实现方式
废话少说,show me the code(伪代码如下):
private static final DelayQueue<RoomDelayer> CATEGORY_QUEUE = new DelayQueue<>(); /** * 给处理器赋值 启动守护线程 */ @PostConstruct public void postConstruct() { setOperLong = redisTemplate.opsForSet(); valueOperLong = redisTemplate.opsForValue(); hashOperLongInt = redisTemplate.opsForHash(); //启用一个守护线程 去监听延迟队列执行任务 Thread t = new Thread(() -> { while (true) { RoomDelayer roomDelayer = null; try { roomDelayer = CATEGORY_QUEUE.take(); } catch (Exception e) { log.error("take出错了", e); } if (roomDelayer == null) { continue; } //获取到延迟的任务后 交给service去执行任务 Integer configType = roomDelayer.getConfigType(); Long courseId = roomDelayer.getCourseId(); Long startTime = roomDelayer.getStartTime(); Long lessonId = roomDelayer.getLessonId(); String key = genRoomCategoryStuKey(configType, courseId, startTime, lessonId); try { log.info("延迟队列[" + key + "]执行start..."); pullQueueDataPersistRoom(configType, courseId, startTime, lessonId); log.info("延迟队列[" + key + "]执行end..."); } catch (Exception e) { //处理一下,万一获取锁等待超时了,导致队列丢失的问题 if (e instanceof RedisLockException) { //若是获取锁超时的异常 那就放进去 等待下次继续尝试 long execTimeAt = addEleIntoDelayQueue(configType, courseId, startTime, lessonId); log.warn("因为获取执行锁超时,补贴一个延迟队列元素,信息如下: {} {} {} {} 执行时间为:{}", configType, courseId, startTime, lessonId, DateUtils.date2Str(new Date(execTimeAt))); } else { log.info("延迟队列持久化的时候报错,key为 " + key, e); } } try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { log.error("sleep出错了", e); } } }); t.setName("daemon thread for CATEGORY_QUEUE"); t.setDaemon(true); t.start(); }
咋一看,这个应该是没有问题的。但是昨晚,我们的reids
出问题了,导致我经常获取不到连接,有不少的报错。
然后到第二天,我在监控日志,发现守护线程竟然没有动静了,所以猜测是死翘翘了。
问题查找
找同事协助用jmx
连上这台机器看看:如下图
再看另外一台(服务是好使的,守护线程正常):
发现这台机器的守护线程很正常的运行着。这符合我表面上看到的现象,那到底是怎么回事呢?导致线程就这么退出了?
刚开始怀疑是不是可能是内存溢出、或者内存漏洞、或者线程其它原因。所以我们尝试这看了dump日志,把日志文件down下来,本地分析。
结果为:没有找到任何名称为此的线程相关信息~
定位到原因
最后,我想。守护线程再怎么说也是个线程啊,如果执行过程中抛出异常,那就会退出线程了。然后我分析了日志,根据线程名去找该下城下的报错:
grep "daemon thread for CATEGORY_QUEUE] ERROR" /opt/g
果然找到了:
然后跟踪代码,本以为自己都给try住了,但是还是有一失误啊。
解决方案
该解决方案也是以后各位使用守护线程一定一定要注意必须做的一个方案:最外层用try包裹住,防止里面一切可能出现但又忘记了的运行时异常发生,从而终止了守护线程。那就影响非常之大了
伪代码如下:
//启用一个守护线程 去监听延迟队列执行任务 Thread t = new Thread(() -> { while (true) { try { // 在这里面书写你的最核心逻辑。。。。里面抛出任何异常,我都不怕了 } catch (Exception e) { log.error("daemon thread for CATEGORY_QUEUE 执行失败", e); } } });
总结
没什么好总结的,一句话:当你使用守护线程去处理逻辑,而必须确保此守护线程不能退出时,请无比使用try,不允许任何异常抛出~来终止守护线程即可