2012-12-20 74 views
2

我尽力去完成看似简单,竞争条件

Db的类型:MyISAM数据
表结构:card_id的,状态
查询:从表中选择一个未使用的card_id的,并设置行作为“用过的”。

是它的竞争条件,当更新状态前两个查询在同一时间运行,并且,同样是card_id的两次取?

我做了一些搜索了。看起来锁表是一个解决方案,但它对我来说是矫枉过正的,需要锁特权。

任何想法?

谢谢!

+0

为了避免正常db用户的'LOCK PRIVILEGE',你可以使用'STORED PROCEDURE'来执行此操作,因为你可以切换用户上下文执行 –

+0

我更新了我的答案;我提供了一个使用用户变量来返回已更新行的'cardid'值的示例。 – spencer7593

回答

2

这真的取决于你正在运行的语句。

对于MyISAM表中普通的旧的UPDATE语句,MySQL将在整个表上获得一个锁,所以两个会话之间没有“竞争”条件。一个会话将等待,直到锁定被释放,然后继续它自己的更新(或将等待指定的时间,并以“超时”中止。)

但是,如果你问的是两个会话既针对表运行SELECT,又检索要更新的行的标识符,并且两个会话检索相同的行标识符,然后两个会话试图更新同一行,那么是的,这是一个明确的可能性,真的不得不考虑。

如果条件不解决,那么它基本上会是“最后更新胜”的问题,在第二届会议将(可能)覆盖由先前的更新所做的更改。

如果这对于您的应用程序来说是不成熟的情况,那么需要使用不同的设计来解决这个问题,或者使用一些机制来防止第二次更新覆盖第一次更新所应用的更新。

正如您所提到的,一种方法是通过首先获得对表的排他锁(使用LOCK TABLES语句),然后运行SELECT获取标识符,然后运行UPDATE以更新确定行,然后终于解除锁定(使用UNLOCK TABLES语句。)

这对于一些低量,低并发应用的一个可行的办法。但它确实有一些明显的缺点。主要关心的是并发性的降低,这是由于在单个资源上获得的排它锁,这可能导致性能瓶颈。

另一种方法是所谓的“乐观锁定”的战略。 (与之前描述的可能被描述为“悲观锁定”的方法相反)。

对于“乐观锁定”策略,将额外的“计数器”列添加到表中。每当将更新应用于表中的一行时,该行的计数器就会加1。

要使用此“计数器”列,当查询检索将要(或可能)稍后更新的行时,该查询还会检索计数器列的值。

当UPDATE尝试,语句也是“计数器”列的当前值的行与所述计数器列的先前检索值进行比较。 (我们只是有一个谓语(如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之间的行的潜在的问题得到。

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的混淆值。 (A SET @id := NULL声明将工作为好,没有返回结果,如SELECT语句一样。)

接下来,我们运行UPDATE声明:1)找到一排,其中status = 'unused'; 2)改变status列的值到'used',以及3)将@id用户变量的值设置为我们更改的行的card_id值。 (我们希望card_id列是整数类型,而不是字符,以避免任何可能的字符集转换问题。)

接下来,我们运行一个查询获取由前一个UPDATE语句更改的行数,使用ROW_COUNT()函数(我们将需要验证,这是1上的客户端),并检索@id用户变量,这将是从该改变该行的值card_id的的值。

+0

感谢您的详细回复! –

+0

很难在这里添加长评论,所以我必须回答我的问题。请检查我的答案并发布您的想法。谢谢! –

1

后,我张贴此问题,我想到了一个解决方案,它是完全一样的,你在最后提到的一个。我使用update语句,它是“update TABLE set status ='used'where status ='unused'limit 1”,它返回TABLE的主Id,然后我可以使用这个主ID来获取cart_id。就像你说的那样,同时发生两个更新语句,“MySQL将获得整个表的锁定,所以两个会话之间不存在”竞争“状况,所以这应该解决我的问题。但我不确定你为什么这么说,“MySQL不提供对样式声明的支持”。

+0

“UPDATE”语句的返回值是该语句影响的行数。在UPDATE语句后面对'mysql_info'(C API)函数的调用会给出一个字符串,其中包含匹配的行数,更改的行数以及警告的数量。如果你有一些机制让MySQL返回受影响的行的主键值,我会非常感兴趣的看到这一点,因为我有几种情况,这将是非常有用的。 – spencer7593

+0

我重写了一下我的答案。当我说“MySQL不提供支持”时,我特别提到了UPDATE语句的RETURNING子句,我们在Oracle等人中支持该语句。 'UPDATE t SET c = 1 WHERE c = 0 LIMIT 1'形式的声明完全符合你的要求,除了你没有任何可靠的方法来确定哪一行是更新的。 (我用一些用户变量和timestamp列来演示一些东西,但我还没有找到一种可靠,简单的识别行的方法。 – spencer7593