微信公众号:java排坑日记,当你厌烦了长篇大论的面经,没时间系统的读书,可以利用茶余饭后地铁上马桶上几分钟的碎片时间来提升一下自己,坚持下来会有惊喜!
最近记录了一些java中常踩的坑、设计思路和小知识点,大家可以看看
详细记录一次接入xxl-job的踩坑路径
30s快速解决循环依赖
idea中一个小小的操作竟能解决如此多的问题
docker中的服务接入xxljob需要注意的一点
关于一次fullgc的告警分析
mysql中的int类型竟变成了它?
jpa中的字段总是自己莫名更新?
获取不到类上的注解?空指针?
学会这招,再也不怕依赖冲突!
redis的热点key还能这么处理?
领导让我设计一个任务系统
当服务重启时,大部分人没考虑这点
参数还能这么优雅校验?
文件上传报错,全局异常处理!
常见的点赞功能如何实现,如何防止刷赞
一次fullGc
整个思路如下
- 起因是这样的,最近收到的告警信息中,发现fullgc出现的频率突然高起来了。
- 然后想到,jvm配置近期无改动,也就是说服务的堆栈大小是正常的。
- 然后查看了一下最近的业务使用情况,发现qps也没有明显的抖动。
- 所以初步怀疑是代码问题。
- 然后仔细看了下cat中的内存使用柱状图,发现一个奇怪的特征,两台机器,基本都是一台机器在频繁做fullgc,另一台则不会,而且出现fullgc之前的内存使用情况是老年代呈现梯度递增的,也就是说在某一个时间点会突然暴增4g直接进入老年代的大对象。
- 经过我们的初步猜测,应该是某个时间点内存突然加载了很大的对象,其实这时候已经大概猜到可能是定时任务了。
- 不过还是去看了下gc日志,发现有一个对象的内存占用率是显著增加的,也就是所谓的大对象。
- 根据这个对象,找到相应代码,发现确实是定时任务中的一个对象,会定时从库中查询,库中的数据量其实还好,但是数据中有大字段,所以开发同学没注意,load了整个对象,然后导致了频繁的fullgc。
- 解决方法倒是好说,根据业务需要,对查询的字段做精简即可。
产生的想法
但是这次定时任务导致的fullgc引发了我的思考,其实在当今分布式应用的大环境下,我们还用之前老一套的定时任务去处理,确实会带来很多不便,比如这次的问题,如果采用分布式任务调度框架的话,是不是就能避免或者更早的发现这样的问题了?
所以决定调研一下定时任务以及分布式调度框架相关的东西,选择一个适合我们业务的框架。
无框架的定时任务
以下按照技术的发展顺序,依次介绍
定时器类Timer
早期没有任何框架的时候,是使用JDK中的Timer机制和多线程机制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执⾏某⼀段程序。
import java.util.Timer;
import java.util.TimerTask;
import java.util.Date;
import java.text.SimpleDateFormat;
class MyTask extends TimerTask
{
@Override
public void run()
{
Date date =new Date();
SimpleDateFormat ft=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("当前时间:"+ft.format(date));
}
}
class TimerDemo
{
public static void main(String[] args)
{
Timer t=new Timer();
MyTask task=new MyTask();
t.scheduleAtFixedRate(task,0, 1000);
}
}
其实就是在主线程中开启一个子线程,按时的执行某个任务。
他也有缺点
- Timer类内部是维护了一个TaskQueue.TimerTask[]数组,数组中放了要执行的TimerTask,然后Timer执行器按照顺序去依次唤醒,这就导致了,如果两个任务的触发时间一致,那么必然有一个任务会被另一个任务耽搁。
- 还是基于上述的原理,在Timer是单线程的前提下,如果一个任务出了异常,那么别的任务也会被中断
比如如下代码
package com.example.demo.test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
/**
* description
* * @author luhui
* @date 2022/8/17
*/
public class TimerTest {
public static void main(String[] args) {
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("task1:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
throw new RuntimeException();
}
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task2:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
throw new RuntimeException();
}
}
};
Timer timer = new Timer();
timer.schedule(task1, 0, 1000);
timer.schedule(task2, 0, 1000);
}
}
task1与task2的调度间隔为1s,但是因为task1与task2的执行时间都超过了1s,所以预期的时间间隔应该是
- task1为2s
- task2为3s
但实际输出如下
task1:2022-08-17 17:06:59
task2:2022-08-17 17:07:01
task1:2022-08-17 17:07:04
task2:2022-08-17 17:07:06
task1:2022-08-17 17:07:09
task2:2022-08-17 17:07:11
输出间隔都为5s,很显然task1和task2互相阻塞。
其实在编译的时候编译器也会提醒
定时器任务池ScheduledExecutorService
承接上文,直接上代码
package com.example.demo.test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* description
*
* @author luhui
* @date 2022/8/17
*/
public class ScheduledExecutorTest {
public static void main(String[] args) {
Runnable task1 = () -> {
System.out.println("task1:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
throw new RuntimeException();
}
};
Runnable task2 = () -> {
System.out.println("task2:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
throw new RuntimeException();
}
};
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(2);
scheduledExecutorService.scheduleAtFixedRate(task1, 0, 1, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(task2, 0, 1, TimeUnit.SECONDS);
}
}
执行结果如下
task1:2022-08-17 17:09:54
task2:2022-08-17 17:09:54
task1:2022-08-17 17:09:56
task2:2022-08-17 17:09:57
task1:2022-08-17 17:09:58
task2:2022-08-17 17:10:00
task1:2022-08-17 17:10:01
因为我们在new ScheduledThreadPoolExecutor的时候已经指定了两个核心线程了,所以不会出现单线程的问题了。
单机框架的定时任务
Quartz任务调度框架
Quartz是很常用很经典的一个任务框架,使用起来也很简单,比较容易集成到项目中。
单机项目就不用说了,在分布式情况下,其实Quartz也是可以做成集群的。
但是它的集群有以下缺点:
缺点
- 不适合大量的短任务 & 不适合过多节点部署;
- 解决了高可用的问题,并没有解决任务分片的问题,存在单机处理的极限(即:不能实现水平扩展)。
- 需要把任务信息持久化到业务数据表,和业务有耦合
- 调度逻辑和执行逻辑并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免地会相互影响。
- quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡机制。
- 管理不便,多机器容易出现时钟问题。
分布式框架的任务调度
对我们的业务场景而言,分布式调度框架的优点就是
- 因为业务系统众多,所以需要一个解耦的分布式调度框架去做定时任务
- 业务系统接入简单
- 有成熟稳定的社区去迭代,文档周全
- 最好能有报警的机制
- 能够实时的监控任务的执行情况(如时间、结果等)
所以最后选定了xxl-job
自研定时任务框架
这里没有写每种分布式框架的原理,其实分布式调度框架的目的是一样的,所以注定了它的原理不会有太大的差异。
基于这种原理,我们也可以研制自己的定时任务框架,只不过对于大部分场景而言,没必要重复造轮子。