Bootstrap

【技术探秘】深入剖析static关键字使用不当导致的诡异Bug,一场历时三月的故障排查记录

在这里插入图片描述

功能需求

在每个月的1号凌晨1点,定时把上个月产生的数据,进行处理汇总到当前月;同时为了方便测试,也可以通过接口的方式,处理指定月份的数据;

需求分析
  1. 需要使用接口调用,那个需要定义一个方法,接受一个时间变量的参数,把上月的产生的数据,汇总到当前月。
  2. 方法需要定时执行,那就给时间设置一个默认值,默认获取上一个月,如果时间字段为空,就使用使用默认值,否则使用接口的值
  3. 增加一个定时任务方法,定时调用方法,处理数据
代码实现
引入工具类

需要使用Hutool的相关工具类,代码增加Maven依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.11</version>
</dependency>
处理数据代码
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.StrUtil;

public class DataHandling {
    // 获取上个月的时间
    private static String lastMonth = DateUtil.lastMonth().toString();

    public void handlingMethod(String handlingTime){
        if(StrUtil.isNotBlank(handlingTime)){
            lastMonth = handlingTime;
        }
        // 根据当前时间,查询数据库,执行相关逻辑
        Console.log("定时任务执行了,时间:{}", lastMonth);
    }
}
定时任务方法

这里为了方便测试,将定时任务设置为每10秒执行一次;

import cn.hutool.cron.CronUtil;
import cn.hutool.cron.task.Task;

public class MyTask1 {

    public static void main(String[] args) {
        // 每月1号凌晨1点执行
        //String schedulingPattern = "0 0 1 1 * ?";

        // 每10秒执行一次
        String schedulingPattern = "0/10 * * * * ? ";

        // 使用CronUtil来安排定时任务的执行
        CronUtil.schedule(schedulingPattern, new Task() {
            @Override
            public void execute() {
                // 调用数据处理方法
                DataHandling dataHandling = new DataHandling();
                dataHandling.handlingMethod(null);
            }
        });

        // 支持秒级别定时任务
        CronUtil.setMatchSecond(true);

        // 启动定时任务
        CronUtil.start();
    }
}
发现问题

该功能经过测试,发现没有什么问题,于是就进行部署上线。某有1号上班的时候,有同事找了过来,就说查不到补数据,让我看一下,于是就开始了问题排查之路。

第一次排查问题

第一次发现这个问题的时候,就先去查了数据库,发现当月的数据为空;是定时任务没有执行吗?于是重点测试了定时任务(Cron)表达式,发现也没有问题,为了不影响同事的使用,就通过接口手动调用,发现代码是可以正常执行,执行完,数据库可以正常查到数据,说明接口没有问题啊!出现这样的问题就很奇怪了,于是就给这个定时任务加了很多log,下个月再排查看下。

在这里插入图片描述

第二次排查问题

第二个月1号的时候,一上班就赶紧看了下那个定时任务,发现数据库里还是没有数据,这时看了下日志,发现定时任务是执行了的,但是竟然执行的是上一个月的数据,纳尼,这什么情况?不可能吧?当时脑袋里的第一个想法就是,总有刁民想害朕!有人动了我的代码?我把源码下载下来看看;有人改了服务器的时间?造成获取的时间不对;经过一系列排查下来,发现竟然没有问题,那是什么问题呢?算了,我把能加日志的地方都加上,下个月再看看,这个问题影响我很多时间了,不能再耽误了,反正也能通过接口执行,实在不行,我就每月手动执行一次。

在这里插入图片描述

第三次排查问题

很快,又到了下一个的1号,经过了前面两次的排查,没有找到问题,这次就等到凌晨1点,等定时任务跑完再休息,看看到底是什么问题;检查了一下部署包,服务器的时间,等快到1点的时候打开日志,就盯着log看是怎么执行的。结果发现定时任务执行的还是上一个月的数据,这么来看,饶了这么一大圈,还是自己的问题?那我就好好排查一下吧!本地启动代码,修改一下机器的时间,让定时任务执行,发现获取的时间始终是服务启动的时获取的时间,时间一直没有变,再看下代码,发现获取上个月的时间竟然有一个static关键字,咳,这下问题算是整明白了。

在这里插入图片描述

问题分析

根据上面的代码,发现lastMonth字段的值是固定的,原因是在程序启动时就获取了当前时间,并将其赋值给了lastMonth字段。

由于lastMonth字段是静态字段,它属于类级别的,而不是对象级别的。

静态字段在类加载时就会被初始化,并且所有的对象共享同一个静态字段的值。因此,无论创建多少个对象,它们都会共享相同的lastMonth值。

为了解决这个问题,我们只需要将获取时间的变量的static关键字去掉即可,这样每次执行定时任务时都会获取最新的当前时间。

修改后的代码如下:

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.StrUtil;

public class DataHandling {
    // 获取上个月的时间
    private String lastMonth = DateUtil.lastMonth().toString();

    public void handlingMethod(String handlingTime){
        if(StrUtil.isNotBlank(handlingTime)){
            lastMonth = handlingTime;
        }
        // 根据当前时间,查询数据库,执行相关逻辑
        Console.log("定时任务执行了,时间:{}", lastMonth);
    }
}

在修改后的代码后,去掉了变量的static关键字,这样每次执行定时任务时,都会重新获取最新的时间。

为什么定位问题用了这么久
  1. 没有把这个问题,当成一个真正的问题去处理,数据不是频繁使用的数据,调用接口也能正常处理数据,没有把它当回事;
  2. 定时任务的执行周期比较长,当月没有发现问题,就把这件事推到下一个月了,很常见的偷懒现象;
  3. 发现问题之后,把服务重启了,这就造成每个月看到的日志,是跑的上一个月的,总以为有人动了我的代码;
Static关键字的作用

在Java语言中,static关键字用于修饰类的成员,包括字段、方法和内部类。

它的作用是将成员声明为静态的,即不依赖于对象的存在而存在。

静态成员属于类级别的,可以通过类名直接访问,而不需要创建对象。

静态字段在类加载时就会被初始化,而不是在对象创建时才被初始化。

静态方法也是类级别的,可以直接通过类名调用,而不需要通过对象调用。

类与对象的区别

类是对一类事物的抽象描述,它定义了事物的属性和行为。

而对象是类的实例化,是具体的实体。类是对象的模板,通过类可以创建多个对象。对象具有自己的状态和行为,可以调用类中定义的方法来执行特定的操作。

最后总结

总结一下,静态字段属于类级别的,所有对象共享同一个静态字段的值;

静态方法属于类级别的,可以直接通过类名调用;

类是对一类事物的抽象描述,对象是类的实例化,具有自己的状态和行为。

;