2.7 复制和并发
并发问题的另一个重要场景是在复制环境下。当排查复制问题的时候,需要记住主服务器总是多线程的,而从服务器在单个线程中执行所有更新[2]。这会影响复制时的性能和一致性,而与你使用的二进制日志格式和选项无关。与复制相关的问题主要为数据不一致性,也就是从服务器上的数据与主服务器上的数据不同。在大多数情况下,MySQL复制会小心处理数据一致性问题,不过你仍有时候会遭遇一致性问题,特别是在你使用了基于语句的复制的时候。本节重点关注一致性问题,在最后会简要介绍复制是如何影响性能的。2.7.1 基于语句的复制问题从5.1版开始,MySQL开始支持三种二进制日志格式:语句、行和混合日志。数据的不一致性问题大多由基于语句的复制(Statement-Basement Replication,-SBR)引起,并且其使用是语句的二进制日志格式。这种格式与基于行的日志相比有很多优势,并且是历史上唯一支持日志变化的格式,因此其用户群非常庞大而且目前仍然是默认配置。然而,它比基于行的日志格式有更多的风险。
提示
基于行的日志在日志中记录原始数据。因此从服务器不会与主服务器执行相同的查询,而是直接更新表中的行数据。这是最安全的日志格式,因为它不可能在从服务器中插入任何主服务器上不存在的数据。该日志类型甚至可以在调用如NOW()等不确定的函数的时候保证安全。基于语句的日志是在日志中记录原始的SQL语句,因此从服务器与主服务器执行相同的命令。如果你使用该格式,网络流量通常(虽然不总是如此)比基于行的日志低,因为查询占用的数据会比实际插入的数据小。这在批量更新表中的BLOB列的时候尤为显著。该格式的缺点就是需要保证数据的一致性。例如,如果向列中插入了NOW()函数的返回结果,你很可能会在主从服务器中保存不同的数据。混合二进制日志综合了基于行和语句的日志的优点:它用语句格式保存大多数执查询,并且当查询不安全的时候改用基于行的格式,即,当你使用了如NOW()等不确定函数的时候。
当你的应用程序计划使用MySQL复制的时候,需要注意不同的语句对一致性的影响。甚至当主服务器给从服务器提供很多额外信息的时候,后者可能无法处理所有问题。
MySQL参考手册包含很多在使用基于语句的复制时不安全的语句列表,也就是说使,用这些语句会使主从服务器间结果不同。Charles Bellet等(O'Reilly)编写的《高可用MySQL》一书中详细介绍了复制时产生的一致性问题。这里重点介绍与并发相关的问题,如果语句在主服务器的很多线程中执行并且按照特定的顺序在从服务器上执行,则该并发问题可能会导致不同的结果。
由于主服务器是多线程的,那么它会按照一个不确定的顺序执行多个连接请求。但是在从服务器上,目前还是单线程的,那么按照日志记录的顺序重新执行语句就有可能与之前在主服务器上执行的顺序不同。这就有可能导致主从服务器的不同。
多线程从服务器
复制团队现在正在开发多线程的从服务器。该增强特性目前为预览版。多线程从服务器可以将事务分发到不同的线程中。可以通过slave_parallel_workers变量调整使用的线程数量。该特性会影响复制的的扩展性,也就是数据的更新速度,因此它会避免从服务器滞后过多而使一致性受到威胁。不过不要指望它会彻底解决一致性问题。你可以在Luis Soares的博客中查到关于多线程从服务器的更多信息。为了举例说明顺序问题,我将使用只有一个字段且没有唯一索引的简单表。在实际项目中,类似的情况即发生在当你查询搜索没有使用的唯一索引的行的时候。
例2.1 由于错误的事务顺序引发的复制问题的示例
现在我要模拟并发事务在批量插入行时的情况:
注意,该事务目前还没提交。接下来,我将在另外一个连接中打开一个新事务,然后插入另一些行:
第二个事务已经提交了。现在我将提交第一个事务:
你可能预期表中将会有以1~5开头的数据,然后是a~e。这绝对是正确的,因为第一个事务于第二个事务开始:
但是在事务提交前,主服务器没有向二进制日志中写入任务数据。因此,从服务器中的数据将以a~e开始,而1~5在第二个集合中:
目前为止还算正常:尽管顺序不同,不过主从服务器仍包含相同的数据。但是当执行不安全的UPDATE时,就会变得不正常了:
如你所见,服务器警告我们查询是不安全的。我们来看看原因。在主服务器上,结尾是:
然而,在从服务器上的数据集是完全不同的:
这个示例非常简单。在现实生活中,混乱的情况通常复杂得多。在这个示例中,我使用了事务性存储引擎并且使用多语句事务来简单地重现一个错误结果。在非事务的存储引擎中,当你使用的MySQL扩展功能延迟了实际数据变化的时候,你可能会看到类似的问题,例如:INSERT DELAYED。
解决这种问题最好的方式就是使用基于行的复制或者混合复制。如果你坚持使用基于语句的复制,那么绝对有必要好好设计应用程序来避免这种情况。请时刻牢记,每个事务都只会在其提交的时候向二进制日志中写入数据。你也需要使用第1章介绍的技术去检查警告。如果在生产环境中对每个查询都执行该检查听起来很困难(例如,出于性能考虑),那么至少在开发阶段执行该检查。
2.7.2 混合事务和非事务表
一件很重要的事情就是不要把事务和表非事务的表混合在同一个事务中[3]。一旦修改非事务表,就不能回滚它们,因此如果事务中止或者回滚,数据会变得不一致。 警告当混合事务表在同一个事务中使用不同的引擎时会产生同样的情况。如果引擎有不同的事务隔离等级或者对会引起隐式提交的语句有不同的规则,你会遭遇与刚才介绍的混合使用有事务表和无事务表相类似的问题。甚至当你使用单独的MySQL服器务的时候也有可能发生这个问题,但是在复制的环境中有时候事情会变得更糟。所以我将本节内容也包含在复制相关的章节里,尽管它也包含无复制环境的内容。对该示例来说,我们将展示一个使用与1.5节中的示例相同概念的存储过程。这里把临时表改为持久表并且添加另外一个存储过程来填充t2表中的行。t2表也将使用MyISAM存储引擎。MyISAM是5.5版本之前的默认存储引擎,因此如果用户忘记在CREATE TABLE语句中设置ENGINE选项,那么这样的临时表很容易创造。用户也有可能想提高性能而错误地使用了MyISAM引擎。
如你所见,在主服务器上有三行。查看从服务器上有几行数据:
从服务器实际上没有数据。这是由于主服务器向事务缓存中写入对无事务表的更新。但是,如我们之前所见,缓存内容仅在事务实际上结束后才会写入二进制日志。在这种情况下,可能相比主服务器,从服务器实际上更符合用户的意图(因为主服务器在t1中拥有一个对应一个永远不会完成事务的“幽灵”条目),不过关键是我们由于非事务的表而最终造成了数据不一致性。
提示
3.9.1节讨论binlog_direct_non_transactional_updates选项,该选项控制何时对非事务表的更新会写入二进制日志。因此,不要把事务表和非事务表混合在一个事务中。如果确实有必要这么做,那么使用另外的锁定方法,如LDCK TABLE,以保证在回滚或崩溃时的一致性。
可以使用基于行的日表或者混合二进制日志解决大多数与复制相关的问题,不过如果主服务器上的数据都不是你想要的那么这些都没有用了。
2.7.3 从服务器上的问题
我们刚才仅讨论了由于主服务器是多线程的而从服务器是单线程的引发的并发问题。如果仅从服务器上的SQL线程在运行,那么在从服务器上不会有任何额外的并发问题。不过在现实生活中,除从主服务器复制外,从服务器还完成其他的任务,因此从服务器上的SQL线程所遇到并发问题就跟其他连接线程遇到的一样。当从服务器上的SQL线程向正在被其他线程使用的表中写入数据的时候,跟其他多线程场景一样,它需要获取这些表的所有锁。可以通过执行SHOW PROCESSLIST命令,看到这些信息:
执行下面的查询以展现SQL线程是如何等待其他线程完成后才可以执行更新的:
在得到SHOW PROCESSLIST命令的输出后,我回滚了并行查询,因此该SQL线程可以成功地完成查询:
你也可能遭遇到从服务器上的SQL线程持有你的应用程序想要获取的行锁的情况。如果想要弄清何时发生了这样的情况,可以检查SHOW PROCESSLIST和SHOW ENGINE INNODB STATUS的输出。这两种情况的统一解决方案就是要么等待活动的线程结束,要么在用户事务等待过久的情况下回滚事务。