MySQL 行锁和原子更新
MySQL row lock and atomic updates
我正在使用 MySQL 构建一个 "poor man's queuing system"。它是一个包含需要执行的作业的 table(table 名称是 queue
)。我在多台机器上有多个进程,它们的工作是调用 fetch_next2
存储过程以从队列中取出一个项目。
这个过程的重点是确保我们永远不会让 2 个客户获得相同的工作。我认为通过使用 SELECT .. LIMIT 1 FOR UPDATE
将允许我锁定单行,以便我可以确定它仅由 1 个调用者更新(更新后它不再符合 SELECT
的标准用于过滤 "READY" 待处理的作业)。
谁能告诉我我做错了什么?我只是遇到过一些情况,相同的工作被分配给了 2 个不同的进程,所以我知道它不能正常工作。 :)
CREATE DEFINER=`masteruser`@`%` PROCEDURE `fetch_next2`()
BEGIN
SET @id = (SELECT q.Id FROM queue q WHERE q.State = 'READY' LIMIT 1 FOR UPDATE);
UPDATE queue
SET State = 'PROCESSING', Attempts = Attempts + 1
WHERE Id = @id;
SELECT Id, Payload
FROM queue
WHERE Id = @id;
END
答案代码:
CREATE DEFINER=`masteruser`@`%` PROCEDURE `fetch_next2`()
BEGIN
SET @id := 0;
UPDATE queue SET State='PROCESSING', Id=(SELECT @id := Id) WHERE State='READY' LIMIT 1;
#You can do an if @id!=0 here
SELECT Id, Payload
FROM queue
WHERE Id = @id;
END
您正在做的事情的问题是操作没有原子分组。您正在使用 SELECT ... FOR UPDATE syntax。文档说它会阻止 "from reading the data in certain transaction isolation levels"。但不是所有级别(我认为)。在您的第一个 SELECT 和 UPDATE 之间,另一个 SELECT 可能来自另一个线程。您使用的是 MyISAM 还是 InnoDB? MyISAM 可能不支持它。
确保其正常工作的最简单方法是 lock the table。
[编辑] 我在这里描述的方法比使用上面代码中的 Id=(SELECT @id := Id)
方法更耗时。
另一种方法是执行以下操作:
- 有一列通常设置为 0。
- 执行“UPDATE ... SET ColName=UNIQ_ID WHERE ColName=0 LIMIT 1。这将确保只有 1 个进程可以更新该行,然后通过 SELECT 获取它之后。(UNIQ_ID 不是 MySQL 特征,只是一个变量)
如果您需要一个唯一的 ID,您可以使用 table 和 auto_increment。
您还可以某种对事务执行此操作。如果您从一个线程在 table、运行 UPDATE foobar SET LockVar=19 WHERE LockVar=0 LIMIT 1;
上启动事务,并在另一个线程上执行完全相同的操作,则第二个线程将等待第一个线程在它之前提交得到它的行。不过,这可能最终会成为一个完整的 table 阻塞操作。
我正在使用 MySQL 构建一个 "poor man's queuing system"。它是一个包含需要执行的作业的 table(table 名称是 queue
)。我在多台机器上有多个进程,它们的工作是调用 fetch_next2
存储过程以从队列中取出一个项目。
这个过程的重点是确保我们永远不会让 2 个客户获得相同的工作。我认为通过使用 SELECT .. LIMIT 1 FOR UPDATE
将允许我锁定单行,以便我可以确定它仅由 1 个调用者更新(更新后它不再符合 SELECT
的标准用于过滤 "READY" 待处理的作业)。
谁能告诉我我做错了什么?我只是遇到过一些情况,相同的工作被分配给了 2 个不同的进程,所以我知道它不能正常工作。 :)
CREATE DEFINER=`masteruser`@`%` PROCEDURE `fetch_next2`()
BEGIN
SET @id = (SELECT q.Id FROM queue q WHERE q.State = 'READY' LIMIT 1 FOR UPDATE);
UPDATE queue
SET State = 'PROCESSING', Attempts = Attempts + 1
WHERE Id = @id;
SELECT Id, Payload
FROM queue
WHERE Id = @id;
END
答案代码:
CREATE DEFINER=`masteruser`@`%` PROCEDURE `fetch_next2`()
BEGIN
SET @id := 0;
UPDATE queue SET State='PROCESSING', Id=(SELECT @id := Id) WHERE State='READY' LIMIT 1;
#You can do an if @id!=0 here
SELECT Id, Payload
FROM queue
WHERE Id = @id;
END
您正在做的事情的问题是操作没有原子分组。您正在使用 SELECT ... FOR UPDATE syntax。文档说它会阻止 "from reading the data in certain transaction isolation levels"。但不是所有级别(我认为)。在您的第一个 SELECT 和 UPDATE 之间,另一个 SELECT 可能来自另一个线程。您使用的是 MyISAM 还是 InnoDB? MyISAM 可能不支持它。
确保其正常工作的最简单方法是 lock the table。
[编辑] 我在这里描述的方法比使用上面代码中的 Id=(SELECT @id := Id)
方法更耗时。
另一种方法是执行以下操作:
- 有一列通常设置为 0。
- 执行“UPDATE ... SET ColName=UNIQ_ID WHERE ColName=0 LIMIT 1。这将确保只有 1 个进程可以更新该行,然后通过 SELECT 获取它之后。(UNIQ_ID 不是 MySQL 特征,只是一个变量)
如果您需要一个唯一的 ID,您可以使用 table 和 auto_increment。
您还可以某种对事务执行此操作。如果您从一个线程在 table、运行 UPDATE foobar SET LockVar=19 WHERE LockVar=0 LIMIT 1;
上启动事务,并在另一个线程上执行完全相同的操作,则第二个线程将等待第一个线程在它之前提交得到它的行。不过,这可能最终会成为一个完整的 table 阻塞操作。