java项目MySQL数据库优化实际案例(同步并插入大批量数据)

目录

  • 需求
  • 第一版:
    • 1.1 方案:
    • 1.2 伪代码实现:
    • 1.3 第一版问题:
  • 第二版:
    • 2.1 方案:
    • 2.2 伪代码实现:
    • 2.3 第二版相对于第一版的优点:
    • 2.4 缺点:
  • 第三版
    • 3.1 方案
    • 3.2 伪代码实现:
    • 3.3 第三版相对于第二版的优点:
    • 3.4 缺点:

需求

同步A系统多张表(oracle数据库)中的明细数据,插入到本系统的数据库中(MySQL),并做一些汇总计算,最后展示成报表。

第一版:

1.1 方案:

使用一个大事务,覆盖面是所有的同步和汇总计算逻辑。如果程序出错或异常,则事务回滚。
直接按顺序查询A系统的每张表中的所有数据,按条插入B系统指定表中。

1.2 伪代码实现:

interface Service{
  void syncAndSummaryData();
}

class ServiceImpl{
  @Override
  @Transactional(rollbackFor = Exception.class)
  public void syncAndSummaryData(){
    syncTable1();
    syncTable2();
    syncTable3();
    summaryData();
  }
  public void syncTable1(){
    1、查询A系统的Oracle库table1表中所有数据到ArrayList中
    2、for循环逐条插入到本系统的MySQL库中
  }
  public void syncTable2(){
    1、查询A系统的Oracle库table2表中所有数据到ArrayList中
    2、for循环逐条插入到本系统的MySQL库中
  }
  public void syncTable3(){
    1、查询A系统的Oracle库table3表中所有数据到ArrayList中
    2、for循环逐条插入到本系统的MySQL库中
  }
  public void summaryData(){
    1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
    2、for循环逐条插入到本系统中
  }
}

1.3 第一版问题:

1、现有的事务机制不支持多个数据库操作,所以方法中事务其实是失效的。
2、存在JVM内存溢出的风险。原因:B系统的JVM最大堆内存是4G,如果A系统中存在一张表中数据量在几百万条(每条数据有45个左右字段),一次性查询出全部数据放入List中,则所有数据全部存放在JVM的堆内存中,可能会撑爆JVM内存。

第二版:

2.1 方案:

1、原有事务不变保持。
2、查询A系统的oracle数据库每张表的逻辑,单独抽成另一个方法,方法上方的事务注解中添加传播机制控制,使用NOT_SUPPORTED,即在查询Oracle数据库方法中挂起当前事务,插入MySQL数据库方法中事务才生效,经过运行测试证明,这样可以起到所有插入操作还是在同一个事务中的效果。
3、查询数据采用分页查询,每次查询1万条。

2.2 伪代码实现:

interface Service{
  void syncAndSummaryData();
}

class ServiceImpl{
  private static final int pageSize = 10000;
  
  @Override
  @Transactional(rollbackFor = Exception.class)
  public void syncAndSummaryData(){
    syncTable1();
    syncTable2();
    syncTable3();
    summaryData();
  }

  public void syncTable1(){
    //循环分页查询A系统的Oracle库table1表中数据到ArrayList中,然后再插入到本系统MySQL数据库中
    List list = null;
    int pageNo = 1;
    do{
      Map result = queryTable1(pageNo, pageSize);
      //由于查询oralce库的table1表方法会挂起当前事务,所以查询方法报错不会造成事务回滚。
      //为了在查询报错时,事务也可回滚,采用判断返回标识的方式。
      //如果返回map为空或者返回标识是失败,则抛异常,让事务回滚。
      if(result == null || !(Boolean)result.get("result")){
        throw new Business("查询table1表出错");
      }
      List list = (List)result.get("data");
      if(list == null || list.isEmpty()){
        break;
      }
      pageNo++;
      for循环(逐条插入到本系统的MySQL库中)
      list.clear();//主动清空list,减轻JVM压力
    }while(list.size() == pageSize);
  }

  //查询其他系统的oracle数据库,使用传播机制中的NOT_SUPPORTED,在执行查询方法时挂起当前事务
  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  public Map queryTable1(pageNo, pageSize){
    Map result = new HashMap();
    result.put("result", Boolean.TRUE);
    try{
      按分页条件查询A系统的Oracle库table1表中数据,返回List
      result.put("data", 查询返回的List);
    }catch(Exception e){
      log.error(String.format("查询table的第%s页时报错,原因:", pageNo), e.getMessage);
      result.put("result", Boolean.FALSE);//如果查询报错就返回false,作为供调用方法识别
    }
    return result;
  }

  public void syncTable2(){
    逻辑同table1
  }
  public void syncTable3(){
    逻辑同table1
  }
  public void summaryData(){
    1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
    2、for循环逐条插入到本系统中
  }
}

2.3 第二版相对于第一版的优点:

1、事务生效,报错时事务可回滚
2、一次只查1万条数据,避免了JVM内存溢出的风险。

2.4 缺点:

性能不理想,经过线上验证,同步64万条数据,用时75分钟左右,不满足性能需求。

第三版

3.1 方案

1、去掉事务,加入版本概念保证展示数据正确性(类似于悲观锁和乐观锁的理念),具体做法如下:
(1)存版本表(主要字段有版本号、版本状态,状态有生成中、有效、失效),每条数据中都加入版本号字段。
(2)同步或计算失败时程序中断,垃圾数据(保存版本号)还在,但置对应版本号状态为“失效”。
(3)前端页面的数据查询时只查询有效版本号的数据。
(4)对于失效版本,写一个定时任务,清理垃圾数据。
去掉事务的原因:事务对于SQL操作的性能有很大的影响,经过我们多次线上验证,单条SQL操作:有事务在几百毫秒左右,无事务在几十毫秒左右,速度相差几倍到十几倍。
2、插入操作采用batchInsert,一次插入250条(经过试验,一次插入太多速度反而会慢,250条1次是一个速度较快的值)。即在Mybatis层使用for循环动态生成INSERT语句。最终的SQL示例:
INSERT INTO table1(data1, data2, version) VALUES (‘zhangsan’, 21, 201901001), (‘lisi’, 20, 201901001),(‘wangwu’, 34, 201901001)…共250条;
使用batchInsert的原因:
(1)众所周知,内存操作的速度远大于数据库IO操作。
(2)for循环调用250次插入数据库方法,至少产生了250次数据库IO操作。而batch操作是在内存中循环250次组装成SQL语句,然后调用一次数据库IO操作插入250条数据。
(3)虽然一次插入250条数据的耗时比一次插入1条数据要多一点,但是这点耗时与250次数据库IO操作比起来就要小太多了。

3、查询条数一次扩大到5万条,经过验证一次查询1万条数据需要13秒以上,一次查询5万条数据需要30秒左右。采用5万条可以节省一半的查询时间。

3.2 伪代码实现:

interface Service{
  void syncAndSummaryData();
  List getData();
}

class ServiceImpl{
  private static final int pageSize = 50000;
  
  @Override
  public void syncAndSummaryData(){
    int newVersion = 获取新版本号,并插入版本表中,新版本号状态为“生成中”
    try{
      syncTable1(newVersion);
      syncTable2(newVersion);
      syncTable3(newVersion);
      summaryData(newVersion);
      程序走到这里,同步和计算成功,更新newVersion版本号状态为“有效”,其他版本号状态都为“无效”
    }catch(Exception e){
      出现异常,则更新newVersion版本号状态为“无效”
    }
  }

  private void syncTable1(){
    //循环分页查询A系统的Oracle库table1表中数据到ArrayList中,然后再插入到本系统MySQL数据库中
    List list = null;
    int pageNo = 1;
    do{
      List list = queryTable1(pageNo, pageSize);
      if(list == null || list.isEmpty()){
        break;
      }
      pageNo++;
      for循环(每250条一次批量插入数据到本系统的MySQL库中)
      list.clear();//主动清空list,减轻JVM压力
    }while(list.size() == pageSize);
  }

  //有异常不catch,直接抛到上层方法
  private List queryTable1(pageNo, pageSize){
    按分页条件查询A系统的Oracle库table1表中数据,返回List
    return 查询返回的List;
  }

  private void syncTable2(){
    逻辑同table1
  }
  private void syncTable3(){
    逻辑同table1
  }
  private void summaryData(){
    1、将本系统中同步的数据使用SQL的group by分类汇总后,放入ArrayList中
    2、for循环(每250条一次批量插入数据到本系统的MySQL库中)
  }

  @Override
  public List getData(){
    int version = getValidVersion();//从版本表中获取状态为有效的版本号
    List list = getData(version);//查询版本号是version的数据
  }
}
  //定时任务删除垃圾数据不再赘述

3.3 第三版相对于第二版的优点:

1、再保证了展示数据正确性的前提下,采用了类似于乐观锁的逻辑去掉事务、使用批量插入、增大每次查询条数3种方式,提高了同步速度。
经过验证,平均数据处理速度(查询、计算、插入3种操作的共同平均速度。其中计算逻辑速度较快,占比较小。查询逻辑,每次查询1万条13秒左右,每次查询5万条30秒左右。)
1、在使用事务、逐条插入、每次查询1万条时,整个同步和计算过程中,平均数据处理速度是150条左右/秒,75分钟左右处理64万条数据。
2、不使用事务、逐条插入、每次查询1万条时,整个同步和计算过程中,平均数据处理速度是410条左右/秒,26~30分钟处理64万条数据。
3、不使用事务、250条每次批量插入数据、每次查询5万条时,整个同步和计算过程中,平均数据处理速度是853条左右/秒,12.5分钟左右处理64万条数据(该方案的纯数据库插入操作速度,一条数据50个字段前提下,已经达到了1800条以上/秒)。
此处要声明一下:我们的数据库服务器是固态硬盘。机械硬盘的入库速度肯定不会这么快。

3.4 缺点:

1、因为没有事务保证,数据同步失败后更新“生成中”的版本号为“无效”状态时,也可能更新失败,概率极小,此时需要人工介入或者再用其他的补充逻辑处理(比如长时间处于“生成中”的版本号修改为“无效”状态)。但是不影响数据的正确展示。
但是这个缺点的代价很小,完全可以接受。

你可能感兴趣的:(系统优化案例)