Related Documentation Download this Manual
PDF (US Ltr) - 40.8Mb
PDF (A4) - 40.9Mb
Man Pages (TGZ) - 294.0Kb
Man Pages (Zip) - 409.0Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb
Excerpts from this Manual

MySQL 8.3 Reference Manual  /  Stored Objects  /  Stored Program Binary Logging

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(或已弃用的SET_USER_IDSUPER特权),此外还需要CREATE ROUTINEALTER ROUTINE特权,通常是必需的。(取决于函数定义中的DEFINER值,SET_ANY_DEFINERSET_USER_IDSUPER可能是必需的,无论二进制日志记录是否启用。见第 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不适用。SUPER特权仅在之前描述的情况下需要,否则函数创建不需要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 可能会调用一个更改数据的存储函数。为了处理这种情况,MySQL 将 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(1);
    SELECT f1(2);

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

  • 服务器还记录了存储函数的调用,如果函数调用了一个存储过程并导致错误。在这种情况下,服务器将 SELECT 语句写入日志,连同预期的错误代码。在副本上,如果出现相同的错误,那么这是预期的结果,复制将继续。否则,复制将停止。

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

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

    这意味着,虽然用户必须拥有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 语句被写入二进制日志,因此副本执行它们。由于副本的应用程序线程具有完全特权,因此它执行危险语句。因此,函数调用在源服务器和副本服务器上具有不同的效果,并且不是复制安全的。

    为了防止这种危险,对于启用了二进制日志记录的服务器,存储函数的创建者必须拥有 SUPER 特权,除了通常需要的 CREATE ROUTINE 特权外。类似地,要使用 ALTER FUNCTION,您必须拥有 SUPER 特权,除了 ALTER ROUTINE 特权外。否则,将发生错误:

    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 不适用。SUPER 特权仅在函数创建时需要,除非定义中指定的 DEFINER 值需要它。

  • 无论您对函数创建者的特权选择如何,都建议使用复制特权检查。复制特权检查可以设置为确保仅授权预期的操作在复制通道上执行。有关如何执行此操作的说明,请参阅 第 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,名称为 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,那么语句将在二进制日志中被preceded by 以下语句,其中 value@my_var 在源服务器上的值:

      SET @my_var = value;
    • 过程调用可以在已提交或回滚的事务中发生。事务上下文被考虑,以便正确地复制事务方面的过程执行。也就是说,服务器记录过程中实际执行的语句,并记录 BEGINCOMMITROLLBACK 语句,如有必要。例如,如果过程仅更新事务表并在回滚的事务中执行,那么这些更新将不被记录。如果过程在已提交的事务中执行,BEGINCOMMIT 语句将与更新一起记录。

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

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

      • 对事务和非事务表的更新被记录在BEGINROLLBACK之间,以便副本可以像源一样进行更改和回滚。

  • 如果存储过程从存储函数内部调用,则不会在语句级别写入二进制日志。在那种情况下,只记录调用函数的语句(如果它发生在记录的语句中)或DO语句(如果它发生在未记录的语句中)。因此,在使用调用存储过程的存储函数时应小心,即使该过程本身是安全的。