欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

不想 CRUD 干到老,就来看看这篇 OOM 排查的实战案例

程序员文章站 2022-04-29 08:18:39
...

  优质文章,及时送达

  一、经历概要

  程序里有个跑数据的job,这个job的主要功能是往数据库写假数据。

  既需要跑历史数据(传给job的日期是过去的时间),也需要能够上线后,实时跑(十秒钟触发一次,传入触发时的当前时间)。

  其中一个job比较奇葩点,要写入的数据比较难以随机生成,是产品的同事从互联网上找的数据,比如当前网络上的热门话题,然后导入到数据库中。所以,

  我这边随机的时候,不能乱造。因此我的策略是,从数据库将已经存在的那几条真实数据查询出来,然后job中根据随机数,选择其中一条来仿造一条新的,

  随机生成新记录的其他字段,再写入数据库中。

  我单元测试一直这么跑的,没有任何问题,直到,将定时触发器打开,然后上线运行。悲剧来了。

  二、程序大体逻辑1、job接口定义:

  /**

  * desc:

  * 造数据的job,可按表来划分。一个表一个job

  * @author :

  * creat_date: 2021/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中了。

  (job实现了

  org.springframework.beans.factory.InitializingBean接口,保证初始化数据只被调用一次,所谓的初始化数据是指:读文件,读数据库之类的准备工作,后续的假数据都从这里面取)

  这边是出问题的job的源码:

  @Component

  publicclassTopicWebsiteJobextendsBaseJobimplementsDataProduceJob{

  @Autowired

  private TopicWebsiteMapper topicWebsiteMapper;

  private Date date;

  Random random=new Random;

  private List topicWebsites;

  /**

  * 当前job执行时的时间,会作为创建时间写入数据库表

  *

  * @param date

  */

  @Override

  publicvoidjobInit(Date date) {

  this.date=date;

  topicWebsites=topicWebsiteMapper.selectAll;

  }

  @Override

  publicvoidjobDetail {

  for (TopicWebsite website : topicWebsites) {

  for (int i=0; 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);

  }

  }

  }

  }

  2、job的历史数据初始化器

  初始化器,主要是用于生成历史数据,用的是随机生成的过去30天内的时间,去new一个job。然后调用job的init,设置date;然后调用job的细节。上面我也说了,没必要搞两个,只是最初设计失误了。

  总体逻辑,就是传入日期,然后根据那个日期,去造假数据。

  package com..datavisual.quartz.init;

  /**

  * desc:

  * 用于造初始化数据

  * @author :

  * creat_date: 2021/6/11 0011

  * creat_time: 14:29

  **/

  publicinterfaceIniter{

  /**

  * 具体的初始化逻辑,可参考

  * @return 成功或失败

  */

  Boolean init;

  }

  出问题的初始化器的源码:

  /**

  * desc:

  *

  * @author:

  * creat_date: 2021/6/11 0011

  * creat_time: 14:28

  **/

  @Component

  publicclassTopicWebsiteIniterimplementsIniter{

  @Autowired

  private TopicWebsiteJob job;

  @Override

  public Boolean init {

  DateTime now=DateTime.now;

  //日期循环,30天

  for (int a=-29; a

  for (int b=0; 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;

  }

  }

  3、目前为止,运行正常?

  到目前为止,运行没什么问题,因为我都是用单元测试的方式去调用游戏账号交易上面的initer.init方法。真的吗?

  4、加上定时触发机制

  这些job,在上线后,还是需要继续运行。具体的间隔,是每十秒触发一次。

  code如下:

  @Component

  publicclassTopicWebsiteSchedulerimplementsDataProduceScheduler{

  private static final Logger logger=LoggerFactory.getLogger(TopicWebsiteScheduler.class);

  @Autowired

  private TopicWebsiteJob job;

  @Override

  @Scheduled(cron="0/10 * * * * ?}")

  public Boolean schedule {

  logger("start...");

  job.jobInit(new Date);

  job.jobDetail(1);

  return true;

  }

  }

  5、问题出来了

  就上面的代码,上线一运行,因为job比较多,说实话,也没注意一些细节,没去查看数据库的数据条数。我一直以为没啥问题,直到运行了没一会,程序假死了,卡着不动了。

  后来将堆转储拿出来分析,才发现,是因为每次init被多次调用了,每次调用都会从表里面查所有数据(一直以为只有10条真实数据)。然后根据这些数据,去生成新的假数据。再插回表里。这时候表里的数据,差不多翻倍了。

  再过10s后,再次查询,这次查到20条,然后,又造了20条假数据,写到表里,变成了40条。再过10s后,再次查询,这次查到40条,然后,又造了40条假数据,写到表里,变成了80条。

  ...

  然后就越来越慢,越来越卡。直到发现表里竟然变成了千万条数据,然后将java程序的内存撑爆了。

  三、总结

  其实这次主要的坑,在于自己设计功力不够,没有考虑清楚。数据库的数据是变化的,而我拿变化的东西作为基准,来生成假数据,再将假数据写入到原表,造成了

  表里数据的指数级增长,然后撑爆了内存。

  抛开这块不说,比较有意思的是,查找这个bug背后原因的过程,后边单独写。如果喜欢我的文章,记得点赞转发支持一波~~~