我需要在不到 3 分钟的时间内选择、进行操作和更新大量数据。并决定创建某种锁定机制以使能够运行单独的进程(并行)并且每个进程都应该锁定、选择和更新自己的行。
为了使它成为可能,决定添加列 worker_id
到 table 上。
表结构:
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)每个过程都遵循以下步骤:
worker_id
使用以下查询将当前进程 ID 添加到前 1000 行: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
select * from offers where worker_id =: process_id
order by offer_id asc limit 1000
offer_id
到变量和准备数据到另一个变量以进一步更新and offer_id > :last_selected_id
选择接下来的 1000 行 update offers set worker_id = null where worker_id =: process_id
以及其他 4 个过程的相同步骤
这里的问题是,当所有 5 个进程同时运行步骤 1 中的查询以锁定行(设置
worker_id
)但每个进程都为自己的行锁定取决于公式时,我遇到了死锁。我尝试将事务隔离级别设置为 READ COMMITED
但仍然是同样的问题。我是锁定机制的新手,我需要帮助来防止这里出现死锁或创建更好的机制
最佳答案
表达式 offer_id%5 = :process_number
不能使用索引,所以只能扫描第一个条件匹配的所有行,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
这表明每个并发 session 锁定重叠的行集,而不仅仅是匹配模数表达式的行。所以 session 排队反对彼此的锁。
如果您像@SloanThrasher 建议的那样将所有步骤都放入事务中,情况会变得更糟。让每个工作人员的工作花费更长的时间会使他们只持有自己的锁的时间更长,并进一步延迟等待这些锁的其他进程。
I do not understand how updated_at field can cause the issue as I'm still updating other fields
我不确定,因为您还没有从
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)
因此, worker 1 和 worker 2 都可以互相等待,并进入死锁。
这只是一个猜测。另一种可能性是 ORM 正在为
updated_at
执行第二次更新。列,这为竞争条件引入了另一个机会。我在心理上还没有完全解决这个问题,但我认为这是可能的。以下是针对可以避免这些问题的不同系统的建议:
还有另一个问题是,您并没有真正平衡流程中的工作以实现最佳完成时间。当您按模数拆分它们时,每个组中的优惠数量可能不相等。无论如何,每个报价可能不会花费相同的时间来处理。所以你的一些 worker 可以完成并且无事可做,而最后一个 worker 仍在处理它的工作。
您可以解决锁定和负载平衡这两个问题:
按以下方式更改表格列:
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
'todo' 状态下的报值(value)到 message queue .所有的报价,无论其 offer_id 值如何,都以相同的方式排队。SELECT id FROM offers WHERE work_state = 'todo'
/* push each id onto the queue */
然后每个 worker 可以拉一个
id
一次从消息队列中。工作人员对每个 id 执行以下步骤:UPDATE offers SET work_state = 'in progress' WHERE id = :id
UPDATE offers SET work_state = 'done' WHERE id = :id
这些工作查询一次只引用一个报价,它们通过主键处理报价,这将使用 PK 索引并且一次只锁定一行。
一旦它完成了一个提议,那么工作人员就会从队列中拉出下一个提议。
这样, worker 将同时完成,工作将更好地平衡 worker 。此外,您可以随时启动或停止工作人员,您不必关心他们是什么工作人员编号,因为您的报价不需要由与 offer_id 的模数相同的工作人员处理。
当工作人员完成所有报价时,消息队列将为空。大多数消息队列允许工作人员进行阻塞读取,因此当队列为空时,工作人员将等待读取返回。当您使用数据库时,工作人员必须经常轮询新工作。
worker 在工作期间有可能会失败,并且永远不会将要约标记为“完成”。您需要定期检查孤立的报价。假设它们不会完成,并将它们的状态标记为“待办事项”。
UPDATE offers SET work_state = 'todo'
WHERE work_state = 'in progress' AND updated_at < NOW() - INTERVAL 5 MINUTE
选择间隔长度,这样可以确定任何 worker 到那时都会完成它,除非出现问题。您可能会在调度程序查询当前优惠待办事项之前执行此“重置”,因此被遗忘的优惠将重新排队。
关于更新时mysql死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51939560/