背景与目的
从过往经验,从Informix到Oracle到DB2,应用使用这些数据库时,都会被强调:在语句中尽量使用变量参数,然后Prepare后可多次使用,减少数据库Server端语句硬解析带来的性能损耗,减少数据库Server端语句Cache空间的占用。
另外,不管是显式声明还是隐式产生,数据库Server端都会为Select语句(或者所有DML语句)一个Cursor(游标),通过Fetch这个Cursor来获取结果集。如果结果集小,一次就可全部取得整个结果集;如果结果集大,就需要多次交互,每次由Server端从库中读取一部分数据返回。每次获取的记录数,可通过参数控制,以在减少交互提高性能与客户端内存资源耗用间权衡。比如Oracle,JDBC是通过fetchsize、OCI是通过OCIStmtFetch函数参数nrows、Pro*C是通过宿主变量数组大小,来控制每次从Server端获取多少条记录回来。
由于上面提到的Prepare与Cursor功能,都是在数据库Server端实现的,所以可以被叫做Server-Side Prepared Statement与Server-Side Cursor。
最近因为某些原因,需要了解下OceanBase分布式数据库,OB对外是兼容MySQL数据库协议,也就看了下MySQL JDBC对Server-Side Prepared Statement和Server-Side Cursor的支持程度,以及OceanBase数据库Server是否支持这两个功能。
MySQL JDBC的实现
这部分内容的主要参考内容是MySQL Connector/J(JDBC) Reference及JDBC源码、MySQL Server源码。
Server-Side Prepared Statement
MySQL JDBC实现了两种Prepared Statement:
Client-Side Prepared Statement
此种方式是由JDBC在客户端模拟对Prepared Statment的功能,实际发往数据库Server端的是已经将变量参数值代入后拼装出来的SQL语句,变量参数值不同就是不同的SQL语句。
这也是MySQL JDBC的默认使用的方式,官方解释的原因是:
default because early MySQL versions did not support the prepared statement feature or had problems with its implementation
让我们来大致看一下JDBC源代码中对Server-Side Prepared Statement的支持实现。
从上面代码可以看出,如果属性useServerPreparedStmts为true,且canServerPrepare也为真,就会使用ServerPreparedStatement类来构造Statement供后续执行SQL使用。属性useServerPreparedStmts是在哪里设置的呢? 是在ConnectionImpl类的initializePropsFromServer()方法中:
而getUseServerPreparedStmts()方法读取了连接属性中的detectServerPreparedStmts属性值:
属性detectServerPreparedStmts在这里被定义:
在JDBC URL里添加useServerPrepStmts=true、或通过Propertiesu将useServerPrepStmts=true传递给DriverManager.getConnection方法、或调用MysqlDataSource的setUseServerPreparedStmts(true)后,在对URL的解析过程中,会调用setUseServerPreparedStmts方法设置这一属性值为true。
再来看下ServerPreparedStatement.getInstance()的实现,它直接调用了构造方法生成Statement类对象:
构造函数会调用serverPrepare(sql)方法,在这个方法代码里有下面的代码行:
此行代码的意思就是,将sql语句封装到MySQL数据包中发给数据库Server,包类型为 COMSTMTPREPARE(MysqlDefs.COMPREPARE与COMSTMTPRPARES值相等:22),由数据库Server完成语句的Prepare,并返回statementid等信息给客户端,客户端后面将此id做为COMSTMTEXECUTE的参数来执行Prepare好的SQL,完成数据查询或操作。
Server-Side Cursor
默认情况下,MySQL JDBC是将数据库查询的结果集,统统地全部收下来放到内存中。在查询返回记录条数不多的情况下,这样做比较高效、性能好。 但当查询结果包含大量记录时,可能会把应用JVM内存撑爆掉。
MySQL官方文档给指出了两条路:
让JDBC Driver一条一条记录地流式返回结果集内容
使用Server-Side Cursor,即数据库Server端游标
第一种方式的激活方式是这样的:
粗略翻了下实现代码,感觉就是控制从网络读取返回数据的速度,一条一条记录地读,实际上Server端已经生成了整个的结果集要往网络连接里写,但客户端这边在卡着小脖进行流量控制。也就理解了为什么官方文档会说:
There are some caveats with this approach. You must read all of the rows in the result set (or close it) before you can issue any other queries on the connection, or an exception will be thrown.
白话点儿讲就是:要么你把所有数据全读回来,要么你中间放弃,否则在这个连接上你做不了其他任何事情。 象我们循环从一张表中取一条记录再根据此记录内容去更新其他表记录的用法,在此种方式是行不通的。
让我们继续看下第二种方式吧,与Server-Side Prepared Statement一样,打开这种方式有三种:
以上面任一种姿势,设置useCursorFetch=true并使用,比如:
属性解析
这一属性的解析路径会沿着路径:ConnectionImpl构造方法 => ConnectionImpl.initializeDriverProperties()=> ConnectionImpl.initializeProperties() => ConnectionPropertiesImpl.postInitialization() 溜达到这段代码:
在溜达的路上,已经将Connection的属性useCursorFetch设置成了true,默认此属性是false,即采用将结果集完全取回客户端的默认方式:
然后判断当useCursorFetch为true时,自动地调方法setDetectServerPreparedStmts()同时设置属性useServerPrepStmts为true,即打开Server-Side Prepared Statement功能,此功能正如上面说得,默认是关闭的,即采用Client-Side Prepared Statement:
由此可见,使用Server-Side Cursor功能时,必定要使用Service-Side Prepared Statement功能。
除此之外,还需要设置其他的一些属性:
设置fetchSize>0,控制每次从Server端取多少条记录回来
resultSetType=ResultSet.TYPEFORWARDONLY
resultSetConcurrency=ResultSet.CONCURREADONLY
因为在这里 和 这里 都在做条件检查 :
实际使用时,只要设置useCursorFetch=true与fetchSize>0即可,因为resultSetType、resultSetConcurrency两个属性值在ConnectionImpl的createStatement()、prepareStatement()方法中,都进行了默认设置:
SQL语句执行
在获取了连接并CreateStatement后,再看看Statement的executeQuery方法针对Server-Side Cursor(useServerFetch=true)做了哪些处理?
首先代码中有下面这样的判断,如果是使用Server-Side Cursor,则去创建一个usingServerFetch的ResultSet,此Result使用PreparedStatement类对象执行SQL。因为自动设置了useServerPrepStmts属性,据上面Server-Side Prepared Statement小节的分析,会创建一个数据库Server端Prepare的语句:
最后PreparedStatement.execute()会被导向ServerPreparedStatement.executeInternal(),然后到其serverExecute()方法里的这里,构建并向数据库Server发送了一个COMSTMTEXECUTE包,包中标志字节中写入了OPENCURSORFLAG标志,这个标志的值与MySQL Server代码里的CURSORTYPEREAD_ONLY是一个值:
MySQL Server的处理
MySQL Server中是怎么处理这个标志的? 在Protocolclassic::parsepacket中对COMSTMTEXECUTE包的处理中是读取了flags域段的:
又在这里,从flags中解析出了是否打开游标的标志位:
再一路到这个位置,判断此标志并调用打开游标的函数:
在这个函数里实现了MySQL的Server端物化游标(materialized cursor) ,即用临时表来存放查询结果集,这个在MySQL官方文档中有所描述:
In MySQL, a server-side cursor is materialized into an internal temporary table. Initially, this is a table, but is converted to a table when its size exceeds the minimum value of the and system variables.
ResultSet获取数据
JDBC 客户端对于Sever-Side Cursor Statement返回的ResultSet使用了RowDataCursor来获取更多的记录:
而RowDataCursor获取更多记录的方法fetchMoreRows()又会去使用MysqlO的方法fetchRowsViaCursor(),最终是发送COMSTMTFETCH包给Server,包里有SQL语句在Server端的id 与 欲读取记录个数:
OceanBase的实现
Server-Side Prepared Statement
开源的OceanBase 0.4源码中,有对Preapare SQL语句的功能实现代码,它读取了MySQL COMSTMTPREPARE包,并有函数方法对此进行了处理:
上面的代码,说明在0.4版本中是实现了此功能的,而且在随源码提供的《OceanBase 0.5 SQL 参考指南.pdf》里面,也是有对Prepare语句语法说明的。 但在蚂蚁金融云OceanBase官方文档《用户指南(SQL语法参考)》中,又说不支持此功能:
不支持prepare, OceanBase不需要你使用prepare。
可能是0.5之后版本,改动了实现架构/功能什么的,取消或暂不能支持此功能。
Server-Side Cursor
从上面对MySQL JDBC的实现分析,OCeanBase要兼容MySQL,必须实现对以下请求包的处理:
COMSTMTPREPARE
带CURSORTYPEREADONLY标志位的COMSTMT_EXECUTE
COMSTMTFETCH
对COMSTMTPREPARE的处理,即对Server-Side Prepared Statement功能的支持,这个在上面已经提到,0.5版本时可能还是支持的,金融云上的新版本是不支持的。
对COMSTMTEXECUTE的处理代码,节略如下:
parseexecuteparams方法中跳过、忽略了请求包中的一些什么东西:
从MySQL COMSTMTEXECUTE包结构描述,可知忽略的内容中包括标志位字节。也就是说,没有理会客户端设置的useCursorFetch属性反映到请求包中的标志CURSORTYPEREAD_ONLY:
OceanBase代码中没有对COMSTMTFETCH的处理代码。
因此,可以说明OceanBase并不支持Server-Side Cursor。这在OceanBase官方文档中有体现:
不支持可更新视图、存储过程、触发器、游标。
另外,还有:
不支持临时表。
结论
如果使用OceanBase做为数据库,应用为规避其对Prepare、Cursor的不支持,需要:
对于Java应用,JDBC设置属性useServerPrepStmts=false(MySQL Connector/J的默认值),采用Client-Side Prepared Statement特性,即将绑定参数值拼入SQL语句,形成完整SQL发给数据库Server端,不调用数据库端Prepare,这些处理由MySQL JDBC Driver完成,应用无感知。
对于C应用,做不到应用无感知,不能使用MySQL Api函数库中所有与Prepare功能相关的函数,包括但不限于:mysqlstmtprepare/mysqlstmtbindprepare/mysqlstmtbindresult/....,需要应用自己完成MySQL JDBC Driver所实现的Client-Side Prepared Statement功能
不管Java还是C应用,都要一次性获取全部结果集到客户端,然后再处理。即采用默认的useCursorFetch属性值(false)。
为使用强大的分布式数据库,必须有所牺牲,没有十全十美,只有折中。
参考资料链接:
https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html
MySQL JDBC源码 - https://github.com/mysql/mysql-connector-j
MySQL Server源码 - https://github.com/mysql/mysql-server
MySQL 5.7文档章节 - C.3 Restrictions on Server-Side Cursors
蚂蚁金融云OceanBase文档《用户指南》- https://www.cloud.alipay.com/docs/2/26469) - “OceanBase SQL快速概览”
OceanBase 0.4源码 - https://github.com/alibaba/oceanbase
领取专属 10元无门槛券
私享最新 技术干货