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节,“Replication Formats”。
关于 MySQL 中存储函数的使用条件可以总结如下。这些条件不适用于存储程序或事件调度,并且不适用于除非启用了二进制日志记录。
-
要创建或更改存储函数,您必须拥有
SET_ANY_DEFINER
特权,除非通常需要的CREATE ROUTINE
或ALTER ROUTINE
特权。 (根据函数定义中的DEFINER
值,SET_ANY_DEFINER
可能需要无论是否启用了二进制日志记录。请参阅第15.1.17节,“CREATE PROCEDURE and CREATE FUNCTION Statements”。) -
当您创建存储函数时,您必须声明该函数是确定的或不修改数据。否则,它可能会对数据恢复或复制不安全。
默认情况下,对于
CREATE FUNCTION
语句被接受,至少需要指定一个DETERMINISTIC
、NO SQL
或READS 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
关键字只需要在最外层调用中指定。如果使用行-based或混合二进制日志记录,语句将被接受并复制,即使函数没有指定DETERMINISTIC
关键字。 -
由于 MySQL 在创建时不检查函数是否真的确定,因此在使用
DETERMINISTIC
关键字的存储函数中可能执行不安全的操作,或者调用包含不安全语句的函数或过程。如果在binlog_format=STATEMENT
设置时发生这种情况,会出现警告信息。如果使用行-based或混合二进制日志记录,无警告信息将被发出,并且语句将以行-based格式复制。 -
要放松函数创建的前提条件(即您必须拥有
SUPER
特权,并且函数必须被声明为确定性或不修改数据),将全局log_bin_trust_function_creators
系统变量设置为 1。默认情况下,这个变量的值为 0,但您可以像这样更改它:mysql> SET GLOBAL log_bin_trust_function_creators = 1;
您也可以在服务器启动时设置这个变量。
如果二进制日志记录不启用,
log_bin_trust_function_creators
不适用。SUPER
不需要函数创建,除非,如前所述,函数定义中的DEFINER
值要求它。 -
关于可能不安全的内置函数(因此也会使存储函数使用它们变得不安全),请见第19.5.1节,“Replication Features and Issues”。
触发器类似于存储函数,因此前面的关于函数的备注也适用于触发器,以下例外:CREATE TRIGGER
没有可选的DETERMINISTIC
特性,因此触发器被假设为总是确定的。但是在某些情况下,这个假设可能无效。例如,UUID()
函数是非确定的(并且不复制)。请小心在触发器中使用这些函数。
触发器可以更新表,因此在CREATE TRIGGER
中出现的错误消息类似于存储函数。如果您没有所需的权限, replica 侧使用触发器DEFINER
属性来确定触发器的创建者。
本节的其余部分提供了关于日志实现及其含义的详细信息。您不需要阅读它,除非您对存储程序使用的日志相关条件感兴趣。这一讨论仅适用于语句基于的日志记录,除非是行基于的日志记录,第一个项目:CREATE
和 DROP
语句无论日志模式如何都将被记录为语句。
-
服务器将
CREATE EVENT
、CREATE PROCEDURE
、CREATE FUNCTION
、ALTER EVENT
、ALTER PROCEDURE
、ALTER FUNCTION
、DROP EVENT
、DROP PROCEDURE
和DROP FUNCTION
语句到二进制日志中。 -
存储函数调用将作为
SELECT
语句记录,如果函数改变数据并出现在不记录语句中。这防止了由于存储函数在非记录语句中的使用而导致的数据变化的非复制。例如,SELECT
语句不会写入二进制日志,但可能会调用一个存储函数,该函数会改变数据。为了处理这个问题,MySQL 在函数改变数据时将SELECT
语句写入二进制日志。假设在源服务器上执行以下语句: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
语句到日志,并且包括预期的错误代码。在复制服务器上,如果出现相同的错误,那么预期结果是正确的,复制继续进行。否则,复制停止。 -
记录存储函数调用,而不是函数执行的语句,对复制有安全隐患,这是由两个因素引起的:
-
可能在源服务器和复制服务器上执行不同的函数路径。
-
复制服务器上的语句将被处理的applier线程。除非您实现复制权限检查(见第19.3.3节,“复制权限检查”),applier线程将拥有全权。
隐患是,虽然用户必须拥有
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 FUNCTION
和INSERT
语句写入二进制日志,因此复制服务器执行它们。由于applier线程拥有全权,因此函数调用在源服务器和复制服务器上执行结果不同,不是复制安全的。为了防止这种危险,对于启用二进制日志记录的服务器,存储函数创建者必须拥有
SUPER
特权,除非拥有通常的CREATE ROUTINE
特权。类似地,要使用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
不适用。SUPER
除非函数定义中的DEFINER
值要求它,否则不需要函数创建者拥有SUPER
特权。 -
-
无论您对函数创建者的特权做出何种选择, replication privilege checks 都是建议使用的。可以将 replication privilege checks 设置为确保 replication channel 中的操作只允许预期和相关的操作。有关设置的说明,请见第19.3.3节,“Replication Privilege Checks”。
-
如果一个函数执行更新操作,但不是可重复的,这可能会导致两个不良后果:
-
导致副本与源服务器不同。
-
恢复的数据不匹配原始数据。
为了解决这些问题,MySQL强制执行以下要求:在源服务器上,创建和修改函数的操作被拒绝,除非您声明函数是确定的或不修改数据。两个函数特征适用于这里:
-
DETERMINISTIC
和NOT DETERMINISTIC
特征指示函数是否总是对给定输入产生相同的结果。默认情况下,如果不指定特征,函数将被认为是NOT DETERMINISTIC
。要声明函数是确定的,您必须明确指定DETERMINISTIC
。 -
以下四个特性:
CONTAINS SQL
、NO SQL
、READS SQL DATA
和MODIFIES SQL DATA
,提供了关于函数是否读取或写入数据的信息。既可以指定NO SQL
或READS SQL DATA
,表示函数不改变数据,但必须显式指定,因为缺省是CONTAINS SQL
如果不指定特性。
默认情况下,对于
CREATE FUNCTION
语句被接受,至少需要指定一个DETERMINISTIC
、NO SQL
或READS 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;
-
过程调用可以在提交或回滚事务中发生。事务上下文将被考虑,以确保过程执行的事务性方面在副本上正确地复制。也就是说,服务器将记录在过程中实际执行并修改数据的语句,并且也将记录
BEGIN
、COMMIT
和ROLLBACK
语句,以便在副本上执行相同的操作。例如,如果过程在提交事务中执行,那么BEGIN
和COMMIT
语句将被记录,以便在副本上执行相同的更新。对于在回滚事务中执行的过程,它们的语句将使用相同的规则记录,以便在独立执行时执行:-
对事务表的更新不被记录。
-
对非事务表的更新被记录,因为回滚不取消它们。
-
对混合的事务和非事务表的更新被记录,以便在副本上执行相同的操作和回滚。
-
-
-
存储过程调用在不在语句级别写入到二进制日志中,如果该过程从存储函数中被调用。在这种情况下,只有调用函数的语句被记录(如果它在被记录的语句中出现)或一个
DO
语句(如果它在不被记录的语句中出现)。因此,在使用存储函数调用过程时,需要小心,即使该过程本身是安全的。