如何使用Spring Data JPA優雅地實現樂觀鎖和悲觀鎖
在并發數據庫操作領域,處理數據完整性至關重要。 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 。該策略假設沖突很少,并且大多數交易將在不受干擾的情況下進行。
根據特定的用例和性能要求,開發人員可以在悲觀鎖定和樂觀鎖定之間進行選擇。每種方法都有其獨特的優點和挑戰。該決定取決于并發數據訪問的預期頻率以及管理數據一致性所需的嚴格程度。