我将从开发人员的角度提供答案。
我认为,当遇到诸如您描述的争用之类的行争用时,这是因为您的应用程序中存在一个错误。在大多数情况下,此类竞争是更新丢失漏洞的迹象。AskTom上的此线程解释了丢失更新的概念:
在以下情况下会丢失更新:
第一节:读出汤姆的员工记录
第二节:读出汤姆的员工记录
第一场:更新汤姆的员工记录
第二节:更新汤姆的员工记录
会话2将覆盖WRITE会话1的更改,而不会看到它们-导致更新丢失。
您遇到了丢失更新的一个讨厌的副作用:由于会话1尚未提交,因此会话2被阻止。但是,主要问题是会话2盲目更新记录。假设两个会话都发出以下语句:
UPDATE table SET col1=:col1, ..., coln=:coln WHERE id = :pk
在这两个语句之后,会话1的修改已被覆盖,而没有通知会话2该行已被会话1修改。
丢失更新(以及竞争副作用)永远不会发生,这是100%可以避免的。您应该通过两种主要方法使用锁定来防止它们:乐观锁定和悲观锁定。
1)悲观锁定
您要更新一行。在这种模式下,您将通过请求对该行进行锁定来防止其他人修改该行(SELECT ... FOR UPDATE NOWAIT
语句)。如果该行已经被修改,您将收到一条错误消息,您可以将其优雅地转换为最终用户(该行正在被其他用户修改)。如果该行可用,请进行修改(UPDATE),然后在事务完成时提交。
2)乐观锁
您要更新一行。但是,您不想维护该行的锁定,可能是因为您使用了多个事务来更新该行(基于Web的无状态应用程序),或者您不想让任何用户将锁定时间太长(这可能会导致其他人被阻止)。在这种情况下,您不会立即请求锁定。您将使用标记来确保在发布更新时该行没有更改。您可以缓存所有列的值,或者可以使用自动更新的时间戳列或基于序列的列。无论您选择哪种方式,当您要执行更新时,都可以通过发出以下查询来确保该行上的标记未更改:
SELECT <...>
FROM table
WHERE id = :id
AND marker = :marker
FOR UPDATE NOWAIT
如果查询返回一行,请进行更新。如果不是,则表示自您上次查询以来,某人已修改了该行。您必须从头开始重新启动该过程。
注意:如果您完全信任访问数据库的所有应用程序,则可以依靠直接更新进行乐观锁定。您可以直接发出:
UPDATE table
SET <...>,
marker = marker + 1
WHERE id = :id;
如果该语句没有更新任何行,则说明您知道有人更改了该行,因此您需要重新开始。
如果所有应用程序都同意该方案,那么您将永远不会被其他人阻止,并且可以避免盲目更新。但是,如果您不事先锁定该行,则在另一个应用程序,批处理作业或直接更新未实现乐观锁定的情况下,仍然容易受到不确定锁定的影响。这就是为什么我建议始终锁定行的原因,无论您选择哪种锁定方案(性能锁定都可以忽略不计,因为在锁定行时您检索了包括rowid在内的所有值)。
TL; DR
- 在没有锁定的情况下更新行会使应用程序潜在地“冻结”。如果对数据库的所有DML都实现了乐观或悲观锁定,则可以避免这种情况。
- 验证SELECT语句返回的值与任何以前的SELECT一致(以避免任何丢失的更新问题)