【问题标题】:How can I avoid deadlocks with my queue table on MariaDB/Galera?如何避免 MariaDB/Galera 上的队列表出现死锁?
【发布时间】:2015-07-01 12:25:06
【问题描述】:

我有一个基本上是先进先出队列的数据库表。行被系统的其他部分简单地插入到表中而被遗忘。每 5 分钟运行一次作业以处理队列中的项目。要处理的每一行的状态字段都从待处理值更改为处理值。队列中的后续副本被匹配并标记为正在处理的较早排队项目的副本。队列处理器作业是唯一对表做任何事情的事情,除了系统中只是盲目地插入行的部分。

这正是处理器对队列所做的:

START TRANSACTION;

SELECT id
FROM api_queue
WHERE status=:status_processing

-- Application checks this result set is empty, then...

UPDATE api_queue qs
INNER JOIN api_queue qdupes ON qdupes.products_id=qs.products_id AND qdupes.action=qs.action
SET qdupes.status = IF(qs.id=qdupes.id, :status_processing, :status_processing_duplicate)
WHERE qs.id IN (:queue_ids) ;

COMMIT;

-- Each queue item is processed

-- Once processing is complete, we purge the queue

START TRANSACTION;

SELECT COUNT(*) AS total FROM api_queue WHERE status = :status_processing ;

-- Application sanity checks the number of processing items it's about to delete against how many it's processed, and then...

DELETE FROM api_queue WHERE status IN (:status_processing, :status_processing_duplicate) ;

COMMIT;

在典型的 5 分钟内,队列会积压大约 100 项,但如果目录中发生大量更改,有时可能会达到数千项。

第一个事务在没有遇到死锁时通常非常快(0.1 - 0.2 秒完成),但似乎有 10% 的时间会遇到死锁。

为什么它经常遇到死锁?即使事务锁定了表中当前的所有行,我是否应该期望这会在向表中添加新行时引起争用?如果是这样,为什么会这样?

我还注意到,有时上面的第一笔交易(包含UPDATE 查询)似乎根本不适用——尽管我认为这很可能是一个不相关的错误。

我的队列表如下所示:

CREATE TABLE IF NOT EXISTS `api_queue` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `products_id` int(11) NOT NULL,
  `action` tinyint(3) NOT NULL,
  `triggered_by` tinyint(3) NOT NULL,
  `status` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

【问题讨论】:

    标签: mysql deadlock mariadb galera


    【解决方案1】:

    我的口头禅:“不要排队,只要去做”。我这样说是因为我看到太多在 MySQL 中实现的队列由于某种原因而失败。一个常见的原因是插入/检查/删除项目的开销可能与“仅执行任务”一样昂贵。那为什么要双倍的成本呢?而且,显然,排队导致了额外的死锁。

    根据您提供的信息,系统应该能够每 5 分钟处理 1500-3000 个。那应该处理“100”到“数千”。

    您的排队机制似乎过于复杂,因为它涉及JOIN 和其他不只是一进一出的东西。

    假设到目前为止你拒绝了我的 cmets,我将继续批评代码......

    SELECT ... FOR UPDATE
    

    SELECTs 可能都需要。

    DELETE 旁边的SELECT 可能与DELETE 合并为一个多表DELETE。或者有可能将其连同相关代码从事务中提取出来。 (更快的事务更不可能死锁。)

    您正在检查COMMITs 之后的错误(死锁等),是吗?这就是 Galera 受到打击的时候。

    使用IN(...) 时,对元素进行排序。底层代码可能会按IN 元素的顺序锁定行。这可以将死锁变成最多innodb_lock_wait_timeout 秒的延迟。 (这样的延迟并不像死锁那么“糟糕”。)

    当事务陷入死锁时,您会重复事务,对吗? (这是处理死锁的简单方法。)

    编辑(输入)

    如果一个线程正在执行UPDATE ... WHERE id IN (11,22) 而另一个正在执行UPDATE ... WHERE id IN (22,11),并且每个线程都锁定了一行,那么尝试锁定另一行就是死锁——一个线程必须ROLLBACK。相反,如果两者都说(11,22),那么(在最坏的情况下)一个人将不得不等待(但不会陷入僵局)。在没有证据的情况下,我假设 InnoDB 代码不足以以某种方式避免这种IN 死锁——通过对数字进行排序、原子锁定或其他方式。 (而且我认为 cleaver=slower,因此对于这种罕见的情况不值得这样做。)

    【讨论】:

    • 它排队的工作通常需要 3-5 分钟才能完成,无论是针对 1 个项目还是 1000 个批次。它执行的 API 任务也被限制为每分钟 1 个请求。我不是“拒绝”你的 cmets,但在这种情况下,排队和分组项目似乎是唯一的选择。感谢您的建议。是的,我检查 all 查询(SELECTINSERTUPDATECOMMIT 等)的死锁。是的,如果发生死锁,我会重试。我仍然不明白为什么我的代码会导致它们。您能否进一步解释一下IN 的订购建议?
    • 啊,当然。我会尝试一下,但我仍然不相信在这种情况下这将是死锁的原因,因为只有一个进程会修改行。不过,我采纳了您的建议并简化了队列,使用INSERT INTO ... SELECT ... FROM ... WHERE NOT EXISTS ... 描述的here 来避免重复。我不想使用ON DUPLICATE KEY UPDATE,因为这可能会导致进一步的死锁(这就是为什么我选择像以前那样做)。这种新方法似乎更好(而且不太复杂)。
    猜你喜欢
    • 2017-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-07
    • 2012-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多