mybatis 批量插入 插件

2017-01-07 更新:这个插件做了重命名、梳理,新的 github 地址为 MyBatis-Batch

可直接从 Maven 中央仓库引用:
<dependency>
<groupId>net.coderbee</groupId>
<artifactId>mybatis-batch</artifactId>
<version>1.1.0</version>
</dependency>

背景

项目中有个设计不合理的表,总共 8 个字段,有 5 个索引,有几个索引字段还是 32 位的字符串。该表数据量已达 1 亿,最近每天新增 100 万。根据日志看,有次用户上传一个有 200 行记录的 excel,需要往这个表插入 3940 条记录,耗时 72 秒。这么大延迟是没法接受的。

要分析数据库方面的问题,首先是找 DBA 分析下表的情况,说跟以往没多大区别,只是跟这个表有关的插入的执行计划很多。因为这个表的批量插入是这样的:

<select id="batchSave" parameterType="java.util.List">
    INSERT INTO TABLE_NAME(ID,NAME) 
<foreach collection="list"item="itm" separator="union all">
    (SELECT #{itm.id}, #{itm.name} FROM DUAL)
</foreach></select>

这是用 MyBatis 对 Oracle 做批量插入的唯一方法。副作用是:假定 List 的最大长度是 N,那么 Oracle 服务器端就可能有 N 个插入的执行计划。这么多执行计划,DBA 也不乐意去分析呀,而且确实生成的每个执行计划都是很简单的。

优化

如果放弃 union all 的方式,则每条记录都需要各提交一次到数据库,显然也不好。

为了解决 N 个执行计划的问题,做到真正的批量插入,只能修改 MyBatis 的执行逻辑,因此就有了这个项目:mybatis-batchinsert-plugin

目前可以批量插入,但不支持返回主键等其他的功能,有空再完善。

我做这个插件主要是希望达到:

  • 1、解决 Oracle 服务器端对一个表做插入的执行计划过多的问题;用 union all 的方式还可能导致硬解析增多,比如需要插入的总记录有 123 条,以 100 条为一批拼接成一条 sql A,那么剩下的 23 条也会拼接成一条 B,而 B 的频率肯定比 A 的频率低很多,因为要做批量插入的记录总数不是固定的,所以余数也不是固定的,容易导致 B 的解析过期,被清除出去,然后又来了条余数 23 的,那么就需要硬解析了。
  • 2、避免用 union all 拼接成一条 sql,导致这条 sql 里的绑定变量过多。
  • 3、兼容 MyBatis 的用法,在使用上更简单了,因为不需要用 MyBatis 的 for 循环,由插件来做循环;用 union all 方式的话,Java 代码要控制批的大小,需要一个循环,MyBatis 里需要一个,用于拼接 union all。
  • 4、一个对于性能有益的是:每次调用 MyBatis 的 insert 语句,都需要调用一次 Connection.prepareStatement 方法,也就需要访问数据库一次,如果对语句没有缓存的话;用插件的话,不管多少记录,只需要一次。
  • 这个插件对性能提升不会很显著,在我们的开发数据库上测试时,以 1000 条为 1 批,已有数据量 160w,插入 40w 数据,用 union all 的方式平均一条需要 32ms,用插件大概是 27ms。


    欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

    《深入理解 Scala》第四章 — 面向对象编程

    限制在对象或特质的 body 里初始化逻辑的代码

    在编译特质的时候, Scala 创建一个接口/实现对(interface/implementation pair,接口的类名是特质的名字,实现的类名是特质的名字加上 $class),接口用于 JVM 交互,实现则是一组静态方法,在类实现特质时可以用到。

    $ cat Test.scala
    trait Application {
        def main(args: Array[String]){
        }
    }
    
    object Test extends Application {
        println("hello world!")
    }
    
    // 编译后的接口和实现
    $ javap -c Test{,\$.class}
    //  接口类
    Compiled from "Test.scala"
    public final class Test {
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #16                 // Field Test$.MODULE$:LTest$;
           3: aload_0
           4: invokevirtual #18                 // Method Test$.main:([Ljava/lang/String;)V  // 转发调用实现类的方法
           7: return
    }
    
    //  实现类
    Compiled from "Test.scala"
    public final class Test$ implements Application {
      public static final Test$ MODULE$;
    
      // 静态初始化块,实例化实现类
      public static {};
        Code:
           0: new           #2                  // class Test$
           3: invokespecial #14                 // Method "<init>":()V
           6: return
    
      public void main(java.lang.String[]);
        Code:
           0: aload_0
           1: aload_1
           2: invokestatic  #21                 // Method Application$class.main:(LApplication;[Ljava/lang/String;)V
           5: return
    }
    

    目前,静态初始化代码块里的方法不能应用 HopSpot 优化。

    继续阅读

    《深入理解 Scala》第三章 — 来点样式 — 编码规范

    编码规范的目标可以归纳为三类:

    • 代码发现:作用在于使工程师能够轻松地推理代码,看懂其他工程师的意图。
    • 代码一致性:作用在于使整个项目的编码风格保持一致。
    • 错误防止:指那些有助于在生产代码里避免 bug 的样式规则。

    设计编码规范应该采取的方法:

    1. 从错误防止规范开始;
    2. 开发一套代码发现相关的规范,应与团队使用的开发环境匹配;
    3. 最后是代码一致性规范,应考虑到自动化工具支持的需要。

    空悬操作符是指一行代码的最后一个非空字符是一个操作符,有助于编译器判断语句真正结束的位置。

    名称重整(name mangling):是指编译器修改或修饰类名或方法名,以便把它编译到底层的平台上。Scala 里名称重整用在嵌套类或辅助方法上。

    当 Scala 必须生成匿名函数或匿名类时,Scala 会生成一个名字,包括函数或类所处的类名、字符串 annofun 和一个数字, 用 $ 符号连接起来。

    编译器也会对默认参数应用名称重整措施。Scala 里默认参数也是被编码成方法的,方法名为 default 加上用来表示参数在函数里的出现顺序的序号。这出现在方法名字空间(namespace)里而不是类名字空间里。方法名$default$序号

    继续阅读

    《深入理解 Scala》第二章 — 核心规则

    在 REPL 里做实验

    优先擦用面向表达式编程

    表达式返回值;语句执行代码,但不返回值。Scala 大部分语句都返回其最后一个表达式的值作为结果。

    • 不变性(immutability):指对象或变量赋值后就不再改变状态。
    • 可变性(mutability):指对象或变量在其生命周期中能够被改变或操纵。

    优先选择不变性

    Scala 里的所有变量都是指向对象的引用。把变量声明为 val 参数意味着它是个不变的“引用”。

    所有的方法参数都是不变引用,类参数默认是不变引用。

    创建可变引用的唯一方法是使用 var 引用。

    Scala 里不变性很重要,它有助于程序员推理代码,在定义判等或写并发程序时尤其明显。

    默认情况下,Scala 用对象位置判等法和散列。对象位置判等法用对象在内存中的位置来作为判等的唯一因素。

    Scala 提供了 ## 方法来对应 hashCode 方法,== 方法来对应 equals 方法。hashCode 和 equals 方法总是成对实现。

    继续阅读

    《深入理解 Scala》第一章 — Scala 一种混合式编程语言

    Scala 的预期目标是将面向对象、函数式编程和强大的类型系统结合起来,同时仍然要能写出优雅、简洁的代码。

    Scala 视图将以下三组对立的思想融合到一种语言中:

    • 函数式编程和面向对象编程
    • 富有表达力的语法和静态类型
    • 高级的语言特性同事保持与 Java 的高度集成

    面向对象编程

    面向对象编程是一种自顶向下的程序设计方法,将代码以名词(对象)做切割,每个对象有某种形式的标识符(self/this)、行为(方法)和状态(成员变量)。识别出名词并且定义出它们的行为后,再定义出名词之间的交互。实现交互时,必须将这些交互放在其中一个对象中(而不能独立存在)。现代面向对象设计倾向于定义出”服务类“,将操作多个领域对象的方法集合放在里面。这些服务类虽然也是对象,但通常不具有独立状态,也没有与它们所操作的对象无关的独立行为。

    函数式编程

    函数式编程方法通过组合和应用函数来构造软件。函数式编程通常倾向于将软件分解为其需要执行的行为或操作,而且通常采用自底向上的方法。函数式编程:对不可变性的强调有助于编写并发程序,视图将副作用推迟到尽可能晚,提供了非常强大的对事物进行抽象和组合的能力。消除副作用使得对程序进行推理(reasoning)变得容易。

    继续阅读