为什么这个基于CASE的更新会导致复制偏差?

时间:2014-04-18 03:14:33

标签: mysql stored-procedures database-replication sqldatatypes

在单个主服务器和从服务器之间使用MySQL 5.6和基于语句的复制,以下方案会创建复制偏差:

创建此表:

CREATE TABLE `ReplicationTest` (
  `TestId` int(11) NOT NULL,
  `Tokens` int(11) NOT NULL,
  PRIMARY KEY (`TestId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1$$

插入此数据:

Insert into ReplicationTest (TestId, Tokens) VALUES
(1, 0),
(2, 0),
(3, 0),
(4, 0);

创建此过程:

CREATE PROCEDURE `MyDatabase`.`ReplicationTestProc` (vTestId int, pSuccessful BIT, pAmount DECIMAL(19, 4))
BEGIN

START TRANSACTION;

Update MyDatabase.ReplicationTest Set Tokens = CASE WHEN pSuccessful THEN Tokens - (pAmount * 100) ELSE Tokens END
where TestId = vTestId;

COMMIT;

END

在一次执行中运行这4个语句

call `MyDatabase`.`ReplicationTestProc`(1, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(2, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(3, 1, 1);
call `MyDatabase`.`ReplicationTestProc`(4, 0, 1);

现在,您将在主服务器和复制之间的ReplicationTest表中具有不同的值。看起来pSuccessful变量被视为全局变量,最后一个值设置为复制服务器上使用的值。

1 个答案:

答案 0 :(得分:4)

通过观察实际记录在二进制日志中的语句,您可以了解发生了什么。使用以下命令查看binlog:

$ mysqlbinlog <datadir>/mysql-bin.000001

(当然,包含这些语句的文件名在您的系统上可能有所不同。)

被调用过程中的语句被记录到二进制日志中,但稍微修改了基于一个过程变量的常量值。这在http://dev.mysql.com/doc/refman/5.6/en/stored-programs-logging.html

中有解释
  

要记录的语句可能包含对本地过程变量的引用。这些变量不存在于存储过程上下文之外,因此无法从字面上记录引用此类变量的语句。相反,每个对局部变量的引用都会被此构造替换以用于记录目的。

以下是我们在测试的binlog中看到的内容。对局部变量的引用已替换为BIT值的字符串表示

. . .
Update test.ReplicationTest Set Tokens = CASE 
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary') 
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId =  NAME_CONST('vTestId',1)
. . .
Update test.ReplicationTest Set Tokens = CASE 
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary') 
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId =  NAME_CONST('vTestId',2)
. . .
Update test.ReplicationTest Set Tokens = CASE 
WHEN NAME_CONST('pSuccessful',_binary'' COLLATE 'binary') 
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId =  NAME_CONST('vTestId',3)
. . .
Update test.ReplicationTest Set Tokens = CASE 
WHEN NAME_CONST('pSuccessful',_binary'\0' COLLATE 'binary') 
THEN Tokens - ( NAME_CONST('pAmount',1.0000) * 100) ELSE Tokens END
where TestId =  NAME_CONST('vTestId',4)
. . .

请注意BIT值1如何替换为字符串文字_binary''

当你的表达式求值为true / false时,空字符串等于false,这就是语句不更新slave上的列的原因:

mysql> select false = _binary'';
+-------------------+
| false = _binary'' |
+-------------------+
|                 1 |
+-------------------+

为什么将BIT值1替换为空字符串? BIT值本质上是一种字符串,就像使用BINARY数据类型一样。下面是一个BIT值的示例,该值对应于'Z'的ASCII代码,表示为'Z':

mysql> select b'01011010';
+-------------+
| b'01011010' |
+-------------+
| Z           |
+-------------+

BIT如何处理b'1'的价值?它就像一个ASCII值0000001,它打印为一个不可打印的零长度字符串:

mysql> select b'1';
+------+
| b'1' |
+------+
|     |
+------+

您可以通过在整数上下文中引用它来强制将此值转换为整数:

mysql> select b'1'+0;
+--------+
| b'1'+0 |
+--------+
|      1 |
+--------+

但即使我们要改变你的存储过程来使用这个技巧,也为时已晚。该值已在二进制日志中转换为空字符串_binary'',并且该字符串在整数上下文中不能转换为除0之外的任何值。

mysql> select _binary''+0;
+-------------+
| _binary''+0 |
+-------------+
|           0 |
+-------------+

为什么BIT值在二进制日志中不像真正的b'1'字面值那样转换为BIT?这似乎是故意的。存储过程的二进制日志代码中甚至有注释(文件sql / sp.c,函数sp_get_item_value())。

/* Bit type is handled as binary string */

我建议您忘记在MySQL中使用BIT作为布尔值。 MySQL BIT数据类型的意外行为有太多情况。有关详情,请参阅Why you should not use BIT columns in MySQL

相反,使用BOOLEANTINYINTBOOLEANBOOL实际上只是MySQL中TINYINT(1)的别名。我测试了您的过程,参数更改为TINYINT,并且按预期工作:

slave1 [localhost] {msandbox} (test) > select * from ReplicationTest;
+--------+--------+
| TestId | Tokens |
+--------+--------+
|      1 |   -100 |
|      2 |   -100 |
|      3 |   -100 |
|      4 |      0 |
+--------+--------+