2

我试图完成的事情似乎很简单,

Db类型:MyISAM
表结构:card_id,status
查询:从表中选择一个未使用的card_id,并将该行设置为“已使用”。

当两个查询同时运行并且在更新状态之前,两次获取相同的 card_id 是否是竞争条件?

我已经做了一些搜索。似乎 Lock table 是一个解决方案,但它对我来说太过分了,需要 Lock Privilege。

任何想法?

谢谢!

4

2 回答 2

2

这实际上取决于您正在运行的语句。

对于针对 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 之间的行。

但是RETURNINGMySQL 不支持该子句。而且我还没有找到任何可靠的方法来从 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 值。

于 2012-12-20T22:17:23.353 回答
1

在我发布这个问题之后,我想到了一个与您最后提到的解决方案完全相同的解决方案。我使用了update语句,即“update TABLE set status ='used' where status ='unused' limit 1”,它返回TABLE的primary Id,然后我可以使用这个primary ID来获取cart_id。说有两个更新语句同时发生,正如你所说,“MySQL 将获得整个表的锁,因此两个会话之间没有“竞争”条件”,所以这应该解决我的问题。但我不确定您为什么说“MySQL 不提供样式声明支持”。

于 2012-12-21T15:50:39.963 回答