因为当初 ORM 框架的成熟使用,很多小伙伴对于 JDBC 的概念有些单薄,ORM 框架底层其实是通过 JDBC 操作的 DB
JDBC(JavaDataBase Connectivity)是 Java 数据库连贯, 说的直白点就是应用 Java 语言操作数据库
由 SUN 公司提供出一套拜访数据库的标准 API,并提供绝对应的连贯数据库协定规范,而后 各厂商依据标准提供一套拜访自家数据库的 API 接口
文章大数据量操作外围围绕 JDBC 开展,目录构造如下:
MySQL JDBC 大数据量操作
- 惯例查问
- 流式查问
- 游标查问
- JDBC RowData
- JDBC 通信原理
流式游标内存剖析
- 单次调用内存应用
- 并发调用内存应用
- MyBatis 如何应用流式查问
- 结言
MySql JDBC 大数据量操作
整篇文章以大数据量操作为议题,通过开发过程中的需要引出相干知识点
- 迁徙数据
- 导出数据
- 批量解决数据
一般而言笔者认为在 Java Web 程序里,可能被称为大数据量的,几十万到千万不等,再高的话 Java(WEB 利用)解决就不怎么适合了
举个例子,当初业务零碎须要从 MySQL 数据库里读取 500w 数据行进行解决,应该怎么做
- 惯例查问,一次性读取 500w 数据到 JVM 内存中,或者分页读取
- 流式查问,建设长连贯,利用服务端游标,每次读取一条加载到 JVM 内存
- 游标查问,和流式一样,通过 fetchSize 参数,管制一次读取多少条数据
文章首发自公众号:源码趣味圈,专一分享高并发、分布式、框架底层源码等常识
惯例查问
默认状况下,残缺的检索后果集会将其存储在内存中。在大多数状况下,这是最无效的操作形式,并且因为 MySQL 网络协议的设计,因而更易于实现
假如单表 500w 数据量,没有人会一次性加载到内存中,个别会采纳分页的形式
@SneakyThrows
@Override
public void pageQuery() {@Cleanup Connection conn = dataSource.getConnection();
@Cleanup Statement stmt = conn.createStatement();
long start = System.currentTimeMillis();
long offset = 0;
int size = 100;
while (true) {String sql = String.format("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE LIMIT %s, %s", offset, size);
@Cleanup ResultSet rs = stmt.executeQuery(sql);
long count = loopResultSet(rs);
if (count == 0) break;
offset += size;
}
log.info("???????????? 分页查问耗时 :: {}", System.currentTimeMillis() - start);
}
上述形式比较简单,然而在不思考 LIMIT 深分页优化状况下,线上数据库服务器就凉了,亦或者你能等个几天工夫检索数据
MySQL 千万数据量深分页优化, 回绝线上故障!
流式查问
如果你正在应用具备大量数据行的 ResultSet,并且无奈在 JVM 中为其调配所需的内存堆空间,则能够通知驱动程序从后果流中返回一行
流式查问有一点须要留神:必须先读取(或敞开)后果集中的所有行,而后能力对连贯收回任何其余查问,否则将引发异样
应用流式查问,则要放弃对产生后果集的语句所援用的表的并发拜访,因为其 查问会独占连贯,所以必须尽快解决
@SneakyThrows
public void streamQuery() {@Cleanup Connection conn = dataSource.getConnection();
@Cleanup Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);
long start = System.currentTimeMillis();
@Cleanup ResultSet rs = stmt.executeQuery("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE");
loopResultSet(rs);
log.info("???????????? 流式查问耗时 :: {}", (System.currentTimeMillis() - start) / 1000);
}
流式查问库表数据量 500w 单次调用工夫耗费:≈ 6s
游标查问
SpringBoot 2.x 版本默认连接池为 HikariPool,连贯对象是 HikariProxyConnection,所以下述设置游标形式就不可行了
((JDBC4Connection) conn).setUseCursorFetch(true);
须要在数据库连贯信息里拼接 &useCursorFetch=true。其次设置 Statement 每次读取数据数量,比方一次读取 1000
@SneakyThrows
public void cursorQuery() {@Cleanup Connection conn = dataSource.getConnection();
@Cleanup Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(1000);
long start = System.currentTimeMillis();
@Cleanup ResultSet rs = stmt.executeQuery("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE");
loopResultSet(rs);
log.info("???????????? 游标查问耗时 :: {}", (System.currentTimeMillis() - start) / 1000);
}
游标查问库表数据量 500w 单次调用工夫耗费:≈ 18s
JDBC RowData
下面都应用到了办法 loopResultSet,办法外部只是进行了 while 循环,惯例、流式、游标查问的外围点在于 next 办法
@SneakyThrows
private Long loopResultSet(ResultSet rs) {while (rs.next()) {// 业务操作}
return xx;
}
ResultSet.next() 的逻辑是实现类 ResultSetImpl 每次都从 RowData 获取下一行的数据。RowData 是一个接口,实现关系图如下
默认状况下 ResultSet 会应用 RowDataStatic 实例,在生成 RowDataStatic 对象时就会把 ResultSet 中所有记录读到内存里,之后通过 next() 再一条条从内存中读
RowDataCursor 的调用为批处理,而后进行外部缓存,流程如下:
- 首先会查看本人外部缓冲区是否有数据没有返回,如果有则返回下一行
- 如果都读取结束,向 MySQL Server 触发一个新的申请读取 fetchSize 数量后果
- 并将返回后果缓冲到外部缓冲区,而后返回第一行数据
当采纳流式解决时,ResultSet 应用的是 RowDataDynamic 对象,而这个对象 next() 每次调用都会发动 IO 读取单行数据
总结来说就是,默认的 RowDataStatic 读取全副数据到客户端内存中,也就是咱们的 JVM;RowDataCursor 一次读取 fetchSize 行,生产实现再发动申请调用;RowDataDynamic 每次 IO 调用读取一条数据
JDBC 通信原理
一般查问
在 JDBC 与 MySQL 服务端的交互是通过 Socket 实现的,对应到网络编程,能够把 MySQL 当作一个 SocketServer,因而一个残缺的申请链路应该是:
JDBC 客户端 -> 客户端 Socket -> MySQL -> 检索数据返回 -> MySQL 内核 Socket 缓冲区 -> 网络 -> 客户端 Socket Buffer -> JDBC 客户端
一般查问的形式在查问大数据量时,所在 JVM 可能会凉凉,起因如下:
- MySQL Server 会将检索出的 SQL 后果集通过输入流写入到内核对应的 Socket Buffer
- 内核缓冲区通过 JDBC 发动的 TCP 链路进行回传数据,此时数据会先进入 JDBC 客户端所在内核缓冲区
- JDBC 发动 SQL 操作后,程序会被阻塞在输出流的 read 操作上,当缓冲区有数据时,程序会被唤醒进而将缓冲区数据读取到 JVM 内存中
- MySQL Server 会一直发送数据,JDBC 一直读取缓冲区数据到 Java 内存中,尽管此时数据已到 JDBC 所在程序本地,然而 JDBC 还没有对 execute 办法调用处进行响应,因为须要等到对应数据读取结束才会返回
- 弊病就不言而喻了,如果查问数据量过大,会一直经验 GC,而后就是内存溢出
游标查问
通过上文得悉,游标能够解决一般查问大数据量的内存溢出问题,然而
小伙伴有没有思考过这么一个问题,MySQL 不晓得客户端程序何时生产实现,此时另一连贯对该表造成 DML 写入操作应该如何解决?
其实,在咱们应用游标查问时,MySQL 须要建设一个长期空间来寄存须要被读取的数据,所以不会和 DML 写入操作产生抵触
然而游标查问会引发以下景象:
- IOPS 飙升 ,因为须要返回的数据须要写入到长期空间中, 存在大量的 IO 读取和写入,此流程可能会引起其它业务的写入抖动
- 磁盘空间飙升 ,因为写入长期空间的数据是在原表之外的,如果表数据过大, 极其状况下可能会导致数据库磁盘写满 ,这时网络输入时没有变动的。而写入长期空间的数据会在 读取实现或客户端发动 ResultSet#close 操作时由 MySQL 回收
- 客户端 JDBC 发动 SQL 查问,可能会有长时间期待 SQL 响应,这段时间为服务端筹备数据阶段。然而 一般查问等待时间与游标查问等待时间原理上是不统一的,前者是统一在读取网络缓冲区的数据,没有响应到业务层面;后者是 MySQL 在筹备长期数据空间,没有响应到 JDBC
- 数据筹备实现后,进行到传输数据阶段,网络响应开始飙升,IOPS 由 ” 读写 ” 转变为 ” 读取 ”
采纳游标查问的形式 通信效率比拟低,因为客户端生产完 fetchSize 行数据,就须要发动申请到服务端申请,在数据库后期筹备阶段 IOPS 会十分高,占用大量的磁盘空间以及性能
流式查问
当客户端与 MySQL Server 端建设起连贯并且交互查问时,MySQL Server 会通过输入流将 SQL 后果集返回输入,也就是 向本地的内核对应的 Socket Buffer 中写入数据,而后将内核中的数据通过 TCP 链路回传数据到 JDBC 对应的服务器内核缓冲区
- JDBC 通过输出流 read 办法去读取内核缓冲区数据,因为开启了流式读取,每次业务程序接管到的数据只有一条
- MySQL 服务端会向 JDBC 代表的客户端内核源源不断的输送数据,直到客户端申请 Socket 缓冲区满,这时的 MySQL 服务端会阻塞
- 对于 JDBC 客户端而言,数据每次读取都是从本机器的内核缓冲区,所以性能会更快一些,个别状况不用放心本机内核无数据生产(除非 MySQL 服务端传递来的数据,在客户端不做任何业务逻辑,拿到数据间接放弃,会产生客户端生产比服务端超前的状况)
看起来,流式要比游标的形式更好一些,然而事件往往不像外表上那么简略
- 绝对于游标查问,流式对数据库的影响工夫要更长一些
- 另外流式查问依赖网络,导致网络拥塞可能性较大
流式游标内存剖析
表数据量:500w
内存查看工具:JDK 自带 Jvisualvm
设置 JVM 参数:-Xmx512m -Xms512m
单次调用内存应用
流式查问内存性能报告如下
游标查问内存性能报告如下
依据内存占用状况来看,游标查问和流式查问都 可能很好的避免 OOM
并发调用内存应用
并发调用:Jmete 1 秒 10 个线程并发调用
流式查问内存性能报告如下
并发调用对于内存占用状况也很 OK,不存在叠加式减少
流式查问并发调用工夫均匀耗费:≈ 55s
游标查问内存性能报告如下
游标查问并发调用工夫均匀耗费:≈ 83s
因为设施限度,以及局部状况只会在极其下产生,所以没有进行生产、测试多环境验证,小伙伴感兴趣能够自行测试
MyBatis 如何应用流式查问
上文都是在形容如何应用 JDBC 原生 API 进行查问,ORM 框架 Mybatis 也针对流式查问进行了封装
ResultHandler 接口只蕴含 handleResult 办法,能够获取到已转换后的 Java 实体类
@Slf4j
@Service
public class MyBatisStreamService {
@Resource
private MyBatisStreamMapper myBatisStreamMapper;
public void mybatisStreamQuery() {long start = System.currentTimeMillis();
myBatisStreamMapper.mybatisStreamQuery(new ResultHandler<YOU_TABLE_DO>() {
@Override
public void handleResult(ResultContext<? extends YOU_TABLE_DO> resultContext) {}});
log.info("???????????? MyBatis 查问耗时 :: {}", System.currentTimeMillis() - start);
}
}
除了下述注解式的利用形式,也能够应用 .xml 文件的模式
@Mapper
public interface MyBatisStreamMapper {@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(YOU_TABLE_DO.class)
@Select("SELECT COLUMN_A, COLUMN_B, COLUMN_C FROM YOU_TABLE")
void mybatisStreamQuery(ResultHandler<YOU_TABLE_DO> handler);
}
Mybatis 流式查问调用工夫耗费:≈ 18s
JDBC 流式与 MyBatis 封装的流式读取比照
- MyBatis 绝对于原生的流式还是慢上了不少,然而思考到底层的封装的个性,这点性能还是能够承受的
- 从内存占比而言,两者稳定相差无几
- MyBatis 绝对于原生 JDBC 更为的不便,因为封装了回调函数以及序列化对象等个性
两者具体的应用,能够针对我的项目理论状况而定,没有最好的,只有最适宜的
结言
流式查问、游标查问能够防止 OOM,数据量大能够思考此计划 。然而这两种形式会占用数据库连贯,应用中不会开释,所以线上针对大数据量业务用到游标和流式操作, 肯定要进行并发管制
另外针对 JDBC 原生流式查问,Mybatis 中也进行了封装,尽管会慢一些,然而 性能以及代码的整洁水平会好上不少
作者马称,坐标帝都 Java 后端研发,CSDN 博客专家,专一高并发、分布式、框架底层源码等常识分享
长按上图微信二维码,增加作者好友,备注公众号,共同进步
举荐几篇品质还行的文章:
- 【通俗易懂】1.5 w 字,16 张图,入门 RLock、AQS 并发编程原理
- 【我的项目必备】1w 字,18 张图,彻底说分明什么是 springboot starter
- 【强烈推荐】审慎应用 JDK 8 新个性并行流 ParallelStream
- 【强烈推荐】一文疾速把握 Redisson 如何实现分布式锁原理
参考文章:
- https://blog.csdn.net/xieyuoo…