分布式 | DBLE 如何实现 Prepared Statement 协议

作者:鲍凤奇

 

本文摘要:

DBLE 是一款企业级的开源分布式中间件,江湖人送外号 “MyCat Plus”。Prepared Statement 协议是 MySQL 5.1 版本新加入的功能。MyCat 从1.6版本实现了 Prepared Statement 协议,但 MyCat 存在一些至今仍未修复的Bug。

本文将从两名 DBLE 用户提交的Bug开始说起,详细解读 DBLE 是如何实现 Prepared Statement 协议的。

 

事发当天 分布式 | DBLE 如何实现 Prepared Statement 协议_第1张图片

2019年4月12日下午,GitHub得到举报,两名 DBLE 用户各发现了一个极为凶残的Bug。DBLE 社区片儿警马上赶到案发现场进行取证并对Bug们开始展开调查。举报信息如下:

BugOne(https://github.com/actiontech/dble/issues/1122)

error message: Dble has an error message 'unknown pStmtId when executing' when the client set useServerPrepStmts=true #1122 dble version: dble-9.9.9.9-884fc6b612d64cc22101226536f8fd1d24580857-20190221182143

BugTwo(https://github.com/actiontech/dble/issues/1124) error message: use PreparedStatement with JDBC and MySQL J Connector will get wrong result when useCursorFetch=true #1124 dble version: dble 2.18.10.5 and before (not yet test on later version)

 

信息分析 分布式 | DBLE 如何实现 Prepared Statement 协议_第2张图片

两个Bug的错误信息虽然不同,但相同点是都涉及到了 Prepared Statement 协议,也就是MySQL 的预处理功能。想要完整了解两个Bug背后的隐情,我们先要回顾一下 MySQL Prepared Statement 协议以及 DBLE 是如何实现 Prepared Statement 协议的。

 

MySQL Prepared Statement 协议

先看看 MySQL 官方的介绍:

MySQL 5.1 版本开始为服务器端预处理语句提供支持。此支持利用了高效的客户端/服务器二进制协议。将带有占位符的预准备语句用于参数值具有以下好处: 

  1. 每次执行时解析语句的开销更少。通常,数据库应用程序处理大量几乎相同的语句,只更改子句中的文字或变量值,例如 WHERE 查询和删除,SET 更新和 VALUES 插入。 

  2. 防止 SQL 注入攻击。参数值可以包含未转义的 SQL 引号和分隔符。

MySQL Prepared Statement 的二进制协议交互过程如图:

分布式 | DBLE 如何实现 Prepared Statement 协议_第3张图片

分布式 | DBLE 如何实现 Prepared Statement 协议_第4张图片

注意:

1. COMSTMTSENDLONGDATA 必须在 COMSTMTEXECUTE 前发送。 2. COMSTMTFETCH 必须在 COMSTMTEXECUTE 后发送。 3. COMSTMTRESET 是专为重置 COMSTMTSENDLONGDATA ,不能单独使用。

通过一图一表,我们对 MySQL Prepared Statement 协议交互中的各种行为做了一个回顾。下面让我们看看 DBLE 是如何实现的。

 

DBLE 对 prepare statement 协议的实现

对于客户端的预编译 请求,DBLE缓存了 SQL 并模拟返回报文,对于服务端改用 COM_QUERY 命令执行,并将各节点返回数据整合转换格式返回客户端。

分布式 | DBLE 如何实现 Prepared Statement 协议_第5张图片

说明:

1. 在 COMSTMTPREPARE 阶段,DBLE 此时接收的 SQL 不完整,不能确定下发节点,但MySQL Prepared Statement 协议要求此处返回一个 response 报文,因此 DBLE 会伪装COMSTMTPREPAREOK 报文返回。若有些 MySQL 驱动(如 JDBC )想从 response 报文中获取信息,这些信息会不准确。

2. 在 COMSTMTEXECUTE 阶段,DBLE 会根据客户端传输的参数将预编译SQL替换为具体的SQL,并使用 COMQUERY 命令下发至后端节点,因为 COMQUERY 不再使用二进制协议传输,因此DBLE需要对后端返回的数据进行转换后再返回客户端。

3. DBLE 不支持 COMSTMT_FETCH 命令。

好了,当我们了解完 MySQL 和 DBLE 对 Prepared Statement 协议实现过程后,我们再回过头来看那个两个Bug到底是怎么来的。

 

 #1122 Bug分析

经过分析,在同一连接中,当发起 COMSTMTCLOSE 销毁当前 prepare statement 后,紧接着又创建一个 prepare statement。这两个操作是在 DBLE 中是异步进行,存在线程安全的问题。知道问题的根源之后,解决方案是 DBLE 将两个操作变成同步操作,避免线程安全的问题。

 

#1124 Bug分析

使用 JDBC 时,在URL中设置 useCursorFetch=true 的参数,希望开启 MySQL Server Side 游标功能。但是要使用 MySQL Server Side 游标需要满足下面条件:

  • 必须是SELECT语句

  • 设置了fetchSize > 0

  • 设置了useCursorFetch = true

  • 数据集类型为ResultSet.TYPEFORWARDONLY

  • 数据集并发设置为ResultSet.CONCURREADONLY

  • Server versions 5.0.5 or newer

这是因为 JDBC 在代码层面做了如下限制:

// we only create cursor-backed result sets if
// a) The query is a SELECT
// b) The server supports it
// c) We know it is forward-only (note this doesn't preclude updatable result sets)
// d) The user has set a fetch size
if (this.resultFields != null && this.useCursorFetch && getResultSetType() == ResultSet.TYPE_FORWARD_ONLY
        && getResultSetConcurrency() == ResultSet.CONCUR_READ_ONLY && getFetchSize() > 0) {
    packet.writeByte(OPEN_CURSOR_FLAG);
} else {
    packet.writeByte((byte) 0); // placeholder for flags
}

那如何开启游标功能呢?以下是 JDBC 部分功能在进行预处理是开启游标的示例:

public static void testPrepareStmt() {
    Connection conn = null;
    PreparedStatement stmt = null;
    try {
        Class.forName("com.mysql.jdbc.Driver");
        conn =  DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/poc?useCursorFetch=true", "root", "123456");
        stmt = conn.prepareStatement("select long_col_1, long_col_2 from problemTable where to_days(create_time) <= to_days(now()) and id = ?");
        stmt.setFetchSize(10);
        stmt.setInt(1, 2);
        ResultSet rs = stmt.executeQuery();
        int count = 0;
        while(rs.next()){
            ++count;
            System.out.println("########### row " + count + " ###################");
            System.out.println("long_col_1 : " + rs.getString(1));
            System.out.println("long_col_2 : " + rs.getString(2));
            System.out.println();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException ce) {
        ce.printStackTrace();
    } finally {
        try {
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

尾声 分布式 | DBLE 如何实现 Prepared Statement 协议_第6张图片

#1122 和#1124 两个Bug从被定位到抓获认罪只用了5天,随后 DBLE 社区发布 DBLE 2.19.03.0 版本,将真相大白于天下。 我们始终相信:真相只有一个!至此DBLE又踩平了一个 MyCat 的坑。

 

推荐阅读:《技术分享 | MyCat的坑如何在分布式中间件DBLE上改善(内含视频链接)》

你可能感兴趣的:(DBLE)