返回

Prepare/Execute 请求处理:深入解析 TiDB 的源码奥秘

见解分享

准备就绪,在 TiDB 源码的汪洋大海中扬帆起航!在上篇文章中,我们深入剖析了 TiDB 对 COM_QUERY 命令的处理流程,而本次航行,我们聚焦于另一个备受欢迎的命令:Prepare/Execute。让我们一同揭开其神秘面纱,探寻其背后的技术奥妙。

一探究竟:Prepare/Execute 的前世今生

在数据库系统中,Prepare/Execute 是一种广泛采用的技术,它可以极大地提升重复查询的执行效率。其背后的原理很简单:它将查询语句划分为两个阶段:

  • Prepare 阶段: 编译并解析查询语句,生成查询计划,但并不执行查询。
  • Execute 阶段: 使用 Prepare 阶段生成的查询计划,执行查询并返回结果。

这样一来,当需要重复执行同一查询时,就不再需要重新编译和解析语句,直接使用 Prepare 阶段生成的查询计划即可,从而大幅缩短执行时间。

揭秘 TiDB 中的 Prepare/Execute 实现

在 TiDB 中,Prepare/Execute 的处理流程主要分为三个步骤:

  1. Prepare 阶段:

    • 解析查询语句,生成查询计划。
    • 将查询计划存储在内存中,并返回一个 Prepare ID。
  2. Execute 阶段:

    • 使用 Prepare ID 检索 Prepare 阶段生成的查询计划。
    • 执行查询并返回结果。
  3. Deallocate 阶段:

    • 释放 Prepare 阶段中分配的资源(例如查询计划)。

技术剖析:从源码到概念

现在,让我们从源码的角度一探究竟,了解 TiDB 中 Prepare/Execute 的实现细节:

Prepare 阶段:

func (c *conn) handlePrepare(p *mysql.PreparePacket) (*mysql.OKPacket, error) {
    // 解析查询语句,生成查询计划
    stmt, err := c.parsePrepare(p)
    if err != nil {
        return nil, err
    }

    // 将查询计划存储在内存中
    prepareID := atomic.AddUint32(&c.lastPrepareID, 1)
    c.preparedStmts[prepareID] = stmt

    // 返回 Prepare ID
    return &mysql.OKPacket{
        AffectedRows: uint64(prepareID),
    }, nil
}

Execute 阶段:

func (c *conn) handleExecute(e *mysql.ExecutePacket) (*mysql.ResultSet, error) {
    // 使用 Prepare ID 检索 Prepare 阶段生成的查询计划
    stmt, ok := c.preparedStmts[e.PrepareID]
    if !ok {
        return nil, errors.New("invalid prepare ID")
    }

    // 执行查询并返回结果
    return stmt.Execute(c)
}

Deallocate 阶段:

func (c *conn) handleDeallocate(d *mysql.DeallocatePacket) (*mysql.OKPacket, error) {
    // 释放 Prepare 阶段中分配的资源
    delete(c.preparedStmts, d.PrepareID)

    // 返回 OK 响应
    return &mysql.OKPacket{}, nil
}