当WHERE子句取决于旧值时,Oracle中同时进行UPDATE的一致性

时间:2019-03-08 17:16:03

标签: sql oracle relational-database concurrentmodification transaction-isolation

我一直在阅读有关Oracle数据一致性保证和受支持的事务隔离级别的信息(例如:https://docs.oracle.com/database/121/CNCPT/consist.htm#CNCPT121),我感觉自己正在获得很多高级信息,但是我不能确定将其应用于我的特定问题。

我将描述用例的简化版本,并且正在寻找令人信服的答案(最好是带有参考),以说明我如何构造事务以获得所需结果。 (请不要太在意我的问题中的语法,数据规范化甚至数据类型;这是一个草率的人,因此,如果您了解我的意思,请继续关注并发问题。):)

场景(简化):

许多用户(成千上万)正在同时玩在线游戏。玩家都是两个团队的成员,红色或蓝色。每次玩家完成游戏时,我们都希望记录用户,他们的团队隶属关系,时间戳和得分。我们希望汇总每支球队有史以来的最高分。我们的数据模型如下:

// each game is logged in a table that looks kind of like this:
GAMES {
 time NUMBER,
 userid NUMBER,
 team NUMBER,
 score NUMBER
}
// high scores are tracked here, assume initial 0-score was inserted at time 0
HIGH_SCORES {
 team NUMBER,
 highscore NUMBER
}

因此,对于我收到的每份分数报告,我都会执行如下所示的交易

BEGIN
  UPDATE HIGH_SCORES set highscore=:1 WHERE team=:2 and :1>highscore;
  INSERT into GAMES (time, userid, team, score) VALUES (:1,:2,:3,:4);
COMMIT

我希望保留的不变性是,在任何时候,如我要扫描GAMES表并找到高分,HIGH_SCORES表中显示的每个团队的高分将是最高分困难的方式。

我对READ_COMMITED隔离级别的了解表明,这不能让我得到想要的东西:

  

已读提交的事务中的写冲突

     

在已提交的已读事务中,当事务尝试更改由未提交的并发事务(有时称为阻塞事务)更新的行时,发生冲突的写操作。读取的已提交事务将等待阻塞事务结束并释放其行锁。

     

选项如下:

     
      
  • 如果阻止事务回滚,则等待的事务将继续更改先前锁定的行,就像其他事务不存在一样。

  •   
  • 如果阻塞事务提交并释放其锁,则等待的事务将其预期的更新继续到新更改的行。

  •   

在我看来,如果红队(第1队)的得分为100,并且两个玩家同时提交了更好的分数,那么多线程服务器可能会同时进行两个数据库事务:

# Transaction A
UPDATE HIGHSCORES set highscore=150 where team=1 and 150>highscore;
INSERT into GAMES (time, userid, team, score) VALUES (9999,100,1,150);

# Transaction B
UPDATE HIGHSCORES set highscore=125 where team=1 and 125>highscore;
INSERT into GAMES (time, userid, team, score) VALUES (9999,101,1,125);

因此(在READ_COMMITED模式下,您可以按以下顺序进行操作:(请参见上面引用的Oracle链接中的表9-2)

A updates highscore for red team row; oracle locks this row

B still sees the 100 score and so tries to update red team highscore; 
  oracle Blocks trasaction B because that row is now locked with a conflicting write

A inserts into the games table;

A commits;

B is unblocked, and completes the update, clobbering the 150 with a 125 and my invariant condition will be broken.

第一个问题-这是对READ_COMMITED的正确理解吗?

但是我阅读的SERIALIZABLE:

  

仅当可序列化事务开始时已经提交了对其他事务所做的行更改时,Oracle数据库才允许可序列化事务修改行。当可序列化事务尝试更新或删除由可序列化事务开始后提交的另一个事务更改的数据时,数据库会生成错误。

建议可序列化在上述情况下也不会做正确的事,唯一的区别是事务B会出错,而我可以选择回滚或重试。这是可行的,但似乎不必要。

第二个问题-这是对SERIALIZABLE的正确理解吗?

...如果是的话,我很困惑。这似乎是一件简单,常见的事情。在代码中,我可以通过在每个团队的高分的测试和更新周围放置一个互斥对象来轻松完成此任务。

第三个也是最重要的问题:如何使Oracle(或任何SQL数据库)达到我想要的位置?

更新:进一步的阅读表明,我可能需要做一些显式的表锁定,如(https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_9015.htm)一样-但我不清楚我到底需要什么。 !!

2 个答案:

答案 0 :(得分:3)

哇,好长的问题。简短的答案是READ_COMMITTED就是您所需要的。

您不会丢失更新,因为事务B提交后,事务B执行的UPDATE重新启动UPDATE将从重新启动的时间点开始读取,而不是提交的时间点。

也就是说,在您的示例中,事务B将更新HIGH_SCORES中的0行。

Oracle概念指南的第9章中有一个很好的例子,展示了Oracle如何保护应用程序免受丢失的更新的影响。

Tom Kyte很好地解释了Oracle将如何以及为什么在内部重新启动UPDATE语句以实现读取一致性,https://asktom.oracle.com/pls/asktom/f?p=100:11:::::P11_QUESTION_ID:11504247549852

答案 1 :(得分:3)

  

这是对READ_COMMITED的正确理解吗?

不完全是。您的方案不是链接到的文档中表9-2中显示的内容。您的情况基本上就是表9-4中的内容。

区别在于9-2版本(显示丢失的更新)不会检查要更新的值-它不会根据要更新的现有薪水进行过滤。 9-4版本正在更新电话号码,但会在更新中查看该列的现有值,而被阻止的更新最终不会更新任何行,因为它会重新读取新更改的值-现在不会t匹配过滤器。

基本上,当删除锁上一个锁时,重新运行被阻止的更新,因此它重新读取新提交的数据,并用来确定是否现在需要更新该行。

该文档also says

  

锁满足以下重要的数据库要求:

     
      
  • 一致性
  •   
     

在用户完成操作之前,其他会话不得更改会话正在查看或更改的数据。

     
      
  • 诚信
  •   
     

数据和结构必须以正确的顺序反映对其所做的所有更改。

     

Oracle数据库通过其锁定机制提供了事务之间的数据并发性,一致性和完整性。锁定会自动发生,不需要用户采取任何行动。

最后两句话意味着您不必担心它,通过从两个会话中手动更新表中的同一行来验证行为非常容易。

automatic locks下:

  

DML锁(也称为数据锁)可确保多个用户同时访问的数据的完整性。例如,DML锁可防止两个客户购买在线书商提供的最后一本书。 DML锁可防止同时发生冲突的DML或DDL操作的破坏性干扰。

在您的情况下,当它重新启动在事务B中被阻止的更新时,找不到highscore小于125的团队1的行。针对执行的语句数据包括从会话A更新的提交,即使该提交发生在B首次确定并要求锁定该行之后(此时与它的过滤器匹配)。因此,没有任何要更新的内容,并且会话A的更新也不会丢失。