我试图完成的事情似乎很简单,
Db类型:MyISAM
表结构:card_id,status
查询:从表中选择一个未使用的card_id,并将该行设置为“已使用”。
当两个查询同时运行并且在更新状态之前,两次获取相同的 card_id 是否是竞争条件?
我已经做了一些搜索。似乎 Lock table 是一个解决方案,但它对我来说太过分了,需要 Lock Privilege。
任何想法?
谢谢!
我试图完成的事情似乎很简单,
Db类型:MyISAM
表结构:card_id,status
查询:从表中选择一个未使用的card_id,并将该行设置为“已使用”。
当两个查询同时运行并且在更新状态之前,两次获取相同的 card_id 是否是竞争条件?
我已经做了一些搜索。似乎 Lock table 是一个解决方案,但它对我来说太过分了,需要 Lock Privilege。
任何想法?
谢谢!
这实际上取决于您正在运行的语句。
对于针对 MyISAM 表的普通旧UPDATE
语句,MySQL 将获得整个表的锁定,因此两个会话之间没有“竞争”条件。一个会话将等到锁被释放,然后继续它自己的更新(或将等待指定的时间段,并以“超时”中止。)
但是,如果您要问的是两个会话都对表运行 SELECT 以检索要更新的行的标识符,并且两个会话都检索相同的行标识符,然后两个会话都尝试更新同一行,那么是的,这是一种确定的可能性,并且确实必须考虑。
如果没有解决该条件,那么基本上将是“最后一次更新获胜”的问题,第二个会话将(可能)覆盖先前更新所做的更改。
如果这对您的应用程序来说是一个站不住脚的情况,那么确实需要解决这个问题,或者使用不同的设计,或者使用某种机制来防止第二次更新覆盖第一次更新所应用的更新。
正如您所提到的,一种方法是通过首先获得表上的排他锁(使用 LOCK TABLES 语句),然后运行 SELECT 来获取标识符,然后运行 UPDATE 来更新标识的行来避免这种情况,并且最后,释放锁(使用 UNLOCK TABLES 语句。)
对于一些低容量、低并发的应用程序来说,这是一种可行的方法。但它确实有一些明显的缺点。主要关注的是并发性降低,因为在单个资源上获得了排他锁,这有可能导致性能瓶颈。
另一种选择是一种称为“乐观锁定”的策略。(与前面描述的方法相反,可以描述为“悲观锁定”。)
对于“乐观锁定”策略,会在表中添加一个额外的“计数器”列。每当对表中的行应用更新时,该行的计数器就会加一。
为了使用这个“计数器”列,当查询检索到稍后将(或可能)更新的行时,该查询也会检索计数器列的值。
当尝试更新时,该语句还将行中“计数器”列的当前值与先前检索到的计数器列值进行比较。(我们只是在 UPDATE 语句中包含一个谓词(例如在 WHERE 子句中)。例如,
UPDATE mytable
SET counter = counter + 1
, col = :some_new_value
WHERE id = :previously_fetched_row_identifier
AND counter = :previously_fetched_row_counter
如果某个其他会话对我们尝试更新的行应用了更新(有时在我们的会话检索该行的时间和我们的会话尝试进行更新之前),那么该行上“计数器”列的值将被改变。
我们的 UPDATE 语句中的谓词会检查这一点,如果“计数器”已更改,那将导致我们的更新不被应用。然后我们可以检测到这种情况(即受影响的行数将是 0 而不是 1)并且我们的会话可以采取一些适当的措施。(“嘿!其他一些会话更新了我们打算更新的行!”)
关于如何实现“乐观锁定”策略有一些很好的文章。
一些 ORM 框架(例如 Hibernate、JPA)为这种类型的锁定策略提供支持。
不幸的是,MySQL 不支持 UPDATE 语句中的 RETURNING 子句,例如:
UPDATE ...
SET status = 'used'
WHERE status = 'unused'
AND ROWNUM = 1
RETURNING card_id INTO ...
其他 RDBMS(例如 Oracle)确实提供了这种功能。有了 UPDATE 语句的这个特性,我们可以简单地运行UPDATE
语句来 1) 找到一行status = 'unused'
2) 更改 的值status = 'used'
3) 返回card_id
我们刚刚更新的行的(或我们想要的任何列) .
这解决了必须运行 SELECT 然后运行单独的 UPDATE 的问题,其他会话可能会更新我们的 SELECT 和我们的 UPDATE 之间的行。
但是RETURNING
MySQL 不支持该子句。而且我还没有找到任何可靠的方法来从 MySQL 中模拟这种类型的功能。
这可能对你有用
我不完全确定为什么我以前使用用户变量放弃了这种方法(我在上面提到过我已经玩过这个。我想也许我需要更通用的东西,它会更新不止一行并返回一组 id值。或者,也许有一些关于用户变量的行为不能保证的东西。(再说一次,我只在精心构造的 SELECT 语句中引用用户变量;我不在 DML 中使用用户变量;可能是因为我不能保证他们的行为。)
由于您只对一行感兴趣,因此这三个语句的序列可能对您有用:
SELECT @id := NULL ;
UPDATE mytable
SET card_id = (@id := card_id)
, status = 'used'
WHERE status = 'unused'
LIMIT 1 ;
SELECT ROW_COUNT(), @id AS updated_card_id ;
重要的是这三个语句在同一个数据库会话中运行(即保持数据库会话;不要放手并获得一个新的。)
首先,我们将用户变量 ( @id
) 初始化为一个不会与表中的真实 card_id 值混淆的值。(SET @id := NULL
语句也可以工作,不返回结果,就像 SELECT 语句一样。)
接下来,我们将UPDATE
语句运行到 1) 找到其中的一行status = 'unused'
,2) 将status
列的值更改为'used'
3) 将@id
用户变量的card_id
值设置为我们更改的行的值。(我们希望该card_id
列是整数类型,而不是字符,以避免任何可能的字符集转换问题。)
ROW_COUNT()
接下来,我们运行一个查询,使用该函数(我们需要在客户端验证这是 1)获取上一个 UPDATE 语句更改的行数,并检索@id
用户变量的值,这将是已更改行中的 card_id 值。
在我发布这个问题之后,我想到了一个与您最后提到的解决方案完全相同的解决方案。我使用了update语句,即“update TABLE set status ='used' where status ='unused' limit 1”,它返回TABLE的primary Id,然后我可以使用这个primary ID来获取cart_id。说有两个更新语句同时发生,正如你所说,“MySQL 将获得整个表的锁,因此两个会话之间没有“竞争”条件”,所以这应该解决我的问题。但我不确定您为什么说“MySQL 不提供样式声明支持”。