文档主页
MySQL 9.0 参考手册
相关文档 下载本手册
PDF (US Ltr) - 40.0Mb
PDF (A4) - 40.1Mb
手册页 (TGZ) - 258.2Kb
手册页 (Zip) - 365.3Kb
信息 (Gzip) - 4.0Mb
信息 (Zip) - 4.0Mb


MySQL 9.0 参考手册  /  存储对象  /  存储程序二进制日志记录

27.8 存储程序二进制日志记录

二进制日志包含有关修改数据库内容的 SQL 语句的信息。此信息以 事件 的形式存储,这些事件描述了修改。(二进制日志事件与计划事件存储对象不同。)二进制日志有两个重要目的

  • 对于复制,二进制日志用作源复制服务器上的语句记录,这些语句将发送到副本服务器。源服务器将二进制日志中包含的事件发送到其副本,副本执行这些事件以进行与源服务器上所做的相同的更改。参见 第 19.2 节,“复制实现”

  • 某些数据恢复操作需要使用二进制日志。在恢复备份文件后,将在备份后记录的二进制日志中的事件重新执行。这些事件将数据库更新到备份时间点。参见 第 9.3.2 节,“使用备份进行恢复”

但是,如果日志记录发生在语句级别,则在存储程序(存储过程和函数、触发器和事件)方面会存在一些二进制日志记录问题。

  • 在某些情况下,语句可能会影响源和副本上的不同行集。

  • 在副本上执行的复制语句由副本的应用线程处理。除非您实现复制权限检查(参见 第 19.3.3 节,“复制权限检查”),否则应用线程将拥有完全权限。在这种情况下,过程可能会在源和副本服务器上遵循不同的执行路径,因此用户可以编写一个例程,其中包含仅在副本上执行的危险语句。

  • 如果修改数据的存储程序是非确定性的,则它不可重复。这会导致源和副本上的数据不同,或者导致恢复的数据与原始数据不同。

本节介绍 MySQL 如何处理存储程序的二进制日志记录。它说明了实现对存储程序使用施加的当前条件,以及您可以采取的操作以避免日志记录问题。它还提供了有关这些条件原因的更多信息。

除非另有说明,否则此处所述内容都假设在服务器上启用了二进制日志记录(参见 第 7.4.4 节,“二进制日志”。)如果未启用二进制日志记录,则无法进行复制,也无法使用二进制日志进行数据恢复。二进制日志记录默认情况下处于启用状态,仅当您在启动时使用 --skip-log-bin--disable-log-bin 启动服务器时才被禁用。

通常,当二进制日志记录发生在 SQL 语句级别(基于语句的二进制日志记录)时,会出现此处描述的问题。如果您使用基于行的二进制日志记录,则日志包含由于执行 SQL 语句而对各个行所做的更改。当例程或触发器执行时,会记录行更改,而不是进行更改的语句。对于存储过程,这意味着不会记录 CALL 语句。对于存储函数,会记录函数内所做的行更改,而不是函数调用。对于触发器,会记录触发器所做的行更改。在副本端,只会看到行更改,而不会看到存储程序调用。

混合格式二进制日志记录 (binlog_format=MIXED) 使用基于语句的二进制日志记录,除了在仅基于行的二进制日志记录才能保证产生正确结果的情况下。使用混合格式,当存储函数、存储过程、触发器、事件或预处理语句包含任何不适合基于语句的二进制日志记录的内容时,整个语句将被标记为不安全并以行格式记录。用于创建和删除过程、函数、触发器和事件的语句始终是安全的,并以语句格式记录。有关基于行的、混合的和基于语句的日志记录以及如何确定安全和不安全语句的更多信息,参见 第 19.2.1 节,“复制格式”

MySQL 中存储函数的使用条件可以概括如下。这些条件不适用于存储过程或事件调度器事件,并且除非启用二进制日志记录,否则这些条件不适用。

  • 要创建或更改存储函数,除了通常所需的 CREATE ROUTINEALTER ROUTINE 权限外,您还必须拥有 SET_ANY_DEFINER 权限。(根据函数定义中的 DEFINER 值,无论二进制日志记录是否启用,都可能需要 SET_ANY_DEFINER。参见 第 15.1.17 节,“CREATE PROCEDURE 和 CREATE FUNCTION 语句”。)

  • 创建存储函数时,必须声明它确定性或不修改数据。否则,它可能不适合数据恢复或复制。

    默认情况下,为了使 CREATE FUNCTION 语句被接受,必须显式指定 DETERMINISTICNO SQLREADS SQL DATA 中的至少一个。否则会发生错误

    ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL,
    or READS SQL DATA in its declaration and binary logging is enabled
    (you *might* want to use the less safe log_bin_trust_function_creators
    variable)

    此函数是确定性的(并且不修改数据),因此是安全的

    CREATE FUNCTION f1(i INT)
    RETURNS INT
    DETERMINISTIC
    READS SQL DATA
    BEGIN
      RETURN i;
    END;

    此函数使用 UUID(),它不是确定性的,因此函数也不是确定性的,也不安全

    CREATE FUNCTION f2()
    RETURNS CHAR(36) CHARACTER SET utf8mb4
    BEGIN
      RETURN UUID();
    END;

    此函数修改数据,因此可能不安全

    CREATE FUNCTION f3(p_id INT)
    RETURNS INT
    BEGIN
      UPDATE t SET modtime = NOW() WHERE id = p_id;
      RETURN ROW_COUNT();
    END;

    对函数性质的评估基于创建者的 诚实。MySQL 不会检查声明为 DETERMINISTIC 的函数是否不包含产生非确定性结果的语句。

  • 尝试执行存储函数时,如果设置了 binlog_format=STATEMENT,则必须在函数定义中指定 DETERMINISTIC 关键字。如果不是这种情况,则会生成错误,并且函数不会运行,除非指定 log_bin_trust_function_creators=1 来覆盖此检查(参见下文)。对于递归函数调用,仅在最外层调用中需要 DETERMINISTIC 关键字。如果使用基于行的或混合的二进制日志记录,即使函数在没有 DETERMINISTIC 关键字的情况下定义,该语句也会被接受和复制。

  • 由于 MySQL 在创建时不会检查函数是否真的是确定性的,因此使用 DETERMINISTIC 关键字调用存储函数可能会执行对基于语句的日志记录不安全的动作,或者调用包含不安全语句的函数或过程。如果这种情况发生在设置了 binlog_format=STATEMENT 时,则会发出警告消息。如果使用基于行的或混合的二进制日志记录,则不会发出警告,并且该语句会以基于行的格式复制。

  • 要放宽函数创建的上述条件(您必须拥有 SUPER 权限,并且函数必须声明为确定性的或不修改数据),请将全局 log_bin_trust_function_creators 系统变量设置为 1。默认情况下,此变量的值为 0,但您可以像这样更改它

    mysql> SET GLOBAL log_bin_trust_function_creators = 1;

    您也可以在服务器启动时设置此变量。

    如果未启用二进制日志记录,则log_bin_trust_function_creators不适用。除非如前所述,函数定义中的DEFINER值需要它,否则创建函数不需要SUPER权限。

  • 有关可能不安全用于复制的内置函数的信息(因此导致使用它们的存储函数也不安全),请参阅第 19.5.1 节“复制功能和问题”

触发器类似于存储函数,因此关于函数的前面说明也适用于触发器,但以下情况除外:CREATE TRIGGER没有可选的DETERMINISTIC特性,因此假定触发器始终是确定性的。但是,这种假设在某些情况下可能无效。例如,UUID()函数是非确定性的(且不会复制)。使用此类函数时,请务必小心。

触发器可以更新表,因此如果您没有所需的权限,则与存储函数类似的错误消息会与CREATE TRIGGER一起出现。在副本端,副本使用触发器DEFINER属性来确定哪个用户被视为触发器的创建者。

本节的其余部分提供了有关日志记录实现及其影响的更多详细信息。除非您对当前与日志记录相关的存储例程使用条件的原理感兴趣,否则您无需阅读它。此讨论仅适用于基于语句的日志记录,不适用于基于行的日志记录,但第一个项目除外:CREATEDROP语句无论日志记录模式如何都作为语句记录。

  • 服务器将CREATE EVENTCREATE PROCEDURECREATE FUNCTIONALTER EVENTALTER PROCEDUREALTER FUNCTIONDROP EVENTDROP PROCEDUREDROP FUNCTION语句写入二进制日志。

  • 如果函数更改数据并且发生在不会被记录的语句中,则存储函数调用将作为SELECT语句记录。这将防止从非记录语句中使用存储函数导致的数据更改不复制。例如,SELECT语句不会写入二进制日志,但SELECT可能会调用进行更改的存储函数。为了解决这个问题,当给定函数进行更改时,SELECT func_name()语句将写入二进制日志。假设以下语句在源服务器上执行

    CREATE FUNCTION f1(a INT) RETURNS INT
    BEGIN
      IF (a < 3) THEN
        INSERT INTO t2 VALUES (a);
      END IF;
      RETURN 0;
    END;
    
    CREATE TABLE t1 (a INT);
    INSERT INTO t1 VALUES (1),(2),(3);
    
    SELECT f1(a) FROM t1;

    SELECT语句执行时,函数f1()被调用三次。其中两次调用插入了一行,MySQL 为它们中的每一个都记录了SELECT语句。也就是说,MySQL 将以下语句写入二进制日志

    SELECT f1(1);
    SELECT f1(2);

    当函数调用存储过程导致错误时,服务器还会为存储函数调用记录SELECT语句。在这种情况下,服务器将SELECT语句与预期的错误代码一起写入日志。在副本上,如果发生相同的错误,那就是预期的结果,复制将继续。否则,复制将停止。

  • 记录存储函数调用而不是函数执行的语句对复制有安全隐患,这来自两个因素

    • 函数可以在源服务器和副本服务器上遵循不同的执行路径。

    • 在副本上执行的语句由副本的应用线程处理。除非您实现复制权限检查(请参阅第 19.3.3 节“复制权限检查”),否则应用线程将拥有完全权限。

    这意味着,尽管用户必须具有CREATE ROUTINE权限才能创建函数,但用户可以编写一个包含危险语句的函数,该语句仅在由拥有完全权限的线程处理它的副本上执行。例如,如果源服务器和副本服务器的服务器 ID 值分别为 1 和 2,则源服务器上的用户可以创建并调用不安全的函数unsafe_func(),如下所示

    mysql> delimiter //
    mysql> CREATE FUNCTION unsafe_func () RETURNS INT
        -> BEGIN
        ->   IF @@server_id=2 THEN dangerous_statement; END IF;
        ->   RETURN 1;
        -> END;
        -> //
    mysql> delimiter ;
    mysql> INSERT INTO t VALUES(unsafe_func());

    CREATE FUNCTIONINSERT语句将写入二进制日志,因此副本将执行它们。由于副本的应用线程拥有完全权限,因此它将执行危险语句。因此,函数调用对源和副本的影响不同,并且不安全用于复制。

    为了防止为启用了二进制日志记录的服务器出现此危险,除了通常需要的CREATE ROUTINE权限外,存储函数创建者还必须拥有SUPER权限。同样,要使用ALTER FUNCTION,除了ALTER ROUTINE权限外,您还必须拥有SUPER权限。如果没有SUPER权限,将发生错误

    ERROR 1419 (HY000): You do not have the SUPER privilege and
    binary logging is enabled (you *might* want to use the less safe
    log_bin_trust_function_creators variable)

    如果您不想要求函数创建者拥有SUPER权限(例如,如果系统上所有拥有CREATE ROUTINE权限的用户都是经验丰富的应用程序开发人员),请将全局log_bin_trust_function_creators系统变量设置为 1。您也可以在服务器启动时设置此变量。如果未启用二进制日志记录,则log_bin_trust_function_creators不适用。除非如前所述,函数定义中的DEFINER值需要它,否则创建函数不需要SUPER权限。

  • 建议使用复制权限检查,无论您对函数创建者的权限做出什么选择。可以设置复制权限检查以确保仅为复制通道授权预期的和相关的操作。有关执行此操作的说明,请参阅第 19.3.3 节“复制权限检查”

  • 如果执行更新的函数是非确定性的,则它不可重复。这可能产生两个不良影响

    • 它会导致副本与源不同。

    • 恢复的数据与原始数据不匹配。

    为了解决这些问题,MySQL 强制执行以下要求:在源服务器上,除非您声明函数是确定性的或不修改数据,否则拒绝创建和更改函数。这里有两组函数特性

    • DETERMINISTICNOT DETERMINISTIC特性指示函数是否始终为给定输入产生相同的结果。如果未给出任何特性,则默认值为NOT DETERMINISTIC。要声明函数是确定性的,您必须显式指定DETERMINISTIC

    • CONTAINS SQLNO SQLREADS SQL DATAMODIFIES SQL DATA特性提供了有关函数是否读取或写入数据的的信息。NO SQLREADS SQL DATA都表示函数不会更改数据,但您必须显式指定其中一个,因为如果没有给出任何特性,则默认值为CONTAINS SQL

    默认情况下,为了使 CREATE FUNCTION 语句被接受,必须显式指定 DETERMINISTICNO SQLREADS SQL DATA 中的至少一个。否则会发生错误

    ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL,
    or READS SQL DATA in its declaration and binary logging is enabled
    (you *might* want to use the less safe log_bin_trust_function_creators
    variable)

    如果将log_bin_trust_function_creators设置为 1,则函数必须是确定性的或不修改数据的要求将被删除。

  • 存储过程调用在语句级别而不是CALL级别记录。也就是说,服务器不会记录CALL语句,而是记录实际执行的过程中的那些语句。因此,在源服务器上发生的相同更改也会发生在副本上。这将防止由于过程在不同机器上的执行路径不同而可能产生的问题。

    一般来说,存储过程中的执行语句使用与独立执行语句相同的规则写入二进制日志。记录过程语句时要特别注意,因为过程中的语句执行与非过程上下文中的语句执行略有不同

    • 要记录的语句可能包含对局部过程变量的引用。这些变量在存储过程上下文之外不存在,因此引用此类变量的语句不能按字面意思记录。相反,对局部变量的每个引用都将替换为以下结构,用于记录目的

      NAME_CONST(var_name, var_value)

      var_name是局部变量名,var_value是表示变量在记录语句时所具有的值的常量。NAME_CONST()的值为var_value名称var_name。因此,如果您直接调用此函数,您将获得类似于以下的结果

      mysql> SELECT NAME_CONST('myname', 14);
      +--------+
      | myname |
      +--------+
      |     14 |
      +--------+

      NAME_CONST()使记录的独立语句能够在副本上执行,从而与在源中存储过程中执行的原始语句具有相同的效果。

      当源列表达式引用局部变量时,使用NAME_CONST()可能会导致CREATE TABLE ... SELECT语句出现问题。将这些引用转换为NAME_CONST()表达式会导致源服务器和副本服务器上的列名不同,或者会导致名称过长而无法成为合法的列标识符。解决方法是为引用局部变量的列提供别名。考虑以下语句,其中myvar的值为 1

      CREATE TABLE t1 SELECT myvar;

      这将被改写为以下形式

      CREATE TABLE t1 SELECT NAME_CONST(myvar, 1);

      为了确保源表和副本表具有相同的列名,请按以下方式编写语句

      CREATE TABLE t1 SELECT myvar AS myvar;

      改写后的语句将变为

      CREATE TABLE t1 SELECT NAME_CONST(myvar, 1) AS myvar;
    • 要记录的语句可能包含对用户定义变量的引用。为了处理这种情况,MySQL 会将一个 SET 语句写入二进制日志,以确保副本上的变量存在,并且值与源上的相同。例如,如果语句引用一个变量 @my_var,则该语句在二进制日志中之前会出现以下语句,其中 value 是源上 @my_var 的值

      SET @my_var = value;
    • 过程调用可以在已提交或已回滚的事务中发生。事务上下文会被考虑在内,以确保过程执行的事务方面能够被正确地复制。也就是说,服务器会记录过程内部实际执行并修改数据的那些语句,还会根据需要记录 BEGINCOMMITROLLBACK 语句。例如,如果一个过程只更新事务表,并且在回滚的事务内执行,那么这些更新不会被记录。如果过程在已提交的事务内发生,则会将 BEGINCOMMIT 语句与更新一起记录。对于在回滚的事务内执行的过程,其语句使用与独立执行语句时相同的规则进行记录

      • 对事务表的更新不会被记录。

      • 对非事务表的更新会被记录,因为回滚不会取消它们。

      • 对事务表和非事务表的混合更新会被记录,并被 BEGINROLLBACK 包围起来,以便副本能够进行与源上相同的更改和回滚。

  • 如果过程是在存储函数内部调用的,则不会在语句级别将存储过程调用写入二进制日志。在这种情况下,唯一被记录的是调用函数的语句(如果它出现在被记录的语句内),或者是一个 DO 语句(如果它出现在未被记录的语句内)。因此,在使用调用过程的存储函数时,应谨慎行事,即使过程本身是安全的。