如何改进 INSERT INTO ... SELECT 锁定行为

Posted

技术标签:

【中文标题】如何改进 INSERT INTO ... SELECT 锁定行为【英文标题】:How to improve INSERT INTO ... SELECT locking behavior 【发布时间】:2011-02-08 02:43:08 【问题描述】:

在我们的生产数据库中,我们每小时运行以下伪代码 SQL 批处理查询:

INSERT INTO TemporaryTable
    (SELECT FROM HighlyContentiousTableInInnoDb
     WHERE allKindsOfComplexConditions are true)

现在这个查询本身不需要很快,但我注意到它锁定了HighlyContentiousTableInInnoDb,即使它只是从中读取。这使得其他一些非常简单的查询需要大约 25 秒(这是其他查询需要多长时间)。

然后我发现在这种情况下 InnoDB 表实际上是被 SELECT 锁定的! https://www.percona.com/blog/2006/07/12/insert-into-select-performance-with-innodb-tables/

但我不太喜欢文章中选择进入 OUTFILE 的解决方案,这似乎是一种 hack(文件系统上的临时文件看起来很糟糕)。还有其他想法吗?有没有办法制作 InnoDB 表的完整副本,而不会在复制过程中以这种方式锁定它。然后我可以将HighlyContentiousTable 复制到另一个表并在那里进行查询。

【问题讨论】:

我这里没有问,但是我还没有找到方法。我正在使用 outfile 来防止我的查询需要 20 分钟的锁定时间:) 有谁知道这个问题在 mysql 5.1 中是否真的如文章所暗示的那样得到了解决? 不,MySQL 5.1.44——同样的问题 请提供SHOW CREATE TABLE TemporaryTable;可能有一些事情不必要地延长了锁定时间。另外,让我们看看条件和SHOW CREATE TABLE HighlyContentiousTableInInnoDb;有办法显着提高SELECT 的速度。 @Ryan,您必须在赏金消息中输入这么多文本这一事实是一个强烈的指标,您应该创建一个新问题(并可能链接到此问题以供参考)。可通过right there in the manual 获取分步过程。 【参考方案1】:

这个问题的答案现在容易多了: - 使用基于行的复制和已提交读隔离级别。

您遇到的锁定消失了。

更多解释:http://harrison-fisk.blogspot.com/2009/02/my-favorite-new-feature-of-mysql-51.html

【讨论】:

我注意到在使用子选择的更新查询上使用时,执行时间减少了 50% 以上,不错的奖励。 刚刚在这个问题上添加了 +50 赏金,以获得上述问题的更详细、逐步的答案。【参考方案2】:

你可以这样设置binlog格式:

SET GLOBAL binlog_format = 'ROW';

编辑 my.cnf 如果你想永久化:

[mysqld]
binlog_format=ROW

在运行查询之前为当前会话设置隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
INSERT INTO t1 SELECT ....;

如果这没有帮助,您应该尝试设置服务器范围的隔离级别,而不仅仅是当前会话:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

编辑 my.cnf 如果你想永久化:

[mysqld]
transaction-isolation = READ-UNCOMMITTED

您可以将 READ-UNCOMMITTED 更改为 READ-COMMITTED,这是一个更好的隔离级别。

【讨论】:

要检查你当前的隔离级别,你可以运行SELECT @@TX_ISOLATION; 我已经为此奋斗了很多次,如果依靠它有缺陷的内部性能,必须在 mysql 中复制 TB 大的表可能需要数周才能完成。您需要使用 20-50 个线程来完成。但是即使在随机访问行时我也遇到了死锁,所以我不得不检查表 2-3 次。 “SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED”终于做到了,不再出现死锁,也不需要随机化。【参考方案3】:

使用 Innodb 表的每个人都可能已经习惯了 Innodb 的事实 表执行非锁定读取,这意味着除非您使用一些 LOCK IN SHARE MODE 或 FOR UPDATE、SELECT 语句等修饰符 运行时不会锁定任何行。

这通常是正确的,但是有一个明显的例外 - INSERT INTO table1 SELECT * FROM table2。该语句将对 table2 表执行锁定读取(共享锁)。它也适用于带有 where 子句和连接的类似表。对于被读取的表来说,它是 Innodb 很重要——即使写入是在 MyISAM 表中完成的。

那么为什么要这样做,这对 MySQL 性能和 并发?

原因是——复制。在 MySQL 5.1 之前的复制是基于语句的,这意味着在主服务器上回复的语句应该与从服务器上产生相同的效果。如果 Innodb 不会锁定源表中的行,则其他事务可以在运行 INSERT .. SELECT 语句的事务之前修改行并提交。这将使该事务在 INSERT...SELECT 语句之前应用于从属,并可能导致与主控不同的数据。在读取源表中的行时锁定行可以防止这种影响,因为其他事务在 INSERT ... SELECT 有机会访问它之前修改行,它也将在从属上以相同的顺序进行修改。如果事务在被访问并被 INSERT ... SELECT 锁定后试图修改该行,事务将不得不等到语句完成以确保它将以正确的顺序在从属服务器上执行。变得相当复杂?好吧,您只需要知道它必须在复制之前完成才能在 5.1 之前的 MySQL 中正常工作。

在 MySQL 5.1 中,这个以及其他一些问题应该通过基于行的复制来解决。然而,我还没有给它真正的压力测试,看看它的表现如何:)

还有一点需要注意——INSERT ... SELECT 实际上在锁定模式下执行读取,因此部分绕过版本控制并检索最新提交的行。所以即使你在 REPEATABLE-READ 模式下操作,这个操作也会在 READ-COMMITTED 下执行 模式,与纯 SELECT 给出的结果相比,可能会给出不同的结果。顺便说一句,这也适用于 SELECT .. LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。

我的一个问题是,如果我不使用复制并且禁用了我的二进制日志会怎样?如果不使用复制,您可以启用 innodb_locks_unsafe_for_binlog 选项,这将放松 Innodb 在语句执行时设置的锁,这通常会提供更好的并发性。但是,顾名思义,它会使锁在前复制和时间点恢复中变得不安全,因此请谨慎使用 innodb_locks_unsafe_for_binlog 选项。

注意禁用二进制日志不足以触发宽松锁。你 还必须设置 innodb_locks_unsafe_for_binlog=1 。这是这样做的 启用二进制日志不会导致锁定的意外更改 行为和性能问题。您也可以将此选项与 有时复制,如果你真的知道你在做什么。我会 除非真的需要,否则不推荐,因为您可能不知道 在未来的版本中将放宽哪些其他锁以及如何放宽 影响您的复制。

【讨论】:

链接到源? percona.com/blog/2006/07/12/…【参考方案4】:

免责声明:我对数据库不是很有经验,我不确定这个想法是否可行。如果不是,请纠正我。

如何设置第二个等效表HighlyContentiousTableInInnoDb2,并在第一个表中创建AFTER INSERT 等触发器,以使新表使用相同的数据进行更新。现在您应该能够锁定HighlyContentiousTableInInnoDb2,并且只减慢主表的触发器,而不是所有查询。

潜在问题:

2 x 数据存储 所有插入、更新和删除的额外工作 可能不适合交易

【讨论】:

【参考方案5】:

如果您可以允许一些异常,您可以将隔离级别更改为最不严格的级别 - 未提交阅读。但是在此期间,允许某人从您的目标表中读取。或者您可以手动锁定目标表(我假设 mysql 正在提供此功能?)。

或者您也可以使用 READ COMMITTED,它也不应该锁定源表。但它也会锁定目标表中插入的行直到提交。

我会选择第二个。

【讨论】:

这是一个有趣的方向。 dev.mysql.com/doc/refman/5.0/en/set-transaction.html 无论如何,目标表是一个临时(非复制)表,所以我认为 READ COMMITTED 是要走的路。我想试试这个。 我现在已经试过了,它似乎没有问题!我现在做: SET TRANSACTION ISOLATION LEVEL READ COMMITTED; INSERT INTO TemporaryTable SELECT ... FROM HighContentiousTableInInnoDb;这不会锁定高度竞争表InnoDb。与 SELECT INTO OUTFILE 方法相比,我不知道使用它有什么缺点。我没有复制这个 TemporaryTable,所以我认为我应该没有问题。【参考方案6】:

也许您可以使用创建视图命令(请参阅Create View Syntax)。 例如,

Create View temp as SELECT FROM HighlyContentiousTableInInnoDb WHERE allKindsOfComplexConditions are true

之后,您可以在此视图中使用您的插入语句。 像这样的

INSERT INTO TemporaryTable (SELECT * FROM temp)

这只是我的建议。

【讨论】:

这真的有效吗?我认为 View 会做同样的工作...... 如果您使用视图编辑/读取字段,您的 DBMS 必须锁定字段以及直接访问它们。唯一的区别是它不锁定整行(包括所有列),而只锁定视图使用的列。如果您的交易使用不相交的列,那么这确实可以帮助您。 (谁他妈给了这个答案-1?) VIEW 只是SELECT 周围的语法糖;没有性能提升。【参考方案7】:

我对 MySQL 不熟悉,但希望有一个等效于 SQL Server 中的事务隔离级别 SnapshotRead committed snapshot。使用其中任何一个都可以解决您的问题。

【讨论】:

mysql中没有快照【参考方案8】:

锁定(readlock)的原因是为了保护您的读取事务不读取并行事务当前可能正在写入的“脏”数据。 大多数 DBMS 提供了用户可以手动设置和撤销读写锁的设置。如果在您的情况下读取脏数据不是问题,这对您来说可能会很有趣。

我认为在具有多个事务的 DBS 中没有任何锁的情况下从表中读取数据是不安全的。

但以下是一些头脑风暴: 如果空间没有问题,您可以考虑运行同一张表的两个实例。 HighlyContentiousTableInInnoDb2 用于您的持续读/写事务,HighlyContentiousTableInInnoDb2_shadow 用于您的批量访问。 也许您可以通过 DBMS 中的触发器/例程自动填充影子表,这比任何地方的额外写入事务更快、更智能。

另一个想法是问题:是否所有事务都需要访问整个表? 否则,您可以使用视图仅锁定必要的列。如果连续访问和您的批量访问在列方面不相交,则它们可能不会相互锁定!

【讨论】:

【参考方案9】:

我在使用 CREATE TEMPORARY TABLE ... SELECT ...SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction 时遇到了同样的问题。

根据您的初始查询,我的问题已通过在开始查询之前锁定HighlyContentiousTableInInnoDb 得到解决。

LOCK TABLES HighlyContentiousTableInInnoDb READ;
INSERT INTO TemporaryTable
    (SELECT FROM HighlyContentiousTableInInnoDb
    WHERE allKindsOfComplexConditions are true)
UNLOCK TABLES;

【讨论】:

以上是关于如何改进 INSERT INTO ... SELECT 锁定行为的主要内容,如果未能解决你的问题,请参考以下文章

Postgresql中无则插入的使用方法INSERT INTO WHERE NOT EXISTS

如何在oracle中 insert into 多条记录

access SQL中如何实现“INSERT INTO”

insert into 插入数据问题

如何解决“INSERT INTO 语句中的语法错误”

如何将sqlserver表中的数据导出sql语句或生成insert into语句