防止使用春季休眠的数据库事务中的更新丢失

本文探讨了数据库中丢失更新的问题,包括乐观锁定和悲观锁定两种解决方法。乐观锁定利用版本号避免更新丢失,而悲观锁定则通过锁定记录确保数据一致性。

什么是丢失更新?

当两个并发事务同时更新相同的数据库记录/列时,导致第一个更新被第二个事务以静默方式覆盖。数据库中的这种现象被称为经典的更新丢失问题。以下是丢失更新时的事件顺序-

典型丢失更新方案的事件序列
JIRA的空中碰撞

当两个人尝试更新 JIRA 中的相同 Bug 时,会发生空中碰撞,JIRA 会优雅地处理它。JIRA的程序员知道如何防止丢失更新!

处理此场景的方法主要有两种:

  1. 乐观锁定

  2. 悲观锁定

我们将通过适当的示例一一讨论这两种方法,

乐观锁定方法

所有用户/线程都可以同时读取数据,但是当多个线程尝试同时更新数据时,第一个线程将获胜,而所有其他线程将因 OptimisticLockException 而失败,他们必须再次尝试执行更新。因此,即使在并发使用的情况下,也不会静默丢失任何更新。

它是如何工作的?

数据库中的每条记录都维护一个版本号。当第一个事务从数据库读取记录时,它也会收到版本。修改后,服务器将记录的版本号与数据库中的版本号进行比较,如果未更改,则使用递增的版本号写入记录。

然后,第二笔交易将以相同的记录编号进入(即两个客户端都在更新该记录)。这一次,服务器将识别出数据库中的版本号在第一次事务中已更改,并拒绝更新。

JPA 乐观锁定允许任何人读取和更新实体,但是在提交时会进行版本检查,如果自上次读取实体以来在数据库中更新了版本,则会引发异常。

如何在 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() { ... }
}

引擎盖下

在后台,JPA 将在每次成功提交时递增 Version 属性。

这会导致 SQL 如下所示(请注意,JPA 处理所有内容,其显示仅用于说明目的):

UPDATE Flight SET ..., version = version + 1 WHERE id = ? AND version = readVersion

乐观方法的利弊

  1. 乐观锁定的优点是没有数据库锁,这可以提供更好的可伸缩性。

  2. 缺点是用户或应用程序必须刷新并重试失败的更新。

悲观锁定方法

使用悲观锁定时,hibernate 会锁定记录供您独占使用,直到您提交事务。这通常是在数据库级别使用语句实现的。在这种情况下,任何其他尝试更新/访问同一记录的事务都将被阻止,直到第一个事务释放锁。SELECT …​ FOR UPDATE

此策略以性能为代价提供更好的可预测性,并且扩展性不大。在内部,它的行为类似于所有线程(读取和写入)对单个记录的顺序访问,这就是可扩展性是一个问题的原因。

通常,如果只是在事务级别(可序列化)指定适当的隔离级别,数据库将自动为您处理它。Hibernate为您提供了在新事务开始时获得独占悲观锁的选项。以下锁定模式可用于在休眠会话中获取悲观锁定。

锁定模式升级

使用支持该语法的数据库根据显式用户请求获取。SELECT …​ FOR UPDATE

LockMode.UPGRADE_NOWAIT

根据明确的用户请求使用 ain Oracle 获取。SELECT …​ FOR UPDATE NOWAIT

请注意,使用上述 UPGRADE 模式,您希望修改加载的对象,并且在提交事务之前不希望任何其他线程在您在此过程中更改它。

如何在 Spring Data JPA & Hibernate 中启用悲观锁定

使用 Spring 数据 JPA 的悲观锁定示例
interface Flight extends Repository<Flight, Long> {

  @Lock(LockModeType.PESSIMISTIC_WRITE)         
  Flight findOne(Long id);
}
从 Spring Data JPA 的 1.6 版开始,CRUD 方法支持@Lock,一旦您提交事务,锁就会自动释放。
使用 Spring & Hibernate 的悲观锁定
tx.begin();
Flight w = em.find(Flight.class, 1L, LockModeType.PESSIMISTIC_WRITE);
w.decrementBy(4);
em.flush();
tx.commit();

事务隔离级别和锁定模式有什么区别?

隔离级别会影响您看到的内容。 锁定模式会影响允许您执行的操作。

为什么不在 Java 级别而不是数据库/休眠级别处理并发?

当我们使用支持事务的数据库时,并发管理必须远离Java代码,而应该由数据库事务隔离级别和锁定策略来处理。造成这种情况的主要原因是——

  1. 它使我们的代码更简单,在数据库级别处理并发更容易。

  2. 如果在 JVM 级别管理并发性,则应用程序在移动到多个分布式 JVM 时将中断,因此解决方案将永远无法扩展。另一方面,数据库是单点入口,即使多个 JVM 正在调用并行请求,也可以处理并发。

  3. 即使您只有单个 JVM 设置,与您自己的手工编织同步 Java 代码相比,乐观锁定也可能产生更高的性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值