sql long转string_TiDB源码学习笔记:SQL的一生

在我的上一篇笔记中,我们成功的在本地启动了TiDB,并基本了解了TiDB的启动流程和客户端连接部分,TiDB向上是兼容MySQL的,所以我们使用MySQL客户端可以直接连接到TiDB服务,并且支持绝大部分的MySQL语法。对于MySQL熟悉的朋友玩起来就非常熟练了。

院长:TiDB源码学习笔记:启动TiDB​zhuanlan.zhihu.com

先看一下一句SQL会经历哪些步骤:SQL语句—>语法解析–>合法性验证–>制定查询计划–>优化查询计划–>根据计划生成查询器–>执行并返回结果。如下图

b58ce258f0798aaaa5b0d11ab9c1746c.png

话不多说,从源码入手,连上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

代码中会将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

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

读取到了第一行,id 为 1 name 为 jk 的数据,之后循环一直读取所有数据。

到这里,一条“Select * from jk"的SQL语句也就基本走到了一生的结尾,整个的流程我们也跟着编译器一步一步走过,大致了解了其每一个过程所经历的方法和步骤,但是文章所介绍的也比较简单,并没有那么详细到每一句代码,中间有很多的细节处理,日志记录,上下文处理都省略过去了,想要深入去学习这些,需要花更多的时间和精力,在这里主要还是让大家了解一下整个SQL层的大概流程,为后面深入TiDB有一定的基础,学习起来也会变的更轻松一点。在后面的文章中(如果有的话)我会继续介绍这些流程中比较重点的部分,中间我们略过的一些步骤,去详细介绍一下某些模块,比如SQL解析的Parser模块,计划制定和优化的Compiler模块等等。

最后附上这一次学习的一个流程图,希望能帮助大家,同时能够一起学习,一起进步。如果有什么问题或者发现我笔记中的错误,可以留言交流。

8edd0d98c3c32af9ab53ab12841a3312.png

传送门

TiKV 源码解读:

Jinn Jin:TiKV源码略读-Config

Power App 实战指南:

JustForFun:微软Canvas App实战踩坑指南——事件驱动 or 数据绑定

JustForFun:微软Canvas App实战踩坑指南——undocumented feature

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值