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

栏目: IT技术 · 发布时间: 4年前

内容简介:点击

MySQL DBA如何

点击 蓝字 关注我们

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

问题描述

开发报错

MySQL DBA如何
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套件里有相关的测试。

MySQL DBA如何

可以看到对应的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

MySQL DBA如何

用户查询一个表的数据时,首先会构造根据库名、表名等信息构造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;

}

过程模拟

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

session 1 session2
prepare xxx
MySQL DBA如何
MySQL DBA如何

调试过程:

MySQL DBA如何

这里我们只需要将reprepare_attempt < MAX_REPREPARE_ATTEMPTS 改为不满足条件即可因此

修改reprepare_attempt变量为3则,reprepare_attempt < MAX_REPREPARE_ATTEMPTS 返回false

进入报错流程而不会重新加载table

总结:

这个问题的本质就是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 变量或者在线上高压力下才可能出现。

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

  1. 增加table_definition_cache,防止表定义被刷出缓存

  2. 增加MAX_REPREPARE_ATTEMPTS次数,但是这个属于hard code,没法通过参数修改

  3. 没事别FLUSH TABLES...(如备份,包括extrabackup和mysqldump获取一致性位点都会做FTWRL,因此建议专门的从库做备份)

最后,广告是不能少滴。

叶老师新课程《 MySQL性能优化 》已经在腾讯课堂发布,本课程讲解读几个MySQL性能优化的核心要素: 合理利用索引,降低锁影响,提高事务并发度 。下面是报名小程序码,厚着脸皮请求大家推荐给需要的小伙伴们。

MySQL DBA如何

下面是本课程内容目录

MySQL DBA如何

扫码加入MySQL技术Q群

(群号: 650149401)

MySQL DBA如何

点“在看”给我一朵小黄花

MySQL DBA如何


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

学习Web设计

学习Web设计

罗宾斯 / 靳志伟 / 机械工业出版社 / 2009-1 / 65.00元

《学习Web设计(第3版)》从说明网站和网页是如何工作开始,逐步深入。当你看完《学习Web设计(第3版)》时,你将掌握使用优化的图像文件来创建多列CSS布局的技术,而且你将知道如何创建网页。这一版经过了彻底的修订,它可以教你如何根据现代设计的实践经验和专业标准来创建网站。《学习Web设计(第3版)》包含了一些练习,可以帮助你学习各种技术,还有一些小测验可以确保你及时掌握重要的概念。如果你对网站设计......一起来看看 《学习Web设计》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具