我需要在不到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 )
每个过程都遵循以下步骤:
worker_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行
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
,但仍然是同一问题。
我是锁定机制的新手,我需要一个帮助来防止死锁或创建更好的机制
答案 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执行以下步骤:
UPDATE offers SET work_state = 'in progress' WHERE id = :id
工人按其一份工作完成工作。
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
字段如何导致此问题,因为我仍在更新其他字段