5

我正在寻找一种为一个线程显式选择一个表行的方法。我写了一个爬虫,它可以处理大约 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 倍,所以我假设会有更多的锁定问题)

我修改为按提示SELECT使用索引。我现在的问题是 lockwait 超时了(死锁消失了)。crawlerUSE INDEX(crawler)

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
4

3 回答 3

3

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

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

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

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

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

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

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


有了更多解释,现在很明显您将 RDBMS 用作 FIFO。这不是对 RDBMS 的有效使用。有用于此目的的消息队列技术。

也可以看看:

于 2012-12-29T20:09:32.550 回答
1

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

当您在进行这样的高度并行工作时,减少基于争用的问题的最简单方法之一是通过发明一种方法让所有线程提前知道它们应该处理哪些行来完全消除争用。然后他们可以锁定而不必争用资源,并且您的数据库不必解决争用。

如何最好地做到这一点?通常人们会选择某种线程 ID 方案并使用模算术来确定哪些线程获取哪些行。如果您有 10 个线程,则线程 0 将获得第 0、10、20、30 行等。线程 1 获得 1、11、21、31 等。

一般来说,如果您有 NUM_THREADS 则您的每个线程都会从数据库中选择 THREAD_ID + i*NUM_THREADS 的 ID 并对其进行处理。

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

于 2012-12-29T19:11:25.823 回答
0

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

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

PS:为了澄清,我说update ... limit 1的是确保你只更新一行。

编辑: 解决方案

是正确的,如下所示。

于 2012-12-31T15:16:28.363 回答