更新时mysql死锁

时间:2018-08-20 23:02:02

标签: mysql deadlock

我需要在不到3分钟的时间内进行选择,操作和更新大量数据。并且决定创建某种锁定机制,以使其能够(并行)运行单独的进程,并且每个进程都应锁定,选择和更新自己的行。

为了使其成为可能,决定将列worker_id添加到表中。

表结构:

CREATE TABLE offers
(
    id int(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    offer_id int(11) NOT NULL,
    offer_sid varchar(255) NOT NULL,
    offer_name varchar(255),
    account_name varchar(255),
    worker_id varchar(255),
);
CREATE UNIQUE INDEX offers_offer_id_offer_sid_unique ON offers (offer_id, offer_sid);
CREATE INDEX offers_offer_id_index ON offers (offer_id);
CREATE INDEX offers_offer_sid_index ON offers (offer_sid);

此外,我们决定从5个并行进程开始,并且不允许使用不同的进程选择同一行,我们使用以下公式:offer_id % max_amount_of_processes = process_number(process_number从0开始,因此first为0,last为4 )

每个过程都遵循以下步骤:

  1. 使用查询worker_id
  2. 将具有当前进程ID的update offers set worker_id =: process_id where worker_id is null and offer_id%5 =: process_number order by offer_id asc limit 1000设置为前1000行
  3. 选择这些行:select * from offers where worker_id =: process_id order by offer_id asc limit 1000
  4. 使用数据进行操作,将最后一个offer_id存储到变量中,并将准备好的数据存储到另一个变量中以进行进一步更新
  5. 在第1步中运行相同的查询以锁定接下来的1000行
  6. 运行与第2步中相同的查询,并使用附加的where子句and offer_id > :last_selected_id选择下一个1000行
  7. 在循环中执行相同的步骤,直到我们锁定所有行
  8. 删除所有锁update offers set worker_id = null where worker_id =: process_id
  9. 运行查询以更新所有收集的数据

以及其他4个过程的相同步骤

这里的问题是,当所有5个进程同时从第1步运行查询以锁定行(设置worker_id)但每个进程都根据公式对自己的行进行锁定时,我遇到了死锁。我试图将事务隔离级别设置为READ COMMITED,但仍然是同一问题。

我是锁定机制的新手,我需要一个帮助来防止死锁或创建更好的机制

2 个答案:

答案 0 :(得分:1)

表达式offer_id%5 = :process_number无法使用索引,因此它只能扫描 all 与第一个条件worker_id is null匹配的行。

您可以通过两个窗口证明这一点:

mysql1> begin;
mysql1> set @p=1;
mysql1> update offers set worker_id = @p where worker_id is null and offer_id%5 = @p;

还不要在窗口1中提交事务。

mysql2> set @p=2;
mysql2> update offers set worker_id = @p where worker_id is null and offer_id%5 = @p;
...waits for about 50 seconds, or value of innodb_lock_wait_timeout, then...
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

这表明每个并发会话都锁定重叠的行集,而不仅是与模数表达式匹配的行。因此,会话在彼此的锁之间排队。

如果将所有步骤都放入@SloanThrasher建议的事务中,情况将变得更糟。使每个工人的工作时间更长,将使他们仅更长地持有他们的锁,并进一步延迟了等待这些锁的其他进程。


  

由于我仍在更新其他字段,因此我不明白updated_at字段如何导致此问题

我不确定,因为您尚未从SHOW ENGINE INNODB STATUS发布InnoDB死锁诊断。

我确实注意到您的表有第二个UNIQUE KEY,它也需要锁。由于锁分配的非原子性,有时会发生死锁。

Worker 1                               Worker 2            

UPDATE SET worker_id = 1
(acquires locks on PK)

                                        UPDATE SET worker_id = 2
                                        (waits for PK locks held by worker 1)

(waits for locks on UNIQUE KEY)

因此,工人1和工人2都可以互相等待,并陷入僵局。

这只是一个猜测。另一种可能性是,ORM正在对updated_at列进行第二次UPDATE,这为竞争条件带来了另一个机会。我还没有在脑子里解决这个问题,但是我认为这是可能的。

以下是对避免这些问题的其他系统的建议:


还有另一个问题,就是您没有真正平衡流程中的工作来获得最佳的完成时间。当您按模数划分报价时,每个组中的报价可能不相等。而且,每个报价可能都不会花费相同的时间来处理。这样,您的某些工作人员可能会完成而无所事事,而最后一个工作人员仍在处理其工作。

您可以解决锁定和负载平衡这两个问题:

通过以下方式更改表列:

ALTER TABLE offers
  CHANGE worker_id work_state ENUM('todo', 'in progress', 'done') NOT NULL DEFAULT 'todo',
  ADD INDEX (work_state),
  ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  ADD INDEX (updated_at);

创建一个进程,该进程定期从表中读取数据,并将处于“待办状态”的商品的主键id值添加到message queue中。所有商品,无论它们的offer_id值如何,都以相同的方式排队。

SELECT id FROM offers WHERE work_state = 'todo'
/* push each id onto the queue */

然后每个工作人员可以一次从消息队列中拉一个id。工作者对每个ID执行以下步骤:

  1. UPDATE offers SET work_state = 'in progress' WHERE id = :id

  2. 工人按其一份工作完成工作。

  3. UPDATE offers SET work_state = 'done' WHERE id = :id

这些工作人员一次只查询一个报价,并且通过主键解决报价,该主键将使用PK索引并且一次仅锁定一行。

一旦完成一个报价,工作人员就会从队列中拉出下一个报价。

这样,工人将全部同时完成,并且工作将更好地平衡于工人。另外,您可以随时启动或停止工作人员,而不必担心他们是多少个工作人员,因为您的报价不需要由与offer_id模数相同的工作人员处理。

当工作人员完成所有报价后,消息队列将为空。大多数消息队列都允许工作程序执行阻止读取,因此当队列为空时,工作程序将仅等待读取返回。使用数据库时,工作人员必须经常轮询新工作。

工人有可能在工作中失败,并且永远不会将要约标记为“完成”。您需要定期检查孤立的报价。假设它们不会完成,并标记其状态为“待办”。

UPDATE offers SET work_state = 'todo' 
WHERE work_state = 'in progress' AND updated_at < NOW() - INTERVAL 5 MINUTE

请选择时间间隔长度,以便可以确定,除非出现问题,否则任何工人都可以在那时完成它。您可能会在调度员查询当前要执行的待办事项之前执行此“重置”操作,以便将已被遗忘的要约重新排队。

答案 1 :(得分:0)

我发现了问题。这是因为我的ORM默认情况下在执行更新操作时会更新时间戳记字段(为简化上述示例,我从表结构中删除了它们),并且在关闭它之后,死锁消失了。但是,尽管如此,我仍不了解updated_at字段如何导致此问题,因为我仍在更新其他字段