在我的上一篇笔记中,我们成功的在本地启动了TiDB,并基本了解了TiDB的启动流程和客户端连接部分,TiDB向上是兼容MySQL的,所以我们使用MySQL客户端可以直接连接到TiDB服务,并且支持绝大部分的MySQL语法。对于MySQL熟悉的朋友玩起来就非常熟练了。
院长:TiDB源码学习笔记:启动TiDBzhuanlan.zhihu.com先看一下一句SQL会经历哪些步骤:SQL语句—>语法解析–>合法性验证–>制定查询计划–>优化查询计划–>根据计划生成查询器–>执行并返回结果。如下图
![b58ce258f0798aaaa5b0d11ab9c1746c.png](https://i-blog.csdnimg.cn/blog_migrate/6eae0b445309eebf6eb44353e31ea6be.jpeg)
话不多说,从源码入手,连上TiDB服务,跑句SQL来跟踪一下SQL的一生到底都经历了什么。上篇文章中我们也说到了监听客户端发送的请求后,会通过 tidb/server/conn.go 里面的dispatch(ctx context.Context, data []byte) 方法来处理客户端传来的数据,所以直接进入,看一下这个方法的具体实现。
// dispatch handles client request based on command which is the first byte of the data.
// It also gets a token from server which is used to limit the concurrently handling clients.
// The most frequently used command is ComQuery.
func (cc *clientConn) dispatch(ctx context.Context, data []byte) error {
......
t := time.Now()
cc.lastPacket = data
// 第一个代表指令类型
cmd := data[0]
// 后面则是处理的SQL语句
data = data[1:]
......
dataStr := string(hack.String(data))
switch cmd {
case mysql.ComPing, mysql.ComStmtClose, mysql.ComStmtSendLongData, mysql.ComStmtReset,
mysql.ComSetOption, mysql.ComChangeUser:
cc.ctx.SetProcessInfo("", t, cmd, 0)
case mysql.ComInitDB:
cc.ctx.SetProcessInfo("use "+dataStr, t, cmd, 0)
}
switch cmd {
case mysql.ComSleep:
// TODO: According to mysql document, this command is supposed to be used only internally.
// So it's just a temp fix, not sure if it's done right.
// Investigate this command and write test case later.
return nil
case mysql.ComQuit:
return io.EOF
case mysql.ComInitDB:
if err := cc.useDB(ctx, dataStr); err != nil {
return err
}
return cc.writeOK(ctx)
case mysql.ComQuery: // Most frequently used command.
// For issue 1989
// Input payload may end with byte '0', we didn't find related mysql document about it, but mysql
// implementation accept that case. So trim the last '0' here as if the payload an EOF string.
// See http://dev.mysql.com/doc/internals/en/com-query.html
if len(data) > 0 && data[len(data)-1] == 0 {
data = data[:len(data)-1]
dataStr = string(hack.String(data))
}
return cc.handleQuery(ctx, dataStr)
case mysql.ComFieldList:
return cc.handleFieldList(ctx, dataStr)
// ComCreateDB, ComDropDB
case mysql.ComRefresh:
return cc.handleRefresh(ctx, data[0])
case mysql.ComShutdown: // redirect to SQL
if err := cc.handleQuery(ctx, "SHUTDOWN"); err != nil {
return err
}
return cc.writeOK(ctx)
// ComStatistics, ComProcessInfo, ComConnect, ComProcessKill, ComDebug
case mysql.ComPing:
return cc.writeOK(ctx)
// ComTime, ComDelayedInsert
case mysql.ComChangeUser:
return cc.handleChangeUser(ctx, data)
// ComBinlogDump, ComTableDump, ComConnectOut, ComRegisterSlave
case mysql.ComStmtPrepare:
return cc.handleStmtPrepare(ctx, dataStr)
case mysql.ComStmtExecute:
return cc.handleStmtExecute(ctx, data)
case mysql.ComStmtSendLongData:
return cc.handleStmtSendLongData(data)
case mysql.ComStmtClose:
return cc.handleStmtClose(data)
case mysql.ComStmtReset:
return cc.handleStmtReset(ctx, data)
case mysql.ComSetOption:
return cc.handleSetOption(ctx, data)
case mysql.ComStmtFetch:
return cc.handleStmtFetch(ctx, data)
// ComDaemon, ComBinlogDumpGtid
case mysql.ComResetConnection:
return cc.handleResetConnection(ctx)
// ComEnd
default:
return mysql.NewErrf(mysql.ErrUnknown, "command %d not supported now", nil, cmd)
}
}
dispatch() 方法中,主要有三个部分,第一个部分是全局追踪的处理,这个我们先不管,第二个部分是Context的处理(Context这个概念理解起来有点难,是go语言的一个标准库,可以脱离出TiDB,单独了解一下该模块),第三部分则是核心部分,对于传入数据 data的处理。基于Mysql协议,Data数据中第一个字节是执行的命令,后面的才是命令的具体语句,我们可以在代码中输出来看一下,前面是单独的一个字节,而后面则是我们输入的SQL:"select * from jk"。
![a6089165b74da4ab7d133fb5e332a53b.png](https://i-blog.csdnimg.cn/blog_migrate/c3bd048205533e895e7c65303f8d5108.jpeg)
代码中会将data切割,分开第一个字节为cmd,然后判断命令类型,大概的基本命令有以下类型:
0x00 COM_SLEEP 内部线程状态
0x01 COM_QUIT 关闭连接
0x02 COM_INIT_DB 切换数据库
0x03 COM_QUERY SQL查询请求
0x04 COM_FIELD_LIST 获取数据表字段信息
0x05 COM_CREATE_DB 创建数据库
0x06 COM_DROP_DB 删除数据库
0x07 COM_REFRESH 清除缓存
0x08 COM_SHUTDOWN 停止服务器
0x09 COM_STATISTICS 获取服务器统计信息
0x0A COM_PROCESS_INFO 获取当前连接的列表
0x0B COM_CONNECT 内部线程状态
0x0C COM_PROCESS_KILL 中断某个连接
0x0D COM_DEBUG 保存服务器调试信息
0x0E COM_PING 测试连通性
0x0F COM_TIME 内部线程状态
0x10 COM_DELAYED_INSERT 内部线程状态
0x11 COM_CHANGE_USER 重新登陆
0x12 COM_BINLOG_DUMP 获取二进制日志信息
0x13 COM_TABLE_DUMP 获取数据表结构信息
0x14 COM_CONNECT_OUT 内部线程状态
0x15 COM_REGISTER_SLAVE 从服务器向主服务器进行注册
0x16 COM_STMT_PREPARE 预处理SQL语句
0x17 COM_STMT_EXECUTE 执行预处理语句
0x18 COM_STMT_SEND_LONG_DATA 发送BLOB类型的数据
0x19 COM_STMT_CLOSE 销毁预处理语句
0x1A COM_STMT_RESET 清除预处理语句参数缓存
0x1B COM_SET_OPTION 设置语句选项
0x1C COM_STMT_FETCH 获取预处理语句的执行结果
我们传入的Select为查询命令,也就是 0x03 COM_QUERY,在数据库的操作中,绝大部分请求都是走的这条路,所以现在就跟着这条语句逐步深入,其他的命令和语句类型就不一一细看了,当判断为COM_QUERY是会执行 cc.handleQuery(ctx, dataStr) 方法,进入这个方法中:
// handleQuery executes the sql query string and writes result set or result ok to the client.
// As the execution time of this function represents the performance of TiDB, we do time log and metrics here.
// There is a special query `load data` that does not return result, which is handled differently.
// Query `load stats` does not return result either.
func (cc *clientConn) handleQuery(ctx context.Context, sql string) (err error) {
......
stmts, err := cc.ctx.Parse(ctx, sql)
......
var pointPlans []plannercore.Plan
if len(stmts) > 1 {
// The client gets to choose if it allows multi-statements, and
// probably defaults OFF. This helps prevent against SQL injection attacks
// by early terminating the first statement, and then running an entirely
// new statement.
capabilities := cc.ctx.GetSessionVars().ClientCapability
if capabilities&mysql.ClientMultiStatements < 1 {
return errMultiStatementDisabled
}
// Only pre-build point plans for multi-statement query
pointPlans, err = cc.prefetchPointPlanKeys(ctx, stmts)
if err != nil {
return err
}
}
if len(pointPlans) > 0 {
defer cc.ctx.ClearValue(plannercore.PointPlanKey)
}
for i, stmt := range stmts {
if len(pointPlans) > 0 {
// Save the point plan in Session so we don't need to build the point plan again.
cc.ctx.SetValue(plannercore.PointPlanKey, plannercore.PointPlanVal{Plan: pointPlans[i]})
}
err = cc.handleStmt(ctx, stmt, parserWarns, i == len(stmts)-1)
if err != nil {
break
}
}
......
}
简化以下代码,这个方法中有两处核心:
第一个核心是 cc.ctx.Parser(ctx, sql) 方法,他会开始解析这个SQL语句,解析为一个AST(抽象语法树),关于解析这一块是整个SQL层中比较复杂的一块,在后面会做详细的讲解,现在我们知道他会解析成一个结构体就行,结构体中包含着解析出来的语法结构和语句内容。
第二个核心是基于解析出来的结构体stmt进行进一步的操作,也就是 cc.handleStmt() 方法,这一块中,会出现一个问题,就是存在多个语句,如果存在多个语句则需要做额外的计划,并进行循环处理,每次只处理一句(处理多个语句需要用户开启相关设置,否则报错返回)
继续进入handleStmt()方法中,一层一层剥开他的面纱
func (cc *clientConn) handleStmt(ctx context.Context, stmt ast.StmtNode, warns []stmtctx.SQLWarn, lastStmt bool) error {
......
rs, err := cc.ctx.ExecuteStmt(ctx, stmt)
......
if rs != nil {
connStatus := atomic.LoadInt32(&cc.status)
if connStatus == connStatusShutdown || connStatus == connStatusWaitShutdown {
return executor.ErrQueryInterrupted
}
err = cc.writeResultset(ctx, rs, false, status, 0)
if err != nil {
return err
}
} else {
handled, err := cc.handleQuerySpecial(ctx, status)
if handled {
execStmt := cc.ctx.Value(session.ExecStmtVarKey)
if execStmt != nil {
execStmt.(*executor.ExecStmt).FinishExecuteStmt(0, err == nil, false)
}
}
if err != nil {
return err
}
}
return nil
}
这个方法进去后,很快啊,看到 ExecuteStmt(ctx, stmt) 这个方法,就突然感觉看到光了,看看他返回的东西叫什么,rs,什么意思? ResultSet! 结果集,代表啥,结果查出来了啊,毫无疑问这段代码中就是这个方法进行结果查询,返回结果集,后面判断结果集如果不为空,就代表有数据需要返回,通过 WriteResultset 方法会将查询出来的数据写到结果集中,如果没有返回数据,就进行一些记录处理。这个地方不多说了,直接先进入 ExecuteStmt() 方法中看看,是怎么处理的。
// ExecuteStmt implements QueryCtx interface.
func (tc *TiDBContext) ExecuteStmt(ctx context.Context, stmt ast.StmtNode) (ResultSet, error) {
rs, err := tc.Session.ExecuteStmt(ctx, stmt)
if err != nil {
return nil, err
}
if rs == nil {
return nil, nil
}
return &tidbResultSet{
recordSet: rs,
}, nil
}
大意了,这个方法很短,只是简单实现个接口,还要需要乘胜追击,步步紧跟,直接进入 tc.Session.ExecuteStmt(ctx,stmt) 方法中
func (s *session) ExecuteStmt(ctx context.Context, stmtNode ast.StmtNode) (sqlexec.RecordSet, error) {
......
// 事务准备,创建一个新的事务上下文
s.PrepareTxnCtx(ctx)
err := s.loadCommonGlobalVariablesIfNeeded()
if err != nil {
return nil, err
}
......
// Transform abstract syntax tree to a physical plan(stored in executor.ExecStmt).
compiler := executor.Compiler{Ctx: s}
stmt, err := compiler.Compile(ctx, stmtNode)
......
// Execute the physical plan.
logStmt(stmt, s.sessionVars)
recordSet, err := runStmt(ctx, s, stmt)
......
return recordSet, nil
}
这个方法内容就比较充实了,首先创建一个事务上下文,准备开始该事务,然后利用 compiler.Compile() 将我们前面解析出来的AST转为一个物理计划,这是执行SQL语句的关键方法,非常重要,其中包括合法性校验,计划制定,物理优化,逻辑优化等等,涉及比较多,如果想去优化TiDB新能,这里就是关键,但是这里就不深入进去了,在后面的文章会进行详细的介绍,第二部分就是执行当前的物理计划 runStmt() 并返回我们想要的结果集,这个是我们的重点,直接进入
// runStmt executes the sqlexec.Statement and commit or rollback the current transaction.
func runStmt(ctx context.Context, se *session, s sqlexec.Statement) (rs sqlexec.RecordSet, err error) {
......
sessVars := se.sessionVars
// Save origTxnCtx here to avoid it reset in the transaction retry.
origTxnCtx := sessVars.TxnCtx
err = se.checkTxnAborted(s)
if err != nil {
return nil, err
}
rs, err = s.Exec(ctx)
sessVars.TxnCtx.StatementCount++
// 如果不是只读的操作,那么需要验证事务合法性,来决定是回滚还是提交
if !s.IsReadOnly(sessVars) {
// All the history should be added here.
if err == nil && sessVars.TxnCtx.CouldRetry {
GetHistory(se).Add(s, sessVars.StmtCtx)
}
// Handle the stmt commit/rollback.
if se.txn.Valid() {
if err != nil {
se.StmtRollback()
} else {
se.StmtCommit()
}
}
err = finishStmt(ctx, se, err, s)
}
if rs != nil {
return &execStmtResult{
RecordSet: rs,
sql: s,
se: se,
}, err
}
if se.hasQuerySpecial() {
// The special query will be handled later in handleQuerySpecial,
// then should call the ExecStmt.FinishExecuteStmt to finish this statement.
se.SetValue(ExecStmtVarKey, s.(*executor.ExecStmt))
} else {
// If it is not a select statement or special query, we record its slow log here,
// then it could include the transaction commit time.
s.(*executor.ExecStmt).FinishExecuteStmt(origTxnCtx.StartTS, err == nil, false)
}
return nil, err
}
这段代码我们简化下,首先会备份一次事务上下文,然后执行一个事务时,如果是只读的语句则直接返回结果集,如果是对数据进行其他操作,就需要验证该操作的合法性,当不合法时需要回滚事务,如果合法则提交事务。当结果集中有数据,则直接返回,没有数据而没有报错,则需要进一步查看是否为特殊语句进行进一步的操作并完成。
这一段中我们主要关注的方法是 rs, err = s.Exec(ctx),执行该语句获取结果,我们继续深入进去学习下
// Exec builds an Executor from a plan. If the Executor doesn't return result,
// like the INSERT, UPDATE statements, it executes in this function, if the Executor returns
// result, execution is done after this function returns, in the returned sqlexec.RecordSet Next method.
func (a *ExecStmt) Exec(ctx context.Context) (_ sqlexec.RecordSet, err error) {
......
// 构建执行器
e, err := a.buildExecutor()
if err != nil {
return nil, err
}
// 初始化执行器所需要的变量
if err = e.Open(ctx); err != nil {
terror.Call(e.Close)
return nil, err
}
......
return &recordSet{
executor: e,
stmt: a,
txnStartTS: txnStartTS,
}, nil
}
这个方法中代码相对较长,我们看一下主要的方法,首先是 e, err := a.buildExecutor() 从计划中构建一个执行者,来执行任务,本文使用的是一句Select的查询语句,所有可以看一下Executor的类型和相关的参数(如下图)是一个 TableReaderExecutor ,根据我们指令的不同和SQL语句解析出来的结果不同,会构建不同的执行者,每个执行者的任务和实现的方法也不同,在这里我们主要看一下 TableReaderExecutor 。
![bee1fc6cf5e830d7a5f5ef093f1c9faf.png](https://i-blog.csdnimg.cn/blog_migrate/fd4818daa1313b97f5fd0f0d92f3f546.jpeg)
Executor 提供的open() 方法会初始化执行器所需要的变量。不同的执行器,该方法的实现不一样,open() 是一个非常重要的步骤,在我们的 TableReaderExecutor 实现的open()中,会使用 distsql.Select 去向 TiKV发送相关的读取数据请求并保存到 SelectResponse 中,然后在后面的步骤中执行 writeResultset() 去读取数据并写给客户端。
所以在这里如果是Insert, Update这种没有返回的数据的请求则会直接在该方法中执行,如果是Select这种有查询结果的语句则会在该方法结束后再去执行,也就是我们上面讲到的 writeResultset() 方法,这里不做过多的讲解,我们接着程序走。
接着往下走后就会开始返回,一直回到 handleStmt() 这个方法中的 writeResultset() 去将数据获取到,并写入结果集中。
// writeResultset writes data into a resultset and uses rs.Next to get row data back.
// If binary is true, the data would be encoded in BINARY format.
// serverStatus, a flag bit represents server information.
// fetchSize, the desired number of rows to be fetched each time when client uses cursor.
func (cc *clientConn) writeResultset(ctx context.Context, rs ResultSet, binary bool, serverStatus uint16, fetchSize int) (runErr error) {
......
var err error
if mysql.HasCursorExistsFlag(serverStatus) {
err = cc.writeChunksWithFetchSize(ctx, rs, serverStatus, fetchSize)
} else {
err = cc.writeChunks(ctx, rs, binary, serverStatus)
}
if err != nil {
return err
}
return cc.flush(ctx)
}
看到核心方法 writeChunks() 和 writeChunksWithFetchSize(),上面先会有一个判断,HasCursorExistsFlag(serverStatus) 这个是判断服务器指示游标是否打开(这是一个数据库的知识点,查询时申请缓冲区存放数据,有兴趣的可以单独了解下),如果没开启就直接调用writeChunk() 方法,进一步跟上。
// writeChunks writes data from a Chunk, which filled data by a ResultSet, into a connection.
// binary specifies the way to dump data. It throws any error while dumping data.
// serverStatus, a flag bit represents server information
func (cc *clientConn) writeChunks(ctx context.Context, rs ResultSet, binary bool, serverStatus uint16) error {
......
for {
// Here server.tidbResultSet implements Next method.
err := rs.Next(ctx, req)
......
// 查看数据有多少行
rowCount := req.NumRows()
......
// 循环读取每一行数据
for i := 0; i < rowCount; i++ {
data = data[0:4]
if binary {
data, err = dumpBinaryRow(data, rs.Columns(), req.GetRow(i))
} else {
data, err = dumpTextRow(data, rs.Columns(), req.GetRow(i))
}
if err != nil {
reg.End()
return err
}
// 数据写入
if err = cc.writePacket(data); err != nil {
reg.End()
return err
}
// 输出看一下 数据是否读出来了
b := string(data)
println(b)
}
......
}
return cc.writeEOF(serverStatus)
}
这段代码比较简单,需要理解的是调用了一个Next(ctx, req)方法,去获取数据,具体的实现比较复杂,有兴趣可以调试时跟着进去看(这个地方调试容易卡住),它会以Region为单位进行,通过nextChunk不断的获取每个Region返回的SelectResponse,并把结果写到Chunk中,接下来就是循环Chunk中的每一行的数据,通过 dumpTextRow() 或 dumpBinaryRow() 将数据读取到,最后由 writePacket(data) 写入到缓冲区给客户端,我们在这个地方输出了一下data, 看一下数据:
![cb6b75eab7747681d8b865204a0e8ce2.png](https://i-blog.csdnimg.cn/blog_migrate/578acdad570aeb9f4aaf081b6dc900b8.jpeg)
读取到了第一行,id 为 1 name 为 jk 的数据,之后循环一直读取所有数据。
到这里,一条“Select * from jk"的SQL语句也就基本走到了一生的结尾,整个的流程我们也跟着编译器一步一步走过,大致了解了其每一个过程所经历的方法和步骤,但是文章所介绍的也比较简单,并没有那么详细到每一句代码,中间有很多的细节处理,日志记录,上下文处理都省略过去了,想要深入去学习这些,需要花更多的时间和精力,在这里主要还是让大家了解一下整个SQL层的大概流程,为后面深入TiDB有一定的基础,学习起来也会变的更轻松一点。在后面的文章中(如果有的话)我会继续介绍这些流程中比较重点的部分,中间我们略过的一些步骤,去详细介绍一下某些模块,比如SQL解析的Parser模块,计划制定和优化的Compiler模块等等。
最后附上这一次学习的一个流程图,希望能帮助大家,同时能够一起学习,一起进步。如果有什么问题或者发现我笔记中的错误,可以留言交流。
![8edd0d98c3c32af9ab53ab12841a3312.png](https://i-blog.csdnimg.cn/blog_migrate/83a192a8c9fda38b7d9da7b084b4fa6c.jpeg)
传送门
TiKV 源码解读:
Jinn Jin:TiKV源码略读-Config
Power App 实战指南:
JustForFun:微软Canvas App实战踩坑指南——事件驱动 or 数据绑定
JustForFun:微软Canvas App实战踩坑指南——undocumented feature