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

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

二进制日志包含有关修改数据库内容的 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 中使用存储函数的条件可概括如下。这些条件不适用于存储过程或事件调度器事件,并且仅在启用二进制日志记录时才适用。

  • 要创建或更改存储函数,您必须拥有 SET_ANY_DEFINER 权限,以及通常需要的 CREATE ROUTINEALTER ROUTINE 权限。(根据函数定义中的 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,您必须拥有 SUPER 权限,以及 ALTER ROUTINE 权限。没有 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,否则函数创建不需要 SUPER

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

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

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

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

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

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

    • CONTAINS SQLNO SQLREADS SQL DATAMODIFIES SQL DATA 特性提供有关函数是否读取或写入数据的的信息。无论是 NO SQL 还是 READS 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,且 namevar_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 语句(如果它发生在一个未被记录的语句中)。因此,在使用调用过程的存储函数时应该小心谨慎,即使该过程本身是安全的。