【问题标题】:Is Optimistic-Locking absolutely safe?乐观锁定绝对安全吗?
【发布时间】:2025-12-19 16:25:07
【问题描述】:

当使用乐观锁策略时,可以解决如下并发问题:

|第一笔交易开始| | | |选择一行 | | |第二笔交易开始 |使用版本检查更新行 | | |选择同一行 |提交交易 | | |使用版本检查更新行 | | | |因为版本脏了所以回滚

但是,如果在极少数情况下,如果第二个事务中的更新是在第一个事务中的 udpate 之后但在事务提交之前呢?

|第一笔交易开始| | |第二笔交易开始 |选择一行 | | |选择同一行 |使用版本检查更新行 | | |使用版本检查更新行 |提交交易 | | |因为版本脏了所以回滚 // 会吗? | | | |

我做了一个实验,第二个事务中的更新无法读取“脏”版本,因为第一个事务尚未提交。这种情况下第二次交易会失败吗?

【问题讨论】:

  • @Adam Arold 谢谢你告诉我这句格言。我用谷歌搜索它,因为我不是以英语为母语的人:) 但是在我提到的情况下,乐观锁定策略会起作用吗?
  • 如果真的看好,你怎么用事务功能?更新将自行失败,无需回滚。
  • @tia 也许在示例中,有或没有事务都可以。但有时我需要将其他更改(例如,可能对子表的一些插入)回滚到数据库
  • 抱歉,这是对《星球大战》的引用,我不得不提一下。

标签: optimistic-locking


【解决方案1】:

正如您已经发现的那样,乐观锁定受制于TOCTOU 竞争条件:在提交决定和实际提交之前,有一个很短的时间窗口,在此期间另一个事务可以修改数据。

要使乐观锁定 100% 安全,您必须确保第二个事务等到第一个事务提交后才进行版本检查:

您可以通过在更新语句之前获取行级(选择更新)锁来实现这一点。

jOOQ does 给你。在 Hibernate 中,您必须手动锁定行:

var pessimisticRead = new LockOptions(LockMode.PESSIMISTIC_READ);
session.buildLockRequest(pessimisticRead).lock(entity);

请注意,您无法在单个 VM 上重现 Hibernate 中令人讨厌的 TOCTOU 竞争条件。由于共享的持久上下文,Hibernate 将顺利解决这个问题。当事务在不同的虚拟机上运行时,Hibernate 无法提供帮助,您必须添加额外的锁定。

【讨论】:

  • 我用 MySQL 8.0 测试了这个。第一次更新在行上加了一个锁,导致第二次更新等到第一个事务被提交。
  • 在 postgres 中,如果我们将 OPTIMISTIC LOCK 与 REPEATABLE_READ 隔离级别结合起来,我们可以摆脱这个问题。在 Mysql 中,您应该使用 SERIALIZABLE,因为它实现 REPEATABLE_READ 级别的方式不完全兼容。
【解决方案2】:

你没有在你的问题中说你实际使用的是什么数据库系统,所以我不知道你系统的细节。

但无论如何,在乐观锁定系统下,进程在执行更新语句时不能只检查行版本,因为正是您担心的问题。

对于完全可序列化的隔离事务,每个进程必须在提交时自动检查它检查和修改的所有行的行版本。因此,在您的第二种情况下,右手进程在尝试提交之前不会检测到冲突(右手进程未包括的步骤)。当它尝试提交时,它会检测到冲突并回滚。

【讨论】:

  • 感谢您的回复。生产环境使用Oracle 10g,隔离级别为read commited。
  • 尝试提交时真的会失败吗?我的意思是,当使用乐观锁定时数字检查失败时它不会抛出异常,它只是更新 0 行,当受影响的行等于 0 时,开发人员会抛出异常。
  • 也许我可以再做一个实验,在执行更新sql之后但在提交事务之前休眠线程并在另一个线程中更新同一行然后唤醒第一个线程。
  • 结果是它在尝试提交事务时抛出一个异常,告诉乐观锁定失败。结果基于 Hibernate。但是如果我删除事务,它将通过脏更新。因此,在这种情况下,事务功能非常重要。稍后我将再次尝试使用 iBATIS(以编程方式检查乐观锁定)