作者:路路

热爱技术、乐于分享的技术人,目前主要从事数据库相关技术的研究。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

1. 概述

本篇文章主要介绍 DBLE LOAD DATA 大规模数据导入功能的实现,包括方案设计、源码解读。

下面就让我们一起来探秘 DBLE 是如何实现该功能的吧!

2. 方案设计

LOAD DATA 为 MySQL 提供的从文本文件导入数据到表的语法,作为数据库中间件,当然也需要实现对应的功能,来满足用户的导入数据需求。

DBLE 对该功能的实现其实就是直接模拟了 MySQL 对 LOAD DATA 命令相应的处理协议。当然作为数据库中间件,还需要处理相应数据的存储、数据路由情况以及与后端 MySQL 的交互等方面的逻辑。

下图即为 DBLE 对 LOAD DATA 处理的整体流程:

3. 源码解读

DBLE 与 LOAD DATA 功能实现相关的类其实主要有两个,一个是 ServerLoadDataInfileHandler类,一个是 LoadDataUtil 类, ServerLoadDataInfileHandler 类主要处理的是与客户端交互的逻辑,而 LoadDataUtil 类主要处理的是与后端 MySQL 交互的逻辑。

下面我们就从客户端发送命令到 DBLE 处理,最后到 DBLE 与后端 MySQL 交互的过程,来详细看下相应的代码。

当客户端发来 LOAD DATA 导入数据到表命令的时候,DBLE 作为服务端会接收到相应的命令并进行处理,对应的代码在 ServerQueryHandler#query 方法中,这里会判断 SQL 的类型为 LOAD DATA,然后进一步处理:
  1. public void query(String sql) {

  2. ServerConnection c = this.source;

  3. if (LOGGER.isDebugEnabled()) {

  4. LOGGER.debug(String.valueOf(c) + sql);

  5. }

  6. ……

  7. int rs = ServerParse.parse(sql);

  8. boolean isWithHint = ServerParse.startWithHint(sql);

  9. int sqlType = rs & 0xff;

  10. ……

  11. switch (sqlType) {

  12. ……

  13. case ServerParse.LOAD_DATA_INFILE_SQL:

  14. //对LOAD DATA的处理,调用FrontendConnection#loadDataInfileStart方法

  15. c.loadDataInfileStart(sql);

  16. break;

  17. ……

继续看一下 FrontendConnection#loadDataInfileStart 方法:
  1. public void loadDataInfileStart(String sql) {

  2. if (loadDataInfileHandler != null) {

  3. try {

  4. //进一步调用了ServerLoadDataInfileHandler#start方法

  5. loadDataInfileHandler.start(sql);

  6. } catch (Exception e) {

  7. LOGGER.info("load data error", e);

  8. writeErrMessage(ErrorCode.ERR_HANDLE_DATA, e.getMessage());

  9. }


  10. } else {

  11. writeErrMessage(ErrorCode.ER_UNKNOWN_COM_ERROR, "load data infile sql is not unsupported!");

  12. }

  13. }

下面便进入到了 ServerLoadDataInfileHandler#start 方法,前面讲过该类主要处理的是 DBLE 与客户端的交互逻辑。

该方法比较长,大家可以去细看,主要功能还是解析了客户端发送过来的 SQL 语句,然后针对 LOAD DATA 语法,如果导入文件是本机文件,则直接进行解析,否则的话会向客户端发送获取文件的命令,让客户端传输文件过来:
  1. public void start(String strSql) {

  2. ……

  3. parseLoadDataPram();

  4. //如果文件不在本地,则向客户端发送命令,请求数据文件,这里的local可能会让人疑惑,但MySQL语法确实是这么规定的,load data local用法反而是文件不在本地的用法

  5. if (statement.isLocal()) {

  6. isStartLoadData = true;

  7. //request file from client

  8. ByteBuffer buffer = serverConnection.allocate();

  9. RequestFilePacket filePacket = new RequestFilePacket();

  10. filePacket.setFileName(fileName.getBytes());

  11. filePacket.setPacketId(1);

  12. filePacket.write(buffer, serverConnection, true);

  13. } else {

  14. //如果文件在本地的话,先判断文件是否存在,不存在则报错,存在的话需要对文件进行读取,计算每一行的路由结果,然后对不同节点的数据分别进行存储

  15. if (!new File(fileName).exists()) {

  16. String msg = fileName + " is not found!";

  17. clear();

  18. serverConnection.writeErrMessage(ErrorCode.ER_FILE_NOT_FOUND, msg);

  19. } else {

  20. if (parseFileByLine(fileName, loadData.getCharset(), loadData.getLineTerminatedBy())) {

  21. RouteResultset rrs = buildResultSet(routeResultMap);

  22. if (rrs != null) {

  23. flushDataToFile();

  24. isStartLoadData = false;

  25. serverConnection.getSession2().execute(rrs);

  26. }

  27. }

  28. }

  29. }

  30. }

DBLE 发送命令给客户端后,客户端便会源源不断地把数据文件发送过来,对发送过来文件的处理逻辑在 ServerLoadDataInfileHandler#handle 方法中,该方法其实就是对传输过来的文件进行转储,默认数据小于 200Mb 则存在内存中,否则的话存储到本地文件:
  1. public void handle(byte[] data) {

  2. try {

  3. if (sql == null) {

  4. clear();

  5. serverConnection.writeErrMessage(ErrorCode.ER_UNKNOWN_COM_ERROR, "Unknown command");

  6. return;

  7. }

  8. BinaryPacket packet = new BinaryPacket();

  9. ByteArrayInputStream inputStream = new ByteArrayInputStream(data, 0, data.length);

  10. packet.read(inputStream);

  11. //这里就是对发送过来的文件进行转储

  12. saveByteOrToFile(packet.getData(), false);

  13. } catch (IOException e) {

  14. throw new RuntimeException(e);

  15. }

  16. }

文件发送完成,客户端还会发送一个空包过来,告诉 DBLE 数据发送完了,然后 DBLE 会进行下一步处理(其实这里就是 MySQL 协议中的规定),下一步处理的逻辑在 ServerLoadDataInfileHandler#end方法中。

该方法也比较长,主要处理逻辑是将接受过来的文件进一步计算路由,根据计算结果将文件根据不同节点分别存储,最后构建路由结果集,通过 DBLE 下发 LOAD DATA 命令到后端不同的 MySQL 节点:
  1. public void end(byte packId) {

  2. isStartLoadData = false;

  3. this.packID = packId;

  4. //empty packet for end

  5. saveByteOrToFile(null, true);


  6. if (isHasStoreToFile) {

  7. //这里便是计算路由,并根据路由结果存储不同节点的数据文件

  8. parseFileByLine(tempFile, loadData.getCharset(), loadData.getLineTerminatedBy());

  9. }

  10. ……

  11. //构建路由结果集,下发后端MySQL,执行LOAD DATA命令

  12. RouteResultset rrs = buildResultSet(routeResultMap);

  13. if (rrs != null) {

  14. flushDataToFile();

  15. serverConnection.getSession2().execute(rrs);

  16. }

DBLE 与后端 MySQL 的交互逻辑跟客户端与 DBLE 的交互逻辑基本一样,因为都是基于 MySQL 协议嘛,DBLE 这边还需要做的就是将不同节点的数据文件发送给后端的 MySQL,具体的逻辑在 LoadDataUtil#requestFileDataResponse 方法中,该方法就是将 DBLE 处理过的数据文件,发送到后端的 MySQL 了,由 MySQL 来进行真正的数据存储:
  1. public static void requestFileDataResponse(byte[] data, BackendConnection conn) {

  2. byte packId = data[3];

  3. MySQLConnection c = (MySQLConnection) conn;

  4. RouteResultsetNode rrn = (RouteResultsetNode) conn.getAttachment();

  5. LoadData loadData = rrn.getLoadData();

  6. List<String> loadDataData = loadData.getData();


  7. BufferedInputStream in = null;

  8. try {

  9. //如果数据较小,都在内存中,则直接发送

  10. if (loadDataData != null && loadDataData.size() > 0) {

  11. ByteArrayOutputStream bos = new ByteArrayOutputStream();

  12. for (String loadDataDataLine : loadDataData) {

  13. String s = loadDataDataLine + loadData.getLineTerminatedBy();

  14. byte[] bytes = s.getBytes(CharsetUtil.getJavaCharset(loadData.getCharset()));

  15. bos.write(bytes);

  16. }

  17. packId = writeToBackConnection(packId, new ByteArrayInputStream(bos.toByteArray()), c);

  18. } else {

  19. //否则的话,先读取文件,然后再发送数据

  20. in = new BufferedInputStream(new FileInputStream(loadData.getFileName()));

  21. packId = writeToBackConnection(packId, in, c);

  22. }

  23. }

  24. ……

  25. }

到这里,整个 DBLE 对 LOAD DATA 的处理流程就讲完啦。

4. 总结

本篇文章主要分析讲解了 DBLE 对 LOAD DATA 功能的实现,包括方案设计以及源码解读,希望大家看完后能对整个 LOAD DATA 功能有更进一步的了解。