不想CRUD幹到老,就來看看這篇OOM排查的實戰案例!

1、經歷概要

程序裏有個跑數據的job,這個job的主要功能是往數據庫寫假數據。java

既須要跑歷史數據(傳給job的日期是過去的時間),也須要可以上線後,實時跑(十秒鐘觸發一次,傳入觸發時的當前時間)。web

其中一個job比較奇葩點,要寫入的數據比較難以隨機生成,是產品的同事從互聯網上找的數據,好比當前網絡上的熱門話題,而後導入到數據庫中。因此,面試

我這邊隨機的時候,不能亂造。所以個人策略是,從數據庫將已經存在的那幾條真實數據查詢出來,而後job中根據隨機數,選擇其中一條來仿造一條新的,spring

隨機生成新記錄的其餘字段,再寫入數據庫中。數據庫

我單元測試一直這麼跑的,沒有任何問題,直到,將定時觸發器打開,而後上線運行。。。悲劇來了。網絡

2、程序大致邏輯

一、job接口定義:

/**
 * desc:
 * 造數據的job,可按表來劃分。一個表一個job
 * @author : 
 * creat_date: 2018/6/11 0011
 * creat_time: 14:46
 **/
public interface DataProduceJob {
    /**
     * job的初始化
     * @param date
     */
    void jobInit(Date date);

    /**
     * 具體的job運行細節
     */
    void jobDetail(Integer recordNum);
}

job之因此分了上面兩個接口,只是由於設計失誤,徹底能夠融合爲一個方法。jobInit的內容,後來我改寫到job的afterPropertiesSet中了。app

(job實現了org.springframework.beans.factory.InitializingBean接口,保證初始化數據只被調用一次,所謂的初始化數據是指:dom

讀文件,讀數據庫之類的準備工做,後續的假數據都從這裏面取)ide

這邊是出問題的job的源碼:單元測試

package com.ceiec.datavisual.quartz.job;

import com.ceiec.common.utils.FileUtils;
import com.ceiec.common.utils.MathUtils;
import com.ceiec.datavisual.dao.GpsLocationSampleMapper;
import com.ceiec.datavisual.dao.TopicAccountMapper;
import com.ceiec.datavisual.dao.TopicMapper;
import com.ceiec.datavisual.dao.TopicWebsiteMapper;
import com.ceiec.datavisual.model.GpsLocationSample;
import com.ceiec.datavisual.model.Topic;
import com.ceiec.datavisual.model.TopicAccount;
import com.ceiec.datavisual.model.TopicWebsite;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Random;

@Component
public class TopicWebsiteJob extends BaseJob implements DataProduceJob {
    @Autowired
    private TopicWebsiteMapper topicWebsiteMapper;

    private Date date;

    Random random = new Random();

    private List<TopicWebsite> topicWebsites;

    /**
     * 當前job執行時的時間,會做爲建立時間寫入數據庫表
     *
     * @param date
     */
    @Override
    public void jobInit(Date date) {
        this.date = date;
        topicWebsites = topicWebsiteMapper.selectAll();
    }

    @Override
    public void jobDetail() {
        for (TopicWebsite website : topicWebsites) {
            for (int i = 0; i < 5; i++) {
                TopicWebsite topicWebsite = new TopicWebsite();

                topicWebsite.setWebsiteName(website.getWebsiteName());
                topicWebsite.setIconUrl(website.getIconUrl());
                topicWebsite.setHotValue((long) random.nextInt(6354147));
                //設置時間
                topicWebsite.setCreateTime(date);

                topicWebsiteMapper.insert(topicWebsite);
            }
        }
    }

}

二、job的歷史數據初始化器

初始化器,主要是用於生成歷史數據,用的是隨機生成的過去30天內的時間,去new一個job。

而後調用job的init,設置date;而後調用job的細節。

上面我也說了,不必搞兩個,只是最初設計失誤了。

整體邏輯,就是傳入日期,而後根據那個日期,去造假數據。

package com..datavisual.quartz.init;

/**
 * desc:
 * 用於造初始化數據
 * @author : 
 * creat_date: 2018/6/11 0011
 * creat_time: 14:29
 **/
public interface Initer {
    /**
     * 具體的初始化邏輯,可參考
     * @return 成功或失敗
     */
    Boolean init();
}

出問題的初始化器的源碼:

package com.ceiec.datavisual.quartz.init;

import com.ceiec.datavisual.quartz.job.TopicWebsiteJob;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * desc:
 *
 * @author: 
 * creat_date: 2018/6/11 0011
 * creat_time: 14:28
 **/
@Component
public class TopicWebsiteIniter implements Initer {
    @Autowired
    private TopicWebsiteJob job;

    @Override
    public Boolean init() {
        DateTime now = DateTime.now();
        //日期循環,30天
        for (int a = -29; a < 1; a++) {
            for (int b = 0; b < 24; b++) {
                int minutes = (int) (Math.random() * 60);
                Date date = com.ceiec.datavisual.quartz.DateUtils.getNeedTime(b, minutes, 0, a);
                if (a == 0 && date.after(now.toDate())) {

                } else {
                    job.jobInit(date);
                    job.jobDetail(360);
                }
            }
        }

        return true;
    }

}

三、目前爲止,運行正常?

到目前爲止,運行沒什麼問題,由於我都是用單元測試的方式去調用上面的initer.init方法。

真的嗎?

四、加上定時觸發機制

這些job,在上線後,仍是須要繼續運行。具體的間隔,是每十秒觸發一次。

code以下:

package com..datavisual.quartz.schedule;

import com..datavisual.quartz.job.TopicWebsiteJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class TopicWebsiteScheduler implements DataProduceScheduler {
    private static final Logger logger = LoggerFactory.getLogger(TopicWebsiteScheduler.class);

    @Autowired
    private TopicWebsiteJob job;
  
    @Override
    @Scheduled(cron = "0/10 * * * * ?}")
    public Boolean schedule() {
        logger.info("start...");
        job.jobInit(new Date());
        job.jobDetail(1);

        return true;
    }

}

五、問題出來了

就上面的代碼,上線一運行,由於job比較多,說實話,也沒注意一些細節,沒去查看數據庫的數據條數。

我一直覺得沒啥問題,直到運行了沒一會,程序假死了,卡着不動了。

後來將堆轉儲拿出來分析,才發現,是由於每次init被屢次調用了,每次調用都會從表裏面查全部數據(一直覺得只有10條真實數據)。

而後根據這些數據,去生成新的假數據。再插回表裏。這時候表裏的數據,差很少翻倍了。

再過10s後,再次查詢,此次查到20條,而後,又造了20條假數據,寫到表裏,變成了40條。

再過10s後,再次查詢,此次查到40條,而後,又造了40條假數據,寫到表裏,變成了80條。

。。。

而後就愈來愈慢,愈來愈卡。。。直到發現表裏居然變成了千萬條數據,而後將java程序的內存撐爆了。

3、總結

其實此次主要的坑,在於本身設計功力不夠,沒有考慮清楚。數據庫的數據是變化的,而我拿變化的東西做爲基準,來生成假數據,再將假數據寫入到原表,形成了

表裏數據的指數級增加,而後撐爆了內存。

拋開這塊不說,比較有意思的是,查找這個bug背後緣由的過程,後邊單獨寫。

推薦閱讀:

相關文章
相關標籤/搜索