在并发数据库操作领域,处理数据完整性至关重要。 Spring Data 与 JPA(Java Persistence API)集成,提供乐观和悲观锁定机制。
乐观锁: 乐观锁的基本思想是,认为在大多数情况下,数据访问不会导致冲突。因此,乐观锁允许多个事务同时读取和修改相同的数据,而不进行显式的锁定。在提交事务之前,会检查是
否有其他事务对该数据进行了修改。如果没有冲突,则提交成功;如果发现冲突,就需要回滚并重新尝试。
乐观锁通常使用版本号或时间戳来实现。每个数据项都会包含一个表示当前版本的标识符。在读取数据时,会将版本标识符保存下来。在提交更新时,会检查数据的当前版本是否与保存的版本匹配。如果匹配,则更新成功;否则,表示数据已被其他事务修改,需要处理冲突。
乐观锁适用于读操作频率较高、写操作冲突较少的场景。它减少了锁的使用,提高了并发性能,但需要处理冲突和重试的情况。
悲观锁: 悲观锁的基本思想是,在数据访问期间假设会发生冲突,因此在访问数据之前就会对其进行锁定,阻止其他事务对该数据进行修改。
悲观锁使用排他锁(Exclusive Lock)来实现。当一个事务对数据进行修改时,它会请求排他锁,并且其他事务无法获取相同的锁直到该事务释放锁。这样可以确保在任何时候只有一个事务能够修改数据,避免了冲突。
悲观锁适用于写操作频率较高、写操作冲突较多的场景。它确保了数据的一致性和完整性,但可能降低并发性能,因为其他事务需要等待锁的释放。
选择乐观锁还是悲观锁取决于具体的应用场景和并发控制需求。乐观锁适合读多写少、冲突较少的情况,而悲观锁适合写多读少、冲突较多的情况。
Spring Data JPA 乐观锁
@Data
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private int version;
private String name;
private double price;
}
public interface ProductRepository extends JpaRepository<Product, Long> {}
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public void updatePrice(Long id, double newPrice) {
Product product = productRepository.findById(id).orElseThrow();
product.setPrice(newPrice);
productRepository.save(product);
}
}
在上面的示例中,当两个线程同时尝试更新同一产品的价格时,第一个线程将成功更新该产品。但第二个线程将失败,因为版本不匹配,抛出ObjectOptimisticLockingFailureException。
updatePrice方法生成的 SQL如下:
SELECT id, name, price, version FROM product WHERE id = ?
UPDATE product SET name = ?, price = ?, version = ? WHERE id = ? AND version = ?
原理如下:
- 在Product实体的version字段添加@Version注解。
- 读取操作(如findById)是非阻塞的,可以由多个线程并行完成。他们不检查也不关心版本列。
- 写入操作(如save)将检查版本列,以确保数据自读取以来未发生更改。如果另一个线程同时更新了数据(因此增加了版本号),则保存操作将失败并显示 ObjectOptimisticLockingFailureException。
如果你想确保读取操作是最新的或在读取时阻止其他操作,则需要采用悲观锁定策略,例如 PESSIMISTIC_READ 或 PESSIMISTIC_WRITE。
Spring Data JPA 悲观锁
@Data
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
}
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findByIdLocked(Long id);
}
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void updatePrice(Long id, double newPrice) {
Product product = productRepository
.findByIdLocked(id)
.orElseThrow(EntityNotFoundException::new);
product.setPrice(newPrice);
}
}
@Lock(LockModeType.PESSIMISTIC_WRITE) 注解确保在调用 findByIdLocked 时获得写锁。
此处,@Transactional 注释在调用 updatePrice 时启动新事务。如果该方法成功完成,则事务提交,如果抛出异常,则回滚。
生成的SQL:
用锁获取:
SELECT id, name, price FROM product WHERE id = ? FOR UPDATE
updatePrice SQL如下:
UPDATE product SET name = ?, price = ? WHERE id = ?
悲观锁提供了一种通过在事务的整个持续时间内获取锁定来防止并发数据访问冲突的方法。此方法在高争用场景中特别有用。然而,必须意识到死锁的可能性以及对系统吞吐量的影响。正确的事务管理(如 @Transactional 所示)可确保操作的原子性。
结论:
在 Spring Data JPA 的事务管理和数据一致性方面,我们有两种主要的锁定策略可供使用:
- @Transactional+@Lock(LockModeType.PESSIMISTIC_WRITE):这种组合实现了悲观锁定方法。当使用此配置执行读取操作时,应用程序将锁定数据库中的特定行,以防止其他事务修改它,直到当前事务完成。虽然这确保了严格的一致性并防止冲突,但在某些情况下,由于等待释放锁的时间可能会降低吞吐量。
- @Version:该注解采用乐观锁定策略。这里,当读取数据时,不应用锁。相反,在尝试更新时,Spring Data JPA 会检查自上次读取以来数据的版本是否已被另一个事务修改。如果发生此类修改,则会抛出 ObjectOptimisticLockingFailureException 。该策略假设冲突很少,并且大多数交易将在不受干扰的情况下进行。
根据特定的用例和性能要求,开发人员可以在悲观锁定和乐观锁定之间进行选择。每种方法都有其独特的优点和挑战。该决定取决于并发数据访问的预期频率以及管理数据一致性所需的严格程度。