我努力完成的事情似乎很简单,
Db类型:MyISAM
表结构:card_id,状态
查询:从表中选择未使用的card_id,并将该行设置为“used”。
竞争条件是当两个查询同时运行,并且状态更新之前,同一个card_id被提取两次?
我已经做了一些搜索。似乎Lock表是一个解决方案,但它对我来说太过分了,需要Lock Privilege。
任何想法?
谢谢!
答案 0 :(得分:2)
这实际上取决于您正在运行的语句。
对于针对MyISAM表的普通旧UPDATE
语句,MySQL将获得对整个表的锁定,因此没有" race"两次会议之间的条件。一个会话将等待锁定被释放,然后继续进行自己的更新(或者等待指定的时间段,然后中止"超时"。)
但是,如果您要问的是两个会话都对表运行SELECT,检索要更新的行的标识符,并且两个会话都检索相同的行标识符,然后两个会话都尝试更新同一行,然后是,这是一个明确的可能性,而且确实需要考虑。
如果没有解决这个问题,那么它基本上将是"最后更新获胜",第二个会话将(可能)覆盖先前更新所做的更改。
如果这对您的应用程序来说是难以维持的情况,则需要使用不同的设计或使用某种机制阻止第二次更新覆盖第一次更新所应用的更新。 / p>
正如您所提到的,一种方法是通过首先获取表上的排它锁(使用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的问题,其他一些会话可能会更新SELECT和UPDATE之间的行。
但是MySQL不支持RETURNING
子句。而且我还没有找到任何可靠的方法来从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 ;
重要的是,这三个语句在SAME数据库会话中运行(即保持数据库会话;不要放弃它并获得新的。)
首先,我们将用户变量(@id
)初始化为一个值,该值不会与表中的真实card_id值混淆。 (SET @id := NULL
语句也可以正常工作,而不返回结果,就像SELECT语句一样。)
接下来,我们将UPDATE
语句运行到1)找到一行status = 'unused'
,2)将status
列的值更改为'used'
,以及3)将@id
用户变量的值设置为我们更改的行的card_id
值。 (我们希望card_id
列为整数类型,而不是字符,以避免任何可能的字符集转换问题。)
接下来,我们使用ROW_COUNT()
函数运行查询获取前一个UPDATE语句更改的行数(我们需要验证客户端这是1),并检索@id
用户变量的值,它将是已更改行的card_id值。
答案 1 :(得分:1)
在我发布这些问题之后,我想到了一个与你最后提到的解决方案完全相同的解决方案。我使用了update语句,它是“update TABLE set status ='used'at status ='unused'limit 1”,它返回TABLE的主Id,然后我可以使用这个主ID来获取cart_id。说有两个更新语句同时发生,正如你所说,“MySQL将获得整个表的锁定,因此两个会话之间没有”竞争“条件”,所以这应该解决我的问题。但我不确定你为什么说,“MySQL不提供支持风格声明”。