快速归档数据库数据

背景介绍

为什么进行数据归档?因为在实际工作中,数据都是需要保存在数据库的,但是由于数据库本身的特性,当数据量太大会对数据库的性能有较大的影响,因此需要减少数据,将一些过旧的数据进行归档。比如一些操作流水记录,这些数据本身没太大意义,但是由于业务要求可能需要给客服能查询出用户的流水信息,方便处理问题,但是这个数据并不需要存太长时间,所以就需要将旧数据归档到另一个数据库,从而提升系统的性能。在实际工作中数据归档是十分普遍的场景,而且每次归档的数据一般都是比较大量的数据,如何能高效的把数据归档,是一个十分重要的事情。

归档的方式

常见方式

在实际的工作过程中,由于 dao 层都是使用现有的框架,所以在归档的时候大多采用的方式就是使用 dao 层框架提供的方式进行归档。而最常见的 dao 层框架就是 mybatis,所以使用的最多的归档方式就是使用 mybatis 提供的 <foreach> 标签,来动态生成 sql 从而完成数据的批量插入。

<!-- foreach标签介绍 -->
<!-- 
collection="tests" 循环集合的名称,默认是list
item="test" 循环后的对象别名
separator="," 循环的分割符
-->
<insert id="insertTestList">
    insert into test (
            id,
            name,
            create_time
       ) values
      <foreach collection="tests"  item="test"  separator=",">
            (
              #{test.id, jdbcType=BIGINT},
              #{test.name, jdbcType=VARCHAR},
              #{test.createTime, jdbcType=TIMESTAMP}
            )
      </foreach>
</insert>

上面的示例就是最简单的一个使用 foreach 标签批量入库的操作,在正常的小批量操作是完全可以满足要求的。但是对于归档的操作,由于归档的数据量通常比较大,所以使用 foreach 标签并不能很好的处理。因为 mybatis 在处理标签的使用需要进行解析,会生成需对的中间对象,不光会影响批量插入的性能,还会导致 GC 频繁,可能会导致 CPU 使用率飙升。使用 foreach 标签归档数据时,每次归档 50 条数据左右性能是最好的。

高效方式

在使用 dao 层框架提供的批量入库操作时,由于框架本身的限制,并不能提供很好的性能,归档的速度也会比较慢。因此要想高效的归档,就必须放弃使用框架本身提供的方式,而使用 mysql 的原生方法进行归档。刚好 mysql 也提供了一种高效的数据导入的方式 load data infile 这个方式能非常高效的插入数据库。

/**
* 往 StringBuilder 里追加数据
* @param builder StringBuilder
* @param object  数据
* @param endFlag  结束的标识,true 就是结束
*/
private void builderAppend(StringBuilder builder, Object object , boolean endFlag) {
    builder.append(objectToString(object));
    if (endFlag) {
        builder.append("\n");
    } else {
        builder.append("\t");
    }
}

/**
* append数据时的一些逻辑处理,如果数据本身含有特殊符号需要进行替换
* @param object 数据
*/
private String objectToString(Object object) {
    if (null == object) {
        return "\\N";
     }
    if (object instanceof Date) {
          return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) object);
    } else {
         String str = String.valueOf(object);
         if (str.contains("\t")) {
              str = str.replaceAll("\t", " ");
          }
          if (str.contains("\n")) {
              str = str.replaceAll("\n" ," ");
          }
          return str;
      }
}
/**
* 批量插入数据
* @param list 数据列表
* @return 插入的行数
* @throws Exception 抛出的异常
*/
@Override
public int batchInsert(List<Test> list) throws Exception{
    String[] columnArray = TABLE_COLUMNS.split(",");
    StringBuilder sb = new StringBuilder();
    for (Test operateLog :list) {
         for (int i = 0, size = columnArray.length ; i < size ; i++) {
             if (Objects.isNull(columnArray[i])) {
                 continue;
             }
            builderAppend(sb , getObjectValue(columnArray[i] , operateLog) , i == size - 1);
         }
   }
  int insertRow = insertDataByLoadData(sb);
  log.info("insert归档积分操作记录表数量insertRow:"+insertRow+" 需插入的数量:"+list.size());
  return insertRow;
}
/**
* 通过 LOAD DATA LOCAL INFILE 批量导入数据到数据库
* @param builder 拼接的数据
*/
private int insertDataByLoadData(StringBuilder builder) throws SQLException, IOException {
        int rows = 0;
        InputStream input = null;
        try {
            byte[] bytes = builder.toString().getBytes();
            if (bytes.length > 0) {
                input = new ByteArrayInputStream(bytes);
                //批量插入数据。
                long beginTime = System.currentTimeMillis();
                rows = realInsertByInputStream(input);
                long endTime = System.currentTimeMillis();
                log.info(INSERT_SAL+":【插入" + rows + "行数据至MySql中,耗时" + (endTime - beginTime) + "ms。】");
            }
        } finally {
            if (null != input) {
                input.close();
            }
            if (null != conn) {
                conn.close();
            }
        }
        return rows;
    }
/**
 * 将数据从输入流批量导入到数据库。
 * @param inputStream   输入流。
 * @return int         成功插入的行数。
 */
private int realInsertByInputStream(InputStream inputStream) throws SQLException {
        if (null == inputStream) {
            log.info("输入流为NULL,没有数据导入。");
            return 0;
        }
        conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection();
        PreparedStatement statement = conn.prepareStatement(INSERT_SAL);
        //mysql-connector-java 6
        if (statement.isWrapperFor(com.mysql.cj.api.jdbc.Statement.class)) {
            com.mysql.cj.jdbc.PreparedStatement mysqlStatement = statement.unwrap(com.mysql.cj.jdbc.PreparedStatement.class);
            mysqlStatement.setLocalInfileInputStream(inputStream);
            return mysqlStatement.executeUpdate();
        }
        return 0;
}

以上是如何使用 mybatis 提供的 load data infile ,在 Java 中的操作方式,这种方式需要注意的是:
1、需要数据库的支持,可以使用以下命令查看,和设置

查看
SHOW VARIABLES LIKE '%local%';
如果 local_infile 为 OFF 则需要设置,用下面的命令设置即可
SET GLOBAL local_infile=1;

2、由于使用的时候不同的 mysql-connector-java 的版本,可能会有稍许差别,所以需要注意。

总结

数据库归档是十分常见的操作,因此使用高效的归档方式是有必要的;
在工作中由于归档操作一般是晚上执行,但是如果效率太低,执行的时间太长,很可能会到白天才执行完成。而白天需要给用户提供很好的体验,所以如果归档太慢,很可能影响用户的整个体验。
最后,我整理了一份简单的归档demo,有需要可以借鉴。
https://github.com/zhanyiping/backup-data.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容