Note: 版本需求
从 1.2.0-alpha 版本开始客户端 GTID 可以使用,这个功能并不需要在同步集群中使用, 例如 MySQL Cluster。他用于异步集群,例如 MySQL 主从同步。
从 MySQL 5.6.5-m8 版本开始,MySQL 使用内置的 GTID,这需要 1.3.0-alpha 以后版本支持。
PECL/mysqlnd_ms 可以使用自己的 GTID 仿真,或者使用 MySQL 内置的 GTID。无论使用哪种方式, 对于使用服务级别来说都是一样的。他们的区别,在 concepts section 进行说明。
这里先使用插件内部的 GTID 模拟来展示如何使用服务端的副本。
概念和客户端模拟
GTID 是 slave 需要同步的 table 在 master 上基于这个 table 的一个计数器,每当事务提交他都会增加。 这个计数器有两个作用,如果 master 产生故障,他帮助数据库管理员确定使用最新的 slave 来 恢复新的 master。最新的 slave 就是那个数值最高的。应用可以使用 GTID 查询某一次写入, 是否已经在 slave 被同步。
插件可以在每次提交事务的时候,增加 GTID。当然这个 GTID 也可以让应用判断写操作是否同步。 这样就可以实现在 session 一致性服务级别中,不一定从 master 读取数据,也可以从已经同步 的 slave 中获取数据,从而减轻 master 的读负载。
客户端 GTID 模拟有一些限制,可以参考 concepts section 说明。在生产换金钟使用前,请细致全面的理解他的工作原理和概念。相关背景的支持, 不在本参考中进行说明。
首先在 master 建立一个计数器表,并且插入一条记录。插件并不会帮助你建立这个表, 数据库管理员需要帮助你操作。如果表不存在或者有问题,基于错误报告机制, 你可能得不到任何错误信息。
Example #1 在 master 创建计数器表
CREATE TABLE `trx` ( `trx_id` int(11) DEFAULT NULL, `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=latin1 INSERT INTO `trx`(`trx_id`) VALUES (1);
在插件的配置文件中,需要在 global_transaction_id_injection 章节中设定 on_commit 参数。一定要确认在 UPDATE 中使用的表明是可达的,例如:使用上一步创建的表, test.trx 要比 trx 更合适。 这一点非常重要,因为不同的数据库连接,可能的默认数据库选择并不相同。 并且确认,使用连接的用户,有权限对这个表执行 UPDATE 命令。
当 GTID 更新时,打开错误报告。
Example #2 Plugin config: SQL for client-side GTID injection
{ "myapp": { "master": { "master_0": { "host": "localhost", "socket": "\/tmp\/mysql.sock" } }, "slave": { "slave_0": { "host": "127.0.0.1", "port": "3306" } }, "global_transaction_id_injection":{ "on_commit":"UPDATE test.trx SET trx_id = trx_id + 1", "report_error":true } } }
Example #3 Transparent global transaction ID injection
<?php
$mysqli = new mysqli("myapp", "username", "password", "database");
if (!$mysqli)
/* Of course, your error handling is nicer... */
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
/* auto commit mode, transaction on master, GTID must be incremented */
if (!$mysqli->query("DROP TABLE IF EXISTS test"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
/* auto commit mode, transaction on master, GTID must be incremented */
if (!$mysqli->query("CREATE TABLE test(id INT)"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
/* auto commit mode, transaction on master, GTID must be incremented */
if (!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
/* auto commit mode, read on slave, no increment */
if (!($res = $mysqli->query("SELECT id FROM test")))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
var_dump($res->fetch_assoc());
?>
以上例程会输出:
array(1) { ["id"]=> string(1) "1" }
上面的范例运行 3 条语句在 master 上,他们都是在 autocommit 下执行,这样会引起 3 次 GTID 的增加。每次插件都会在执行语句以前,根据配置中的 UPDATE 设定 增加 GTID。
第四条语句,因为是 SELECT 语句,并不会在 master 上执行, 所以不会引发 master 增加 GTID。
Note: 基于 SQL 的 GTID 如何有效率的工作
在客户端通过 GTID 模拟在每个 SQL 执行的时候处理是很没有效率的做法。 这样做,是为了能够清楚的说明情况,而不是为了执行效率,不要在实际的 生产环境中这样使用。可以在本文中找到更有效率的做法。
Example #4 Plugin config: SQL for fetching GTID
{ "myapp": { "master": { "master_0": { "host": "localhost", "socket": "\/tmp\/mysql.sock" } }, "slave": { "slave_0": { "host": "127.0.0.1", "port": "3306" } }, "global_transaction_id_injection":{ "on_commit":"UPDATE test.trx SET trx_id = trx_id + 1", "fetch_last_gtid" : "SELECT MAX(trx_id) FROM test.trx", "report_error":true } } }
Example #5 Obtaining GTID after injection
<?php
$mysqli = new mysqli("myapp", "username", "password", "database");
if (!$mysqli)
/* Of course, your error handling is nicer... */
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
/* auto commit mode, transaction on master, GTID must be incremented */
if (!$mysqli->query("DROP TABLE IF EXISTS test"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
printf("GTID after transaction %s\n", mysqlnd_ms_get_last_gtid($mysqli));
/* auto commit mode, transaction on master, GTID must be incremented */
if (!$mysqli->query("CREATE TABLE test(id INT)"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
printf("GTID after transaction %s\n", mysqlnd_ms_get_last_gtid($mysqli));
?>
以上例程会输出:
GTID after transaction 7 GTID after transaction 8
应用可以通过插件获取最后一次写操作产生的 GTID。函数mysqlnd_ms_get-last-gtid() 通过在配置文件中 global_transaction_id_injection 章节中 定义的 fetch_last-gtid 方法,返回最后一次 写操作产生的 GTID。函数应该在 GTID 增加后调用。
不建议应用运行自己运行哪些可能产生风险的 SQL 语句,从而增加 GTID。并且,使用函数 可以轻松的将查询 GTID 迁移到其他应用中。例如,使用任何 MySQL 内置的 GTID。
这里展现了一个 SQL 语句获得了他的 GTID 或者比实际执行得到的 GTID 更大的数据。 在 SELECT 和 查询 GTID 之间,可能有其他的客户端执行 SQL 语句,从而增加了 GTID,所以获得的 GTID 可能比实际数据大。
Example #6 Plugin config: Checking for a certain GTID
{ "myapp": { "master": { "master_0": { "host": "localhost", "socket": "\/tmp\/mysql.sock" } }, "slave": { "slave_0": { "host": "127.0.0.1", "port": "3306" } }, "global_transaction_id_injection":{ "on_commit":"UPDATE test.trx SET trx_id = trx_id + 1", "fetch_last_gtid" : "SELECT MAX(trx_id) FROM test.trx", "check_for_gtid" : "SELECT trx_id FROM test.trx WHERE trx_id >= #GTID", "report_error":true } } }
Example #7 Session consistency service level and GTID combined
<?php
$mysqli = new mysqli("myapp", "username", "password", "database");
if (!$mysqli)
/* Of course, your error handling is nicer... */
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
/* autocommit 模式下,在 master 执行,用于增加 GTID */
if (!$mysqli->query("DROP TABLE IF EXISTS test") ||
!$mysqli->query("CREATE TABLE test(id INT)") ||
!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
/* 获取最后一次写入的 GTID */
$gtid = mysqlnd_ms_get_last_gtid($mysqli);
/* Session 一致性,尝试从 slave 读取,而不只从 master 读取 */
if (false == mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION, MYSQLND_MS_QOS_OPTION_GTID, $gtid)) {
die(sprintf("[006] [%d] %s\n", $mysqli->errno, $mysqli->error));
}
/* Either run on master or a slave which has replicated the INSERT */
if (!($res = $mysqli->query("SELECT id FROM test"))) {
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
}
var_dump($res->fetch_assoc());
?>
通过 mysqlnd_ms_get_last_gtid() 获取的 GTID 可以被用于 Session 一致性服务级别。通过 mysqlnd_ms_set_qos() 设定 Session 一致性服务级别,他决定从哪里读取写入的数据。在范例中, 通过判断 INSERT 是否已经被同步,来决定 SELECT 从哪个服务器中读取数据,
插件检查配置中的所有 slave 服务器,通过查看 GTID 表中的值,判断是否 INSERT 已经被同步。检查的方法在 global_transaction_id_injection 章节中,使用 check_for_gtid 参数定义。 请注意,这是一种低效,浪费资源的方法。 在 master 的读取压力很大的时候,应用可以零星的采用这种方式,来降低读取压力。
使用服务器端的 GTID
自从 MySQL 5.6.5-m8 版本开始,MySQL 主从同步开始支持服务器端的 GTID。GTID 的 创建和增长由服务器控制,用户可以不再关心这些问题。这也就是说,不需要再添加任何 数据库表用于记录 GTID,也不用设置 on_commit 方法。客户端模拟 的 GTID 不再需要使用。
客户端可以顺畅使用 GTID 完成 Session 一致性服务,运算的方式与上面描述的 GTID 模拟 是一样的。不同的是 check_for_gtid 和 fetch_last_gtid 还是需要进行配置。 请注意,MySQL 5.6.5-m8 是一个研发版本,具体执行细节在实际的运行版本对于这些功能可能有改变。
使用下面的配置,可以上上面讨论过的任何一个脚本,能够利用服务器端的 GTID 正常工作。 函数 mysqlnd_ms_get_last_gtid() 和 mysqlnd_ms_set_qos() 工作也一样正常。不同点在于, 服务器并不采用简单的顺序序列,而是采用一个包含服务器标识号和序列数字的字符串。 所以,用户并不能简单的通过 mysqlnd_ms_get_last_gtid() 得到的顺序判断 GTID。 译者注:从 MySQL 5.6.9 版本开始 GTID_DONE 已经被 GTID_EXECUTED 替代,所以下面的 范例中,应该做相应变更。
Example #8 使用 MySQL 5.6.5-m8 内置 GTID
{ "myapp": { "master": { "master_0": { "host": "localhost", "socket": "\/tmp\/mysql.sock" } }, "slave": { "slave_0": { "host": "127.0.0.1", "port": "3306" } }, "global_transaction_id_injection":{ "fetch_last_gtid" : "SELECT @@GLOBAL.GTID_DONE AS trx_id FROM DUAL", "check_for_gtid" : "SELECT GTID_SUBSET('#GTID', @@GLOBAL.GTID_DONE) AS trx_id FROM DUAL", "report_error":true } } }