在高并行连接上仅选择一个表行

时间:2012-12-23 20:45:18

标签: mysql parallel-processing locking mysqli message-queue

我正在寻找一种为一个线程明确选择一个表行的方法。我编写了一个爬虫程序,它可以处理大约50个并行进程。每个进程都必须从表中取出一行并进行处理。

CREATE TABLE `crawler_queue` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `url` text NOT NULL,
 `class_id` tinyint(3) unsigned NOT NULL,
 `server_id` tinyint(3) unsigned NOT NULL,
 `proc_id` mediumint(8) unsigned NOT NULL,
 `prio` tinyint(3) unsigned NOT NULL,
 `inserted` int(10) unsigned NOT NULL,
 PRIMARY KEY (`id`),
 KEY `proc_id` (`proc_id`),
 KEY `app_id` (`app_id`),
 KEY `crawler` (`class_id`,`prio`,`proc_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

现在我的流程执行以下操作:

  • 启动数据库事务
  • 选择SELECT * FROM crawler_queue WHERE class_id=2 AND prio=20 AND proc_id=0 ORDER BY id LIMIT 1 FOR UPDATE
  • 然后使用UPDATE crawler_queue SET server_id=1,proc_id=1376 WHERE id=23892
  • 更新此行
  • 提交交易

这应该有助于没有其他进程可以获取已处理的行。在选择节目上进行说明

id  select_type  table          type  possible_keys    key      key_len  ref    rows    Extra
1   SIMPLE       crawler_queue  ref   proc_id,crawler  proc_id  3        const  617609  Using where

但这些过程似乎导致过高的并行性,因为有时我会在日志中看到两种类型的错误/警告(每隔5分钟左右):

mysqli::query(): (HY000/1205): Lock wait timeout exceeded; try restarting transaction (in /var/www/db.php l
ine 81)

mysqli::query(): (40001/1213): Deadlock found when trying to get lock; try restarting transaction (in /var/www/db.php line 81)

我的问题是:有人能指出我正确的方向来减少这些锁定问题吗? (在生产状态下,并行性将比现在高3-4倍,所以我假设存在更多的锁定问题)

编辑2012-12-29 :我通过提示SELECT修改了crawler以使用索引USE INDEX(crawler)。我现在的问题是lockwait超时(死锁消失)。

编辑2012-12-31 EXPLAIN现在显示USE INDEX()(行数没有更高,因为表格现在包含更多数据):

id  select_type  table          type  possible_keys    key      key_len  ref                rows     Extra
1   SIMPLE       crawler_queue  ref   proc_id,crawler  crawler  5        const,const,const  5472426  Using where

3 个答案:

答案 0 :(得分:3)

您的EXPLAIN报告显示您只使用单列索引proc_id,并且查询必须检查超过600K行。如果优化器选择了crawler索引,那可能会更好。

InnoDB可能会锁定所有600K行,而不仅仅是与WHERE子句中的完整条件匹配的行。 InnoDB锁定所有检查的行,以确保并发更改不会以错误的顺序写入binlog。

解决方案是使用索引来缩小检查行的范围。这可能不仅可以帮助您更快地找到行,还可以避免锁定大范围的行。 crawler索引在这里应该有所帮助,但不能立即清楚为什么它没有使用该索引。

您可能必须ANALYZE TABLE确保更新InnoDB的表统计信息,以便在优化计划中使用该索引之前了解crawler索引。 ANALYZE TABLE是一种廉价的操作。

另一种选择是使用索引提示:

SELECT * FROM crawler_queue USE INDEX(crawler) ...

这告诉优化器使用该索引,并且不考虑此查询的其他索引。我更喜欢避免使用索引提示,因为优化器通常能够自己做出正确的决策,并且在代码中使用提示意味着我可能会强制优化器不要考虑我将来创建的索引,否则它会选择


有了更多解释,现在很清楚你正在使用你的RDBMS作为FIFO。这不是RDBMS的有效使用。为此目的有消息队列技术。

另见:

答案 1 :(得分:0)

从我可以告诉您所面临的问题是,两个线程正在为表中的同一行进行维护,并且它们都无法拥有它。但是没有任何优雅的方法让数据库说“不,你不能拥有那个,找到另一行”,因此你会得到错误。这称为资源争用。

当您正在进行高度并行的工作时,减少基于争用的问题的最简单方法之一是通过为所有线程创建一种方法来完全消除争用,以便知道它们应该在哪些行上工作时间然后,他们可以锁定而无需争用资源,您的数据库也不必解决争用。

如何做到最好?通常人们选择某种线程id方案并使用模运算来确定哪些线程获得哪些行。如果您有10个线程,那么线程0将获得第0,10,20,30等行。线程1获得1,11,21,31等。

一般情况下,如果你有NUM_THREADS个,那么你的每个线程都会从数据库中选择THREAD_ID + i * NUM_THREADS个ID并对其进行处理。

我们引入了一个问题,即线程可能会停滞或死亡,并且您最终可能会在数据库中找到永远不会被触及的行。这个问题有几种解决方案,其中之一是在大多数/所有线程完成后运行“清理”,所有线程都可以随意抓取任何行并抓取它们直到没有未爬网的URL。你可以变得更复杂并且有一些清理线程在不断运行,或者让每个线程偶尔执行清理任务等。

答案 2 :(得分:0)

更好的解决方案是进行更新并完全跳过选择。然后,您可以使用last_insert_id()来获取更新的项目。这应该允许您在完成同时执行更新时完全跳过锁定。更新记录后,您可以开始处理它,因为考虑到并非所有初始条件都匹配,它将永远不会被完全相同的查询再次选择。

我认为这可以帮助您解决与锁定相关的所有问题,并且应该允许您并行运行任意数量的进程。

PS:只是为了澄清,我在谈论update ... limit 1以确保你只更新一行。

编辑: Solution

是正确的,如下所示。