MySQL DBA如何"土土"地利用源码解决没有遇到过的错误?(1615错误)

简介: MySQL DBA如何"土土"地利用源码解决没有遇到过的错误?(1615错误)

03.MySQL DBA如何"土土"地利用源码解决没有遇到过的错误?(1615错误)

本篇文章记录的是遇到一个未知错误的排查过程,由于本人水平有限,有描述不正确的欢迎指正。

问题描述

开发报错

img

MySQL error code 1615 (ER_NEED_REPREPARE): Prepared statement needs to be re-prepared

排查过程

咋一看,没见过这个错误啊,用大腿想了下这个应该是php程序为了防止SQL注入用的prepare执行的。赶紧官方bug搜了一下,一通操作以后路由到了如下地址:https://dev.mysql.com/doc/refman/5.5/en/statement-repreparation.html

简单看了一下,大概意思就是after prepare before execute的阶段,对应的表进行了DDL或者FLUSH TABLES以后table definition cache里面的metadata信息发生了改变,需要reprepare

接着我搜了一下源码,关键字

re-prepare

,然后我看到官方test套件里有相关的测试

img

可以看到对应的worklog为4166 拿到worklog id以后,我赶紧去官方的work log下搜,在High Level Architecture标签下,我注意到了下面几行

Prepared_statement::execute_loop() -- try to execute
a statement, reprepare in case of validation error, and try
again until MAX_REPREPARE_ATTEMPTS has been reached.
mysql_{sql_}stmt_execute() are changed to invoke this
method instead of plain execute().
Prepared_statement::reprepare() -- reprepare a prepared
statement, called from execute_loop() in case of ER_NEED_REPREPARE
error.

找到了对应的入口函数: Prepared_statement::execute_loop() 主要抛出错误位置如下:

if ((sql_command_flags[lex->sql_command] & CF_REEXECUTION_FRAGILE) &&
      error && !thd->is_fatal_error && !thd->killed &&
      // reprepare观察者发现invalidated,尝试MAX_REPREPARE_ATTEMPTS后报错ER_NEED_REPREPARE
      reprepare_observer.is_invalidated() &&
      reprepare_attempt++ < MAX_REPREPARE_ATTEMPTS)
  {
    DBUG_ASSERT(thd->get_stmt_da()->mysql_errno() == ER_NEED_REPREPARE);
    thd->clear_error();
    error= reprepare();
    if (! error)                                /* Success */
      goto reexecute;
  }

注意一下观察者Reprepare_observer定义

/**
Reprepare_observer观察者是用来观察某个表从上一次执行后的版本变化  
这里的"table"可以是MySQL表、临时表、视图或者information schema的表
当我们执行prepared SQL进行打开表并加锁的时候,必须要确认表没有发生改变(DML除外)。因为如果从上次prepare后表发生了改变,那么解析树可能就失效了,例如它可能包含了基于表metadata的优化。
  @sa check_and_update_table_version() 获取版本跟踪算法
*/
class Reprepare_observer
{
public:
  /**
  检查meatadata是否有变化
  */
  bool report_error(THD *thd);
  bool is_invalidated() const { return m_invalidated; }
  void reset_reprepare_observer() { m_invalidated= FALSE; }
private:
  bool m_invalidated;
};

注释里写的非常明显,

check_and_update_table_version() for details of the
  version tracking algorithm

所以我们主要目光聚集在函数check_and_update_table_version,其定义如下:

/**
这里需要比较table definition cache中的元数据版本和之前prepare生成的parse tree中的node进行版本对比
 
*/
static bool
check_and_update_table_version(THD *thd,
                               TABLE_LIST *tables, TABLE_SHARE *table_share)
{
   // 如果table_id != prepare时的table id,抛出错误,如果是prepare时期,虽然也不匹配,但是这个时候并没有观察者,也就不会抛出错误,但是到execute时,已经有了观察者,这个时候不匹配的话,就会抛出错误了
  if (! tables->is_table_ref_id_equal(table_share))
  {
    Reprepare_observer *reprepare_observer= thd->get_reprepare_observer();
    if (reprepare_observer &&
        reprepare_observer->report_error(thd))
    {
      /*
        版本不匹配,抛出错误,返回TRUE
      */
      DBUG_ASSERT(thd->is_error());
      return TRUE;
    }
    /* 根据table definition cache中的table id更新,总是维护最新的 */
    tables->set_table_ref_id(table_share);
  }
  DBUG_EXECUTE_IF("reprepare_each_statement", return inject_reprepare(thd););
  return FALSE;
}

从函数check_and_update_table_version中可以看出来,在prepare和execute之间这段时间内,如果table_ref_id(这里的table id其实就是binlog里面的table map event里面的table id,是可以变化的)如果发生了变化,那么需要reprepare。其中还有一点需要注意的是,在prepare之后,会释放对应的MDL锁,所以这个时候是可以进行DDL操作的。那么问题来了,什么情况下,这个table id会发生变化呢?

  1. DDL(包括ALTER/RENAME/TRUNCATE等)
  2. FLUSH TABLES显式地将表定义刷出缓存
  3. TABLE_DEFINITION_CACHE太小,导致对应的表定义缓存被刷出

以上根据自己的经验不完全统计。。。

关于table id

img

用户查询一个表的数据时,首先会构造根据库名、表名等信息构造hash key,然后从table_def_cache这个hash map中找是否有对应表的缓存,如果存在的话,实例化TABLE_SHARE结构体为TABLE,跟用户交互。如果不存在的话,那么会获取库名、表名等信息存入TABLE_SHARE结构体,在这里生成table_id。 生成table_id的函数在

static Table_id last_table_id;
void assign_new_table_id(TABLE_SHARE *share)
{
  DBUG_ENTER("assign_new_table_id");
  /* Preconditions */
  DBUG_ASSERT(share != NULL);
  mysql_mutex_assert_owner(&LOCK_open);
  DBUG_EXECUTE_IF("dbug_table_map_id_500", last_table_id= 500;);
  DBUG_EXECUTE_IF("dbug_table_map_id_4B_UINT_MAX+501",
                  last_table_id= 501ULL + UINT_MAX;);
  DBUG_EXECUTE_IF("dbug_table_map_id_6B_UINT_MAX",
                  last_table_id= (~0ULL >> 16););
  share->table_map_id= last_table_id++;
  DBUG_PRINT("info", ("table_id=%llu", share->table_map_id.id()));
  DBUG_VOID_RETURN;
}

过程模拟

本模拟案例由重庆八怪提供,非常感谢

session1 session2
prepare xxx
flush tables;
execute tttt;ERROR 1615(HY000): Prepared statement need be re-prepared

调试过程:

img

这里我们只需要将reprepare_attempt < MAX_REPREPARE_ATTEMPTS 改为不满足条件即可因此 修改reprepare_attempt变量为3则,reprepare_attempt < MAX_REPREPARE_ATTEMPTS 返回false 进入报错流程而不会重新加载table

总结:

为解决上述的1615问题,可以通过以下办法:

  1. 增加table_definition_cache,防止表定义被刷出缓存
  2. 增加MAX_REPREPARE_ATTEMPTS次数,但是这个属于hard code,没法通过参数修改
  3. 没事别FLUSH TABLES...(如备份,包括extrabackup和mysqldump获取一致性位点都会做FTWRL,因此建议专门的从库做备份)

这个问题的本质就是table share 在 prepare 和 execute 之间被重新加载了多次 伪代码逻辑如下:

prepare:reprepare_attempt=0 MAX_REPREPARE_ATTEMPTS=3 :
execute:
     如果table table被重置过了那么 reprepare_attempt +=1
     loop:
        重新准备 reprepare
        再次 execute
        如果table table被重置过了继续循环 reprepare_attempt +=1,如果reprepare_attempt>=MAX_REPREPARE_ATTEMPTS 这报错
        否则正常退出循环,正常执行
     end loop

实际会进行3次执行尝试,如果都失败就会报错。因此自己模拟的话还是比较难模拟的,除非直接gdb set 变量,只有在线上高压力下才可能出现。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
2月前
|
Ubuntu 关系型数据库 MySQL
MySQL源码编译安装
本文详细介绍了MySQL 8.0及8.4版本的源码编译安装全过程,涵盖用户创建、依赖安装、cmake配置、编译优化等步骤,并提供支持多Linux发行版的一键安装脚本,适用于定制化数据库部署需求。
327 4
MySQL源码编译安装
|
4月前
|
NoSQL 关系型数据库 MySQL
在Visual Studio Code中设置MySQL源码调试环境
以上步骤涵盖了在VS Code中设置MySQL源码调试环境的主要过程,是一个相对高级的任务,旨在为希望建立强大开发和调试环境的开发者提供指引。遵循这些步骤,将可以利用VS Code强大的编辑和调试功能来深入理解和改进MySQL数据库的底层实现。
379 0
|
9月前
|
关系型数据库 MySQL PHP
源码编译安装LAMP(HTTP服务,MYSQL ,PHP,以及bbs论坛)
通过以上步骤,你可以成功地在一台Linux服务器上从源码编译并安装LAMP环境,并配置一个BBS论坛(Discuz!)。这些步骤涵盖了从安装依赖、下载源代码、配置编译到安装完成的所有细节。每个命令的解释确保了过程的透明度,使即使是非专业人士也能够理解整个流程。
280 18
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
526 1
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,并与使用 RPM 包安装进行了对比。通过具体案例,读者可以了解如何准备环境、下载源码、编译安装、配置服务及登录 MySQL。编译源码安装虽然复杂,但提供了更高的定制性和灵活性,适用于需要高度定制的场景。
673 3
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
1050 2
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置服务等,并与使用 RPM 包安装进行了对比,帮助读者根据需求选择合适的方法。编译源码安装虽然复杂,但提供了更高的定制性和灵活性。
566 2
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤
【10月更文挑战第7天】本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择合适的方法。
497 3
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
334 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
存储 自然语言处理 关系型数据库
MySQL全文索引源码剖析之Insert语句执行过程
【8月更文挑战第17天】在MySQL中,处理含全文索引的`INSERT`语句涉及多步骤。首先进行语法解析确认语句结构无误;接着语义分析检查数据是否符合表结构及约束。随后存储引擎执行插入操作,若涉及全文索引则进行分词处理,并更新倒排索引结构。此外,事务管理确保了操作的完整性和一致性。通过示例创建含全文索引的表并插入数据,可见MySQL如何高效地处理此类操作,有助于优化数据库性能和提升全文搜索效果。
204 2

推荐镜像

更多