本文摘要:
DBLE是一款企业级的开源分布式中间件,江湖人送外号 “MyCat Plus”。Prepared Statement协议是MySQL 5.1版本新加入的功能。MyCat 从1.6版本实现了Prepared Statement协议,但 MyCat 存在一些至今仍未修复的Bug。
本文将从两名 DBLE 用户提交的Bug开始说起,详细解读DBLE是如何实现 Prepared Statement 协议的。

事发当天

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)

信息分析

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

MySQL Prepared Statement 协议
先看看MySQL官方的介绍:
MySQL 5.1 版本开始为服务器端预处理语句提供支持。此支持利用了高效的客户端/服务器二进制协议。将带有占位符的预准备语句用于参数值具有以下好处:
1.每次执行时解析语句的开销更少。通常,数据库应用程序处理大量几乎相同的语句,只更改子句中的文字或变量值,例如 WHERE 查询和删除,SET 更新和 VALUES 插入。
2.防止 SQL 注入攻击。参数值可以包含未转义的 SQ L引号和分隔符。

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

注意:
1.COM_STMT_SEND_LONG_DATA 必须在COM_STMT_EXECUTE 前发送。
2.COM_STMT_FETCH 必须在 COM_STMT_EXECUTE 后发送。
3.COM_STMT_RESET 是专为重置COM_STMT_SEND_LONG_DATA ,不能单独使用。

通过一图一表,我们对 MySQL Prepared Statement 协议交互中的各种行为做了一个回顾。下面让我们看看DBLE 是如何实现的。
DBLE 对 prepare statement 协议的实现
DBLE 对于客户端的缓存了预编译 SQL 并模拟返回报文,对于服务端改用 COM_QUERY 命令执行,并将各节点返回数据整合转换格式返回客户端。

说明:
1.在 COM_STMT_PREPARE 阶段,DBLE 此时接收的 SQL 不完整,不能确定下发节点,但 MySQL Prepared Statement 协议要求此处返回一个 response 报文,因此 DBLE 会伪装 COM_STMT_PREPARE_OK 报文返回。若有些 MySQL 驱动(如 JDBC )想从 response 报文中获取信息,这些信息会不准确。
2.在 COM_STMT_EXECUTE 阶段,DBLE 会根据客户端传输的参数将预编译 SQL 替换为具体的 SQL,并使用 COM_QUERY 命令下发至后端节点,因为 COM_QUERY 不再使用二进制协议传输,因此 DBLE 需要对后端返回的数据进行转换后再返回客户端。
3、DBLE 不支持 COM_STMT_FETCH 命令。

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

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

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

  • 必须是 SELECT 语句
  • 设置了 fetchSize > 0
  • 设置了 useCursorFetch = true
  • 数据集类型为 ResultSet.TYPE_FORWARD_ONLY
  • 数据集并发设置为 ResultSet.CONCUR_READ_ONLY
  • 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();
        }
    }
}

尾声

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

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

开源分布式中间件DBLE
社区官网:https://opensource.actionsky.com/
GitHub主页:https://github.com/actiontech/dble
技术交流群:669663113

开源数据传输中间件DTLE
社区官网:https://opensource.actionsky.com/
GitHub主页:https://github.com/actiontech/dtle
技术交流群:852990221