当两个并发事务同时更新相同的数据库记录/列时,导致第一个更新被第二个事务以静默方式覆盖。数据库中的这种现象被称为经典的更新丢失问题。以下是丢失更新时的事件顺序-
当两个人尝试更新 JIRA 中的相同 Bug 时,会发生空中碰撞,JIRA 会优雅地处理它。JIRA的程序员知道如何防止丢失更新!
处理此场景的方法主要有两种:
乐观锁定
悲观锁定
我们将通过适当的示例一一讨论这两种方法,
所有用户/线程都可以同时读取数据,但是当多个线程尝试同时更新数据时,第一个线程将获胜,而所有其他线程将因 OptimisticLockException 而失败,他们必须再次尝试执行更新。因此,即使在并发使用的情况下,也不会静默丢失任何更新。
数据库中的每条记录都维护一个版本号。当第一个事务从数据库读取记录时,它也会收到版本。修改后,服务器将记录的版本号与数据库中的版本号进行比较,如果未更改,则使用递增的版本号写入记录。
然后,第二笔交易将以相同的记录编号进入(即两个客户端都在更新该记录)。这一次,服务器将识别出数据库中的版本号在第一次事务中已更改,并拒绝更新。
JPA 乐观锁定允许任何人读取和更新实体,但是在提交时会进行版本检查,如果自上次读取实体以来在数据库中更新了版本,则会引发异常。
要在 JPA 中为实体启用乐观锁定,只需对属性进行批注,如下面的代码示例所示@Version
- @Entity
- @Table (name="t_flight")
- public class Flight {
- @ID
- @GeneratedValue (strategy=GenerationType.AUTO)
- private int id;
-
- @Version
- private long version;
- ...
- }
| 只需两行代码即可在 JPA 中启用乐观锁定。 |
请务必注意,只有短、整型、长和时间戳字段才能使用 @Version 属性进行批注。
时间戳是一种不如版本号可靠的乐观锁定方式,但应用程序也可以将其用于其他目的。
- @Entity
- @Table ( name = "t_flight" )
- public class Flight implements Serializable {
- ...
- @Version
- public Date getLastUpdate() { ... }
- }
使用悲观锁定时,hibernate 会锁定记录供您独占使用,直到您提交事务。这通常是在数据库级别使用语句实现的。在这种情况下,任何其他尝试更新/访问同一记录的事务都将被阻止,直到第一个事务释放锁。SELECT … FOR UPDATE
此策略以性能为代价提供更好的可预测性,并且扩展性不大。在内部,它的行为类似于所有线程(读取和写入)对单个记录的顺序访问,这就是可扩展性是一个问题的原因。
通常,如果只是在事务级别(可序列化)指定适当的隔离级别,数据库将自动为您处理它。Hibernate为您提供了在新事务开始时获得独占悲观锁的选项。以下锁定模式可用于在休眠会话中获取悲观锁定。
使用支持该语法的数据库根据显式用户请求获取。SELECT … FOR UPDATE
根据明确的用户请求使用 ain Oracle 获取。SELECT … FOR UPDATE NOWAIT
请注意,使用上述 UPGRADE 模式,您希望修改加载的对象,并且在提交事务之前不希望任何其他线程在您在此过程中更改它。
- interface Flight extends Repository
{ -
- @Lock(LockModeType.PESSIMISTIC_WRITE)
- Flight findOne(Long id);
- }
| 从 Spring Data JPA 的 1.6 版开始,CRUD 方法支持@Lock,一旦您提交事务,锁就会自动释放。 |
- tx.begin();
- Flight w = em.find(Flight.class, 1L, LockModeType.PESSIMISTIC_WRITE);
- w.decrementBy(4);
- em.flush();
- tx.commit();