我有以下交互(伪代码):
start transaction
select delivery for update # semaphore
select delivery_items where processed = false # get unprocessed data and process it
update delivery_items SET processed = true
commit # release semaphore
我原以为这足以锁定交付,以便没有两个人能够同时对其项目执行操作,但由于某种原因,我遇到了两个人确实设法修改的竞争条件(我delivery_items
是将相同的项目处理两次)。
我没有正确理解 ACID 吗?
在另一个用户完成事务之前,第一个选择是否应该阻止第二个选择和更新之间的提交阻止竞争条件?
所有 MySQL 设置都设置为默认值(某些内存池大小除外)。
如果这有什么不同的话,我正在使用 Doctrine。
我也在使用嵌套事务(doctrine 使用SAVEPOINT
s 实现它们)。
您正确理解锁定逻辑,但我怀疑嵌套事务导致了您的问题。(如果没有看到您的实际代码,很难确定,但以下是可能的解释。)
如果您
SELECT
在事务中的第一条语句不是锁定,SELECT ... FOR UPDATE
那么您将遇到问题,因为默认的事务隔离级别是 READ-REPEATABLE。假设你的事务中确实有一个更早的
SELECT
事务,那么整个事务正在拍摄数据库状态的快照,并否定了锁定的好处。详细来说,这是我认为导致您的比赛条件的原因:
Step 1. Request1:
SELECT
statement withoutFOR UPDATE
(此处截图)Step 2. Request1:
SELECT ... FOR UPDATE
启动锁步骤 3. 请求 1:
SELECT processed=false
Step 4. Request2:
SELECT
statement withoutFOR UPDATE
(此处截图)Step 5. Request2:
SELECT ... FOR UPDATE
等待Request1完成步骤 6. 请求 1:
UPDATE processed=true
步骤 7. 请求 1:
COMMIT
步骤 8. Request2:(
SELECT processed=false
将在步骤 4 中看到数据库,在更改处理的行之前,因此查询将返回与 Request1 相同的行)创建一个
SAVEPOINT
不会阻止此问题。我过去在stackoverflow上写过这个问题。
你的伪代码说你没有这样做,但关于嵌套事务的评论表明你可能在你的伪代码启动时已经有一个打开的事务,这导致为你的需要拍摄“不正确”的快照。
我的建议与我的链接帖子中的建议相同:确保您的交易以
SELECT ... FOR UPDATE
声明开头。这意味着提交较早的事务,而不是使用嵌套逻辑,因为像 Doctrine 那样简单地创建一个 SAVEPOINT 并不足以确保您正在使用正确的数据快照。如果您使用嵌套事务试图将整个 HTTP 请求包装在一个事务中,然后在请求的更深处使用嵌套事务,那么这是一个预期的问题,我建议不要尝试像那样包装整个请求。(我只提到这一点,因为我曾经天真地认为像那样包装整个请求是个好主意......然后发现了我在这个答案中概述的困难问题。)
语法问题。也许你的意思是这样的:
据推测,“处理”将仅作用于“错误”行。这些是唯一真正需要为 ACID“锁定”的行。
如果没有合适的索引,可能会锁定表中的所有行。您可能会考虑使用以
processed
. 这样,只有“错误”的行会被“锁定”。嗯......这提出了一个问题,如果一个单独的连接插入/更新另一行以具有“假”,会发生什么。
UDPATE
修改不是“processed=false”行而是修改返回的行可能更安全SELECT
。