乐观锁绝对安全吗?

11
使用乐观锁策略时,可以解决如下的并发问题:
| 第一个事务开始         |
|                                      |  
| 选择一行数据                     |
|                                      |  第二个事务开始
| 检查版本并更新该行数据   |
|                                      |  选择同一行数据 
| 提交事务                           |
|                                      |  检查版本并更新该行数据
|                                      |  
|                                      |  因为版本已过期所以回滚
但是,如果极少数情况下第二个事务的更新在第一个事务更新之后但在事务提交之前呢?
| 第一个事务开始         |
|                                      |  第二个事务开始
| 选择一行数据                     |
|                                      |  选择同一行数据 
| 检查版本并更新该行数据   |
|                                      |  检查版本并更新该行数据
| 提交事务                           |
|                                      |  因为版本已过期所以回滚 //会吗?
|                                      |  
|                                      |  
我进行了实验,发现第二个事务的更新无法读取“过期”的版本,因为第一个事务尚未提交。那么在这种情况下,第二个事务是否会失败?

@Adam Arold,感谢您告诉我这个格言。因为我不是以英语为母语的人,所以我在谷歌上搜索了一下 :) 但是,在我提到的情况下,乐观锁策略是否有效呢? - Yugang Zhou
如果真的很乐观,为什么要使用事务功能?更新本身会失败,不需要回滚。 - tia
@tia 在这个例子中,也许有或没有事务都可以。但有时我需要回滚对数据库的其他更改(例如,可能是对子表的一些插入)。 - Yugang Zhou
抱歉,那是对《星球大战》的参考,我必须提一下。 - Adam Arold
2个回答

3
您在问题中没有说明您实际使用的数据库系统,因此我不知道您系统的详细信息。
但是,在任何情况下,在乐观锁定系统下,一个进程不能只在执行更新语句时检查行版本,因为正是您所担心的问题。
对于完全串行化的孤立事务,每个进程在提交时必须原子地检查它检查和修改的所有行的行版本。因此,在您的第二种情况中,右侧进程将在尝试提交时才会检测到冲突(您没有包含右侧进程的这一步)。当它尝试提交时,它将检测到冲突并回滚。

谢谢您的回复。我们在生产环境中使用Oracle 10g,隔离级别为读已提交。 - Yugang Zhou
当进行提交时,它真的会失败吗?我的意思是,在使用乐观锁定时,如果数字检查失败,它不会抛出异常,而只是更新0行,开发人员会在受影响的行数等于0时抛出异常。 - Yugang Zhou
1
也许我可以进行另一个实验,在执行更新 SQL 但尚未提交事务之后将线程休眠,然后在另一个线程中更新同一行,然后唤醒第一个线程。 - Yugang Zhou
1
结果是,在试图提交事务时,它会抛出一个乐观锁定失败的异常。该结果基于Hibernate。但是如果我删除事务处理,它将以脏更新方式通过。所以在这种情况下,事务处理特性非常重要。稍后我会再次尝试使用iBATIS(在程序中检查乐观锁定)。 - Yugang Zhou

0

如你所知,乐观锁存在TOCTOU竞争条件:在提交决策和实际提交之前,有一个短暂的时间窗口,另一个事务可以修改数据。

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

enter image description here

你可以在更新语句之前获取行级别的(select for update)锁来实现这一点。
jOOQ 为您完成。在Hibernate中,您需要手动锁定行:
var pessimisticRead = new LockOptions(LockMode.PESSIMISTIC_READ);
session.buildLockRequest(pessimisticRead).lock(entity);

请注意,您无法在单个虚拟机上重现Hibernate中令人恼火的TOCTOU竞态条件。由于共享的持久上下文,Hibernate将平稳地解决此问题。当事务在不同的虚拟机上运行时,Hibernate无法帮助,您需要添加额外的锁定。

我使用MySQL 8.0进行了测试。第一个更新在行上放置了锁,导致第二个更新等待直到第一个事务提交。 - adrianboimvaser
在Postgres中,如果我们将OPTIMISTIC LOCK与REPEATABLE_READ隔离级别结合使用,就可以摆脱这个问题。在MySQL中,由于其实现REPEATABLE_READ级别的方式不完全符合标准,因此应该使用SERIALIZABLE。 - GionJh
这样的时间窗口是不存在的。通常乐观锁定是以一种方式实现的,至少Hibernate和eclipselink是这样做的,即版本的检查和更新与其他数据的更新一起在一个更新命令中进行,因此是原子的。 更新会锁定该行以防其他事务在提交或回滚之前进行操作。 数据库锁定发生在数据库中,与在多个虚拟机上运行的Hibernate无关。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接