使用Spring中的@Version实现乐观锁

简介

乐观锁是数据库和应用程序中使用的一种并发控制策略,用于在多个事务尝试更新单个记录时确保数据完整性。Java Persistence API (JPA) 提供了一种借助@Version注解在 Java 应用程序中实现乐观锁的机制。在基于 Spring 的应用程序的上下文中,与扩展的 Spring Data JPA 库结合使用时,他就发挥了作用。

了解乐观锁

在数据库和应用程序开发领域,维护数据的完整性和一致性至关重要。这种情况下遇到的主要挑战之一是处理对共享数据资源的并发访问,尤其是在涉及更新时。在分布式系统中,这个问题就变得更加严重,其中多个用户或服务可能会尝试同时修改同一条数据。乐观锁是一种旨在管理并发访问而无需进行严格锁定的策略。

什么是乐观锁?

从本质上讲,乐观锁是在首次读取或访问数据记录进行潜在更新,但实际上并未锁定数据记录。使系统在乐观的假设下运行,即冲突很少且不会经常发生。仅当即将提交实际更新时,系统才会检查冲突。如果数据同时被另一个事务更改,则当前更新失败。

悲观锁与乐观锁

悲观锁: 在很多方面与乐观锁相反,一旦事务开始就锁定数据,从而防止任何其他事务在第一个事务完成之前访问相同的数据。虽然这保证了数据完整性,但这样做的代价是潜在的瓶颈、吞吐量降低和死锁的风险。

乐观锁: 乐观锁,如前所述,允许多个事务并发访问数据,只在最后一刻检查冲突。这使得它适合读操作远多于写操作并且数据冲突很少的场景。

底层机制

乐观锁的机制通常涉及系统的版本控制。数据库中的每个记录或实体都有一个关联的版本号或时间戳。当事务读取记录时,它还会记下其版本。在事务提交更新之前,它会检查数据库中的当前版本是否与其之前记录的版本匹配。如果它们匹配,则更新完成,并且版本号递增。如果不是,则检测到冲突,并且更新失败。

乐观锁的好处

  1. 增强吞吐量: 由于在事务持续时间的大部分时间内没有持有锁,因此等待时间最少。这导致同时处理更多的交易。
  2. 最小化死锁: 死锁是一种事务无限期地等待其他人锁定的资源的情况,这种情况的可能性要小得多,因为数据不会长时间锁定。
  3. 更好的可扩展性: 随着分布式系统和微服务架构的兴起,乐观锁定在确保系统能够有效扩展而无需管理复杂锁机制的开销方面发挥着关键作用。

缺点

  1. 冲突管理开销: 在冲突频繁的场景中,管理和解决冲突可能会占用大量资源。
  2. 复杂性: 实现乐观锁需要经过深思熟虑的设计,特别是在处理失败的事务时。
  3. 过时数据的可能性: 由于数据在读取时未锁定,因此事务可能会使用过时或过时的数据,如果管理不正确,可能会导致逻辑错误或不一致。

Spring @Version注解

Spring JPA(Java Persistence API)为开发人员提供了一组强大的工具来管理应用程序中的关系型数据。其最强大的功能之一是处理并发性并确保多用户环境中的数据完整性。这就是@Version注解发挥作用的地方,它提供了一种在 Spring JPA 中实现乐观锁的优雅方法。

@Version的作用

该@Version注解用于启用实体上的乐观锁,确保数据库中的数据更新不会出现并发修改问题。当实体中的某个字段标记为@Version时,JPA 将使用该字段来跟踪更改并确保一次只有一个事务可以更新特定行。

它是如何工作的?

每个用注解标记的实体都@Version将由 JPA 跟踪其版本。这是基本机制:

  1. 初始化: 当实体第一次被持久化(保存到数据库)时,版本字段(通常是整数或时间戳)被设置为其初始值,通常为零。
  2. 读取: 稍后获取实体时,JPA 会从数据库中检索当前版本。
  3. 更新: 在尝试更新或删除实体时,JPA 会根据实体的版本检查数据库中的当前版本。如果它们匹配,则操作继续,并且数据库中的版本增加(用于更新)。
  4. 冲突: 如果版本不匹配,则表明另一个事务同时更新了实体,导致 JPA 抛出OptimisticLockException.

应用

kotlin 复制代码
@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String author;

    @Version
    private int version;
}

在此设置中,版本字段跟踪每个图书实体的更改。如果两个用户检索同一本书,然后尝试同时更新它,则只有其中一个会成功。另一个将遇到 OptimisticLockException,因为数据库中书籍的版本号将因第一个用户的更新而增加。

主要优势

  1. 自动管理:一旦设置了 @Version,Spring JPA 就会自动管理版本检查,从而抽象出与手动并发控制相关的大部分复杂性。
  2. 增强的数据完整性:使用 @Version 后,可以确保数据不会被并发事务意外覆盖。
  3. 简化的错误处理:当出现冲突时,会通过明确定义的异常 (OptimisticLockException) 抛出异常,从而使错误处理变得简单。

在 Spring JPA 应用程序中实现 @Version

使用 Spring 的 @Version 注解非常简单,但在确保应用程序中的数据一致性时却非常重要。以下是如何逐步实施:

定义实体

第一步是向您的实体添加版本字段并使用 @Version 对其进行注解。该字段通常可以是整数或时间戳。

java 复制代码
@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @Version
    private int version;

}

在此设置中,JPA 将使用版本字段来跟踪每个产品实体的更改。

创建Repository

Spring Data JPA 提供了一种无需太多样板代码即可执行 CRUD 操作的方法。只需为您的实体创建一个扩展了 JpaRepository 的接口:

csharp 复制代码
public interface ProductRepository extends JpaRepository<Product, Long> {

}

执行CRUD操作

创建Repository后,执行 CRUD 操作变得轻而易举。

通过id获取产品:

kotlin 复制代码
@Autowired
ProductRepository productRepository;

public Product getProduct(Long id) {
    return productRepository.findById(id).orElse(null);
}

更新产品:

scss 复制代码
public void updateProductName(Long id, String newName) {
    Product product = productRepository.findById(id).orElseThrow(() -> new RuntimeException("Product not found"));
    product.setName(newName);
    productRepository.save(product);
}

如果在获取产品和尝试保存更新版本之间另一个事务已更改该产品,JPA 将抛出 OptimisticLockException,指示版本冲突。

处理冲突

使用 @Version 时,冲突由 OptimisticLockException 发出信号。捕获此异常并通过重试操作、通知用户或任何其他适合您的应用程序逻辑的纠正措施进行适当处理是至关重要的。

typescript 复制代码
@Transactional
public void updateProductNameWithConflictHandling(Long id, String newName) {
    try {
        Product product = productRepository.findById(id).orElseThrow(() -> new RuntimeException("Product not found"));
        product.setName(newName);
        productRepository.save(product);
    } catch (OptimisticLockException e) {
//处理异常,重试或者通知用户
    }
}

处理乐观锁异常

使用 @Version 注解的显著优点之一是它通过 OptimisticLockException 清楚地发出冲突信号。虽然异常对于标记并发修改非常有价值,但处理它的方式会显著影响用户体验和应用程序可靠性。

了解异常

在深入研究处理策略之前,了解 OptimisticLockException 的本质至关重要。当 JPA 在更新或删除操作期间检测到版本不匹配时,JPA 会抛出此异常,表明自上次访问以来尝试修改的数据已被另一个事务更改。

基本处理:通知用户

最直接的方法是捕获异常并通知用户发生了冲突。此策略通常用于冲突很少且可以手动解决的应用程序。

csharp 复制代码
try {

} catch (OptimisticLockException e) {
    // 通知用户
    throw new ResponseStatusException(HttpStatus.CONFLICT, "数据被另一个事务更改");
}

高级处理:自动重试

在冲突更加频繁且手动解决不切实际的情况下,可以考虑实施自动重试

csharp 复制代码
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
    try {
        //执行更新
        break; //如果成功跳出循环
    } catch (OptimisticLockException e) {
        if (i == maxRetries - 1) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "正在重试");
        }
    }
}

高级处理:合并更改

另一种方法是获取实体的最新版本,合并更改,然后再次尝试更新。当需要保留多个来源的更改时,此方法非常有用。

scss 复制代码
try {
    // 执行更新
} catch (OptimisticLockException e) {
    // 获取最新的版本
    Product latestProduct = productRepository.findById(productId).orElseThrow();

// 合并更改。此步骤将根据您的应用程序逻辑而有所不同。
// 例如,您可能决定合并不冲突的字段或应用其他业务规则。
    // 保存最新的
    productRepository.save(latestProduct);
}

总结

利用Spring JPA 提供的@Version 注解实现乐观锁是确保并发数据更新的应用程序中数据完整性的一种简单方法。乐观锁对于提高吞吐量和减少死锁的优势很明显,但有效处理乐观锁定异常对于保持流畅的用户体验至关重要。通过实现重试机制或向用户提供反馈信息以做出明智的决策,您可以将乐观锁定无缝集成到 Spring JPA 应用程序中。

相关推荐
救救孩子把12 分钟前
深入理解 Java 对象的内存布局
java
落落落sss14 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
万物皆字节20 分钟前
maven指定模块快速打包idea插件Quick Maven Package
java
夜雨翦春韭27 分钟前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
我行我素,向往自由33 分钟前
速成java记录(上)
java·速成
一直学习永不止步39 分钟前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
邵泽明39 分钟前
面试知识储备-多线程
java·面试·职场和发展
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
煸橙干儿~~1 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
2401_854391081 小时前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端