该篇文章来源于线上案例结合官方文档翻译和自己理解。
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)
这个函数定义中没有包含deterministic,no sql或者read sql data中的一种,并且binlog是开启的。
很显然是业务创建函数的时候触发了该错误,那为何会导致这种错误呢,这是由于mysql复制要保证主从数据的一致决定的,这些函数创建语句会被记录在binlog中,然后复制到slave执行。但是这些函数在slave上执行的时候有可能导致主从数据不一致,为了避免该问题,mysql拒绝直接创建函数,创建函数失败,返回异常。那为何mysql会有这个机制,对于函数的创建又将如何处理?
在一些场景下,一条语句在分别在主从执行可能导致不同的结果,在slave执行复制语句是通过sql线程执行的,而SQL线程是有所有的权限,因此有可能出现一种情况就是,函数中有危险语句在master上执行并不会有问题,但是当slave的SQL线程权限不同的时候,就会执行到这些危险语句,从而导致主从数据不一致。如果一个函数更改数据的结果是不确定的,或者是不可重复的,也会导致主从数据不一致,或者导致更改的数据和原始数据不一致(这主要在备份恢复中出现)。
通常这些问题出现在复制是语句模式的情况下,如果使用的行模式,binlog记录的是执行SQL语句影响到的具体的行(不是执行的SQL语句),当routines或者触发器执行,binlog中记录的也是更改行信息,而不是影响行变更的SQL语句,对于存储过程也是一样,并不是记录call 语句,也是记录更改的行记录。对于函数,日志记录的是函数更改的行记录,而不是函数调用语句。对于触发器,记录的是触发器更改之后的行记录。因此在slave这边,看的是变更之后的行记录,而不是这些子程序的调用语句。因此在行模式则不会导致主从不一致。
如果复制模式是混合模式,除非行模式能保证正确的结果,不然上面的结果记录到binlog采用的是语句模式。在混合模式下,当一个存储过程,函数,触发器,事件包含了对于语句模式不安全的SQL,这些语句就会标记为不安全的并且采用行模式记录在binlog。
在mysql中,下面有一些条件是对函数有效,对存储过程或者事件无效,或者没有开启binlog也是无效的:
1.创建或者更改一个函数必须要有super权限
2.创建一个函数,必须要定义为确定结果的或者是不更改数据的。否则,就会被认为是对复制或者数据恢复是不安全的,也就是报错1418
默认情况下,要创建一个函数,deterministic,no sql,reads sql data中三个属性中的一个必须被显示指定,这样就能确认函数对结果集的影响,否则就会报错1418,函数创建不成功。
下面这个函数就是确定结果的,因此是可以创建成功的:
CREATE FUNCTION f1(i INT)
RETURNS INT
DETERMINISTIC
READS SQL DATA
BEGIN
RETURN i;
END;
下面函数使用了uuid(),这个函数的结果是不确定的,因此下面函数是非确定结果的,是复制不安全的,因此创建失败:
CREATE FUNCTION f2()
RETURNS CHAR(36) CHARACTER SET utf8
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并不会检查一个函数定义为确定结果但实际上产生了不确定的结果。在函数的定义中可以指定deteministic来显示的说明函数是安全的,但是在函数体中定义可以使用不安全的语句。
这种情况,mysql会认为是安全的,可以创建函数,但实际上这种函数调用对主从数据可能导致不一致。
如果试图执行一个函数,若binlog_format 设置为statement模式,这个函数属性必须要显示指定为deterministic才行执行,否则就会报错1418异常并且函数不会被执行。但是设置了log_bin_trust_function_creators = 1,则可以正常执行。
比如:
先set global log_bin_trust_function_creators =1 创建了一个没有指定deterministic的函数:
CREATE FUNCTION f2()
RETURNS CHAR(36) CHARACTER SET utf8
BEGIN
RETURN UUID();
END;
再set globallog_bin_trust_function_creators = 0;set binlog_format = statement;
然后调用该函数
mysql> select f2();
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)
调用函数失败,报错1418。
如果设置binlog_format=mixed 或者row 模式或者set globallog_bin_trust_function_creators = 1 ,select f2()则可以正常执行了(创建函数的时候没有指定determinisric 关键字),或者在函数定义中指定deterministic属性,也是可以正常执行的。
因为mysql并没有检查一个函数在创建的时候是否是正是确定结果的,因此调用一个指定了deterministic关键字的函数在statement模式下可能带来的结果是不安全的,因为这样的函数中可能包含不安全的语句。在statement模式下,调用这种函数会触发warning,如果是mixed或者row模式,不会有warning,函数中语句会以row模式进行复制。如下:
创建函数的时候明确指定deterministic属性
CREATE FUNCTION f3(p_id INT)
RETURNS INT
deterministic
BEGIN
UPDATE t1 SET x=1 WHERE x = p_id;
RETURN ROW_COUNT();
END;
这个函数不管log_bin_trust_function_creators 设置为多少都能创建成功,因此指定的结果是确定的。
在format_format=statement模式下调用:
mysql> select f3(1)//
+-------+
| f3(1) |
+-------+
| 0 |
+-------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings//
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1592 | Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it uses a system function that may return a different value on the slave. |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
产生了warning,提示语句在statement模式下是不安全的(和log_bin_trust_function_creators 设置无关,设置1或者0,都会有warning产生)
在binlog_format = row模式下调用:
mysql> set binlog_format=row//
Query OK, 0 rows affected (0.00 sec)
mysql> select f3(1)//
+-------+
| f3(1) |
+-------+
| 0 |
+-------+
1 row in set (0.00 sec)
并不会提示warning,在mixed模式下也是一样,不会有warning,都是以row模式进行binlog记录。
可以看出,即使我们创建函数的时候绕过了mysql的检查,成功创建了函数,但是在调用的时候,mysql还是会根据binlog_format来确认结果,从而选择是row模式,还是statement,并作出提示。
为了避开创建函数的检查条件,可以设置log_bin_trust_function_creators = 1,这样mysql就不会进行检查了,默认设置0,这个参数只能设置global 级别。也可以在server启动的时候加上--log_bin_trust_function_creators =1选项。如果binlog没有开启,log_bin_trust_function_creators 参数也就没作用。
触发器和函数类似,因此前面讲的关于函数的说明同样对触发器有效,除了下面这个:create trigger语句没有deterministic属性,因此触发器总是被假定为deterministic,但是在一些场景下这个假设会失效。例如,uuid()函数就是nondeterministic的(不能复制的),因此在使用这些函数尤其注意。触发器会更新表,因此create trigger语句没有更新表所需要的权限时,会和创建函数一样报异常。在slave端,slave通过触发器的definer属性来决定触发器的创建者,这就就决定了触发器所需的权限。
如果一个函数更新了数据 ,那么mysql会以select语句的形式记录调用方式,这就避免了数据更新无法记录日志从而使得无法复制。一般情况下,select语句是不会记录子binlog中的,但是一个select语句有可能调用一个函数
导致数据变更。为了解决这种方式,当select使用的函数更新了数据,那么函数调用方式就以select语句的形式记录在binlog中(这里有个前提,binlog_format=statment。row和mixed不存在这个问题)
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;
在binlog中可以发现是以select语句记录binlog的:
如果是row或者mixed模式,则都是以row模式进行binlog记录。
当一个函数中调用一个存储过程的时候发生了错误,这样mysql同样会以select语句的形式记录函数的调用,这种情况下,binlog中记录的select语句同时会记录
期望的error code,在slave端,如果同样的错误出现,那么这就是期望的结果同时复制不会中断,否则复制会中断。
binlog记录函数的调用方式而不是记录函数中的执行语句对复制是一种安全的结果,主要有两个方面的原因:
在master slave上函数的的调用路径可能不同,其次执行语句的SQL 线程有所有权限,和master可能不同,master可能没这么大的权限,但是slave上有,就有可能导致主从上执行结果不同。
这样的结果就是虽然一个用户需要create routine的权限来创建一个函数,用户可以写很危险的语句在函数中,而且只能在slave上通过有所有权限的SQL 线程来执行。例如,master slave有不同的server id 1 和2,一个用户可以在master上创建一个不安全的函数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());
函数创建语句和插入语句都会记录在binlog中,因此slave可以执行这些语句,因为SQL 线程有所有权限(往往主库创建和调用函数的用户权限比较有限),因此将会执行到这些危险语句,因此,在master slave
上这个函数调用产生来不一样的结果,所以它不是复制安全的。
为了避免开启binlog的mysql上的这种危险情况,函数的创建这必须有所有权限,不仅仅是必须的create routine权限。同样的,alter function也是一样。没有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权限,可以设置全局参数log_bin_trust_function_creators =1,或者在服务启动的时候增加参数--log_bin_trust_function_creatros =1,如果binlog没有开启,这个参数则没有作用。
如果一个函数更新数据是不确定的,是不可重复的,这会导致两个不良后果:
导致slave和master的数据不一致
恢复数据的时候导致和原始数据不同(这里主要是数据恢复时出现)
为了处理这些问题,mysql做如下强制要求:在master上,拒绝创建或者变更一个函数,除非定义的函数是确定结果的或者不更新数据的。这两个函数属性的作用如下:
deterministic 或者not deterministic属性决定一个函数对于给定的输入是否每次产生相同的结果,如果没有属性给定,默认是not deteministic的。定义一个函数是deterministic的,需要明确指定deterministic属性。
contains sql ,no sql,reads sql data和modify sql data属性指出一个函数是读数据还是更新数据,no sql 或者reads sql data明确一个函数是不更新数据的。如果不指定属性,默认是contains sql,如果一个函数是明确不更新数据的话,
需要特别指定no sql、reads sql data中的一种。默认情况下,要使得create function语句能否执行,至少需要deteministic ,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 ,则deterministic 或者不更新数据的属性被忽略。
调用存储过程,binlog记录是在语句执行阶段,而不是调用阶段。也就是说binlog记录的不是call 语句,而是存储过程中真正执行的语句。因此在master更新的数据在slave上同样被更新到,这就避免了同一个存储过程在主从上导致不一样的结果。
通常,binlog中记录的存储过程中执行的语句只要有同样的权限就可以在另外一台机器上被执行。有一个特殊场景需要注意:在非标准的上下文中存储过程的执行结果不完全相同:
一个语句记录在binlog中可以要包含关联一个本地的过程变量,这些变量在存储过程外部是不存在的,因此一个引用了变量的语句不能直接按照语句的原样进行记录binlog,而是为了写入binlog每一个被引用的变量用如下的结构进行替换:
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()函数使得binlog中记录在slave上执行和存储过程中的原始语句在 master上执行产生同样的效果。使用create table...select语句时候,当select 语句中当列表达式引用本地变量的时候,使用name_cons函数的时候会导致一些问题。转变这些引用为name_const表达式的过程中导致master和slave为不同的列名,或者name太长也不能作为一个合法的列标识。一种解决方式就是为列名提供一个别名引用本地变量。下面语句myvar 的值为1:
create table t1 select myvar;
记录在binlog中会被重写为如下语句:
create table t1 select name_const(myvar ,1);
为了确保master和slave有相同的列名,将语句写成如下方式:
create table t1 select myvar as myvar;
binlog中会被记录为如下:
create table t1 select name_const(myvar ,1) as myvar; 这样就能确保主从的列名都是一致的。
另外一条语句记录到binlog中可能包含引用用户定义的变量。为处理这个特点,mysql写入一个set 语句到binlog确保该变量在slave上和master上有相同的值。例如,一条语句引用了变量@my_var,该语句在binlog中会处理为如下语句,value就是master上@my_var变量的值:
set @my_var = value;
存储过程调用可以在包含commit或者rollback的事务中,事务的上下文会被记录下来,这样事务中的存储过程部分就能够在slave中正确的复制执行。也就是说,mysql记录存储过程中真实执行和更改数据的语句,同时必要的时候会记录begin,commit,rollback。例如,一个存储过程只更新事务表而且在一个事务中执行,如果被回滚了,那么这部分更新不会被记录在binlog中。如果一个存储过程调用在一个commit的事务中,更新前后会记录begin和commit语句。如果一个存储过程调用在一个rollback的事务中,这些被记录会以同样的规则被记录在binlog中,当其以独立的方式执行时这些语句都会被应用:
事务表的更新不会记录在binlog中
非事务表的更新会被记录在binlog中,因为rollback不会回滚掉非事务表的更新
如果同时更新事务表和非事务表,这些记录会前后会记录begin和rolleback语句,因此slave更新和回滚的记录就和master上更新和回滚的记录一致。
在statement复制模式下,如果一个函数调用了一个存储过程,binlog中不会记录call语句。在这种场景下,只有函数调用语句被记录(如果调用它的语句被记录在binlog)或者是一个do语句(如果调用它的数据没有记录),正因如此,在一个函数中调用存储过程需要小心,尽管存储过程自身是安全的。也就是说,在一个函数中调用存储过程,binlog中只会记录函数的调用,不会记录存储过程的调用。
说了这么多,就是为了说明mysql对于创建函数的一些限制,以及调用函数如何记录binlog,以及我们在日常使用函数过程中,应该注意什么。
再回到业务的报错,创建函数失败,函数中没有指定deterministic ,no sql,reads sql data 属性,从而导致报错,解决办法如下:
1.如果函数只是为了查询方便而创建的,不更改数据,那么可以指定reads sql data,deterministic属性中的一种即可
2.如果函数要更改数据,那么可以指定deterministic属性通过创建函数,但是为了确保复制安全,需要将binlog_format设置为row模式