添加主键时增加死锁。为什么?

时间:2016-01-03 13:34:15

标签: mysql sql database

首先,有一些必要的背景(拜托,请耐心等待)。我是一名使用MySQL进行持久性的Web应用程序的开发人员。我们通过为每个数据表创建审计跟踪表来实现审计日志记录。例如,我们可以为Customer实体提供以下表定义:

-- Data table definition.
CREATE TABLE my_database.customers (
  CustomerId INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  FirstName VARCHAR(255) NOT NULL,
  LastName VARCHAR(255) NOT NULL,
  -- More data columns, removed for simplicity.
  ...
);

-- Audit table definition in separate schema.
CREATE TABLE my_database_audittrail.customers (
  CustomerId INT(11) DEFAULT NULL,
  FirstName VARCHAR(255) DEFAULT NULL,
  LastName VARCHAR(255) DEFAULT NULL,
  -- More data columns.
  ...
  -- Audit meta data columns.
  ChangeTime DATETIME NOT NULL,
  ChangeByUser VARCHAR(255) NOT NULL 
);

如您所见,审计表只是数据表的副本加上一些元数据。请注意,审计表没有任何键。例如,当我们更新客户时,我们的ORM会生成类似于以下内容的SQL:

-- Insert a copy of the customer entity, before the update, into the audit table.
INSERT INTO my_database_audittrail.customers (
  CustomerId,
  FirstName,
  LastName,
  ...
  ChangeTime,
  ChangeByUser)
)
SELECT
  CustomerId,
  FirstName,
  LastName,
  ...
  NOW(),
  @ChangeByUser
FROM my_database.customers
WHERE CustomerId = @CustomerId;

-- Then update the data table.
UPDATE
  my_database.customers
SET
  FirstName = @FirstName,
  LastName = @LastName,
  ...
WHERE CustomerId = @CustomerId;

这已经足够好了。但是,最近,出于各种原因,我们需要将一个主键列添加到审计表中,将审计表定义更改为类似于以下内容:

CREATE TABLE my_database_audittrail.customers (
  __auditId INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  CustomerId INT(11) DEFAULT NULL,
  FirstName VARCHAR(255) DEFAULT NULL,
  LastName VARCHAR(255) DEFAULT NULL,
  ...
  ChangeTime DATETIME NOT NULL,
  ChangeByUser VARCHAR(255) NOT NULL
);

我们的ORM在更新数据表时生成的SQL尚未修改。这种变化似乎极大地增加了死锁的风险。有问题的系统是一个带有大量夜间批处理作业的Web应用程序。我们的网络用户在日常使用系统时未显示死锁的增加。然而,夜间批处理作业确实遭受了死锁,因为他们在一些数据库表上做了大量工作。我们的“解决方案”一直是添加一个重试死锁策略(几乎没有争议),虽然这似乎工作正常,但我非常希望理解为什么上述变化会增加死锁的风险(如果我们可以的话)以某种方式解决问题)。

更多信息:

  • 我们的夜间批处理作业在我们的数据表上执行INSERTS,UPDATES和DELETES。只对审计表执行INSERTS。
  • 我们在数据库事务中使用可重复的读隔离级别。
  • 在此次更改之前,我们在运行夜间批处理作业时没有看到任何一个死锁。

更新:选中SHOW ENGINE INNODB STATUS以确定死锁的原因并找到了:

*** WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `my_database_audittrail`.`customers` trx id 24972756464 lock mode AUTO-INC waiting

我认为自动增量是在任何交易之外处理的,以避免在不同交易中使用相同的自动增量值?但我想我们引入的主键上的AUTO_INCREMENT属性似乎是问题吗?

2 个答案:

答案 0 :(得分:0)

这是推测。

使用索引插入或更新到表中不仅会锁定数据页,还会锁定索引页,包括索引的更高级别。当多个线程同时影响记录时,它们可能会锁定索引的不同部分。

这通常不会显示单个记录插入。但是,更新多个记录的两个语句可能会开始获取索引上的锁,并发现它们相互死锁。重试可能足以解决此问题。或者,似乎"太多"可能正在运行,您可能想要考虑每晚更新工作的布局。

答案 1 :(得分:0)

当插入具有自动增量列的表时,MySQL使用不同的策略来获取自动增量列的值,具体取决于插入的类型,插入语句以及MySQL如何配置为处理自动增量列,插入可能会导致完整的表锁定。

使用"简单插入",即插入,MySQL可以预先确定将插入表中的行数(例如INSERT INTO表(col1,col2)VALUES(val1,val2); )使用自动增量计数器上的轻量级锁定获取自动增量列值。一旦获得自动增量值,就会释放这种轻量级锁定,因此不必等到实际插入完成。我没有桌子锁。

然而,使用"批量插入",其中MySQL无法确定手前插入的行数(例如INSERT INTO表(col1,col2)SELECT col1,col2 FROM table2 WHERE ...;)a创建表锁以获取自动增量列值,并且在插入完成之前不会放弃。

以上是MySQL的默认配置。可以将MySQL配置为不对批量插入使用表锁,但这可能导致自动增量列在主服务器和从服务器上具有不同的值(如果已设置复制),因此可能是也可能不是可行选项。