Global transaction IDs (GTID)

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($mysqliMYSQLND_MS_QOS_CONSISTENCY_SESSIONMYSQLND_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_gtidfetch_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
        }
    }
}