如何避免mysql'尝试locking时发现死锁; 尝试重新启动交易“

我有一个innoDB表logging在线用户。 它会更新每个用户刷新的页面,以跟踪他们所在的页面以及他们上次访问网站的date。 然后我有一个每15分钟运行一次的cron删除旧logging。

我得到了一个“试图locking时发现的死锁; 尝试重新启动事务“昨天晚上大约5分钟,它似乎是运行INSERT到这个表中。 有人可以build议如何避免这个错误?

===编辑===

以下是正在运行的查询:

首次访问网站:

INSERT INTO onlineusers SET ip = 123.456.789.123, datetime = now(), userid = 321, page = '/thispage', area = 'thisarea', type = 3 

在每个页面刷新:

 UPDATE onlineusers SET ips = 123.456.789.123, datetime = now(), userid = 321, page = '/thispage', area = 'thisarea', type = 3 WHERE id = 888 

克伦每15分钟一class:

 DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND 

然后logging一些统计数据(即在线会员,在线访客)。

一个简单的技巧,可以帮助解决大多数死锁是按特定的顺序sorting操作。

当两个事务尝试以相反的顺序locking两个锁时,您会遇到死锁,即:

  • 连接1:锁键(1),锁键(2);
  • 连接2:locking键(2),locking键(1);

如果两者同时运行,则连接1将locking密钥(1),连接2将locking密钥(2),每个连接将等待另一方释放密钥 – >死锁。

现在,如果您更改了查询,以便连接将以相同的顺序locking键,即:

  • 连接1:锁键(1),锁键(2);
  • 连接2:locking键( 1 ),locking键( 2 );

将不可能陷入僵局。

所以这是我的build议:

  1. 确保除了删除语句之外,没有其他查询一次locking多个键的访问权限。 如果你这样做(我怀疑你这样做),按照(k1,k2,… kn)的顺序排列他们的WHERE。

  2. 修复您的删除语句以升序工作:

更改

 DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND 

 DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u; 

另外要记住的是,mysql文档build议,如果发生死锁,客户端应该自动重试。 您可以将此逻辑添加到您的客户端代码。 (说,放弃之前,这个特定的错误3次重试)。

当两个事务彼此等待获取锁时发生死锁。 例:

  • Tx 1:lockingA,然后B
  • Tx 2:lockingB,然后A

关于死锁有很多问题和答案。 每次插入/更新/删除一行时,都会获取一个锁。 为避免死锁,您必须确保并发事务不会以可能导致死锁的顺序更新行。 一般来说,即使在不同的事务中,也总是以相同的顺序获取锁 (例如总是首先是表A,然后是表B)。

数据库死锁的另一个原因可能是缺less索引 。 当一行被插入/更新/删除时,数据库需要检查关系约束,也就是确保关系是一致的。 为此,数据库需要检查相关表中的外键。 这可能会导致其他锁获取比被修改的行。 请确保始终在外键(当然还有主键)上有索引,否则可能会导致表锁而不是行锁 。 如果发生表锁,则锁争用较高,死锁的可能性增加。

删除语句可能会影响表中总行数的很大一部分。 最终,这可能会导致在删除时获取表锁。 坚持一个锁(在这种情况下,行或页锁)和获取更多的锁始终是一个死锁风险。 然而,我无法解释为什么插入语句会导致锁升级 – 这可能与页面分割/添加有关,但有人更好地了解Mysql将不得不填写在那里。

首先,值得一试的是,为了删除语句,立即明确获取一个表锁。 请参阅locking表和表locking问题 。

你可以试着让这个delete作业进行操作,首先将每个要删除的行的键插入到这个伪代码的临时表中

 create temporary table deletetemp (userid int); insert into deletetemp (userid) select userid from onlineusers where datetime <= now - interval 900 second; delete from onlineusers where userid in (select userid from deletetemp); 

像这样分解效率较低,但是它避免了在delete期间保持键范围locking的需要。

此外,修改您的select查询以添加一个where子句,排除超过900秒的行。 这样可以避免依赖cron作业,并允许您将其重新安排为不太经常运行。

关于死锁的理论:在MySQL中我没有太多的背景知识,但是在这里… delete将持有datetime的键范围锁,以防止where添加where子句的行匹配事务,并且当它发现要删除的行时,它将试图获取它正在修改的每个页面上的锁。 insert将要在其插入的页面上获取锁, 然后尝试获取密钥锁。 通常情况下, insert将耐心地等待该键锁打开,但是如果delete尝试lockinginsert正在使用的同一页,这将会死锁,因为delete需要该页锁和insert需要该键锁。 这似乎不正确的插入虽然, deleteinsert使用date时间范围不重叠,所以也许别的事情正在进行。

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

对于使用Spring的Java程序员,我已经使用AOP方面避免了这个问题,该方面会自动地重试进入瞬态死锁的事务。

有关更多信息,请参阅@RetryTransaction Javadoc。