MySQL是如何得到一个客户端命令的?
因为在线程池中看到AliSQL的优化,其中说到改进到在线程池中做到:
接收到一个请求之后,先读取网络包,在根据包的进行操作类型的识别,然后得到SQL语句,然后可以根据SQL的类型与上下文将其分类:
- 查询操作,会话处于自动提交模式,SQL类型为查询语句。
- 更新操作,会话处于自动提交模式,SQL类型为DML语句。
- 事务操作,会话处于事务模式(start transaction或autocommit=0)下的任何语句。
- 管理操作,以上之外的操作,比如“show”、“set”等操作。
所以在解析器执行之前对SQL进行分类,是到底是怎么做到的呢? 通过源码来做一个分析
Server等待客户端的请求
在mysql原生的连接管理方式中每线程每连接
,每个连接会阻塞在get_command这个函数里,等待客户端的请求
在这之前会设置阻塞的时间(net_wait_timeout默认8小时),超时后会退出
> do_command
> my_net_set_read_timeout(net, thd->variables.net_wait_timeout)
> thd->get_protocol()->get_command()
而在percona线程池中,其通过监听线程(listener)来监听(epoll方式)事件,如果客户端有数据可以读,那么监听线程会把这个连接分配给一个工作线程去处理(也可能自己处理)
这里可以注意到,工作线程处理时,在正常情况下,到了get_command()
的时候一定是有数据的,不用等待
一步步读取网络包
> Protocol_classic::get_command()
> read_packet()
> my_net_read()
> net_read_compressed_packet()/net_read_uncompressed_packet() // 压缩/未压缩
> net_read_packet() // 这里可能会有多个包,超过最大长度
> net_read_packet_header() // 读取包头 包括协议头,大小和数量
> net_read_raw_loop() // 读取包头
> net_read_raw_loop() // 读取包的内容
> vio_read() // 循环读取
> mysql_socket_recv()
> recv() // 接收socket缓存区的数据
> parse_packet() // 解析包
解析数据包
parse_packet()函数把网络包解析并保存
格式:
Type | Name | Description |
---|---|---|
int<1> | 执行命令 | 执行的操作,比如切换数据库,查询表等操作 |
string | 参数 | 命令相关的内容 |
第一个参数是执行的命令类型(enum_server_command),如COM_QUERY,COM_INIT_DB等
第二个参数是命令相关的内容,如果COM_INIT_DB保存切换的数据库名,COM_QUERY保存了执行的SQL及参数等
如何区分不同的SQL
在线程池的处理阶段,我们可以通过从网络包中解析到客户端执行命令的类型,但是有一个问题:
由于事务在do_command阶段还需要解析,意味着每个请求的网络包会被解析两次,第一次需要通过MSG_PEEK
的方式读取,即不会清空缓冲区,这样第二次仍然可以读取到网络包
通过解析数据包,可以很容易的区别不同的server_command,直接通过返回的网络包的第一个字节即可判断
如何区分COM_QUERY命令中执行的SQL,没有通过解析器解析SQL,只能通过字符串匹配的方式来获取信息,这样获取到的信息是不足的
再看AliSQL的区分的:
- 查询操作,会话处于自动提交模式,SQL类型为查询语句。
- 更新操作,会话处于自动提交模式,SQL类型为DML语句。
- 事务操作,会话处于事务模式(start transaction或autocommit=0)下的任何语句。
- 管理操作,以上之外的操作,比如“show”、“set”等操作。
只能通过简单的字符串匹配select
,来确定是否是查询语句
通过匹配insert
,update
,delete
来确定是否是DML
可以通过thd_is_transaction_active()
来判断是否开启事务