程序裏有個跑數據的job,這個job的主要功能是往數據庫寫假數據。java
既須要跑歷史數據(傳給job的日期是過去的時間),也須要可以上線後,實時跑(十秒鐘觸發一次,傳入觸發時的當前時間)。web
其中一個job比較奇葩點,要寫入的數據比較難以隨機生成,是產品的同事從互聯網上找的數據,好比當前網絡上的熱門話題,而後導入到數據庫中。因此,面試
我這邊隨機的時候,不能亂造。所以個人策略是,從數據庫將已經存在的那幾條真實數據查詢出來,而後job中根據隨機數,選擇其中一條來仿造一條新的,spring
隨機生成新記錄的其餘字段,再寫入數據庫中。數據庫
我單元測試一直這麼跑的,沒有任何問題,直到,將定時觸發器打開,而後上線運行。。。悲劇來了。網絡
/** * 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); } } } }
初始化器,主要是用於生成歷史數據,用的是隨機生成的過去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程序的內存撐爆了。
其實此次主要的坑,在於本身設計功力不夠,沒有考慮清楚。數據庫的數據是變化的,而我拿變化的東西做爲基準,來生成假數據,再將假數據寫入到原表,形成了
表裏數據的指數級增加,而後撐爆了內存。
拋開這塊不說,比較有意思的是,查找這個bug背後緣由的過程,後邊單獨寫。