如何使用Spring Data JPA優(yōu)雅地實(shí)現(xiàn)樂(lè)觀鎖和悲觀鎖
在并發(fā)數(shù)據(jù)庫(kù)操作領(lǐng)域,處理數(shù)據(jù)完整性至關(guān)重要。 Spring Data 與 JPA(Java Persistence API)集成,提供樂(lè)觀和悲觀鎖定機(jī)制。
樂(lè)觀鎖: 樂(lè)觀鎖的基本思想是,認(rèn)為在大多數(shù)情況下,數(shù)據(jù)訪問(wèn)不會(huì)導(dǎo)致沖突。因此,樂(lè)觀鎖允許多個(gè)事務(wù)同時(shí)讀取和修改相同的數(shù)據(jù),而不進(jìn)行顯式的鎖定。在提交事務(wù)之前,會(huì)檢查是
否有其他事務(wù)對(duì)該數(shù)據(jù)進(jìn)行了修改。如果沒(méi)有沖突,則提交成功;如果發(fā)現(xiàn)沖突,就需要回滾并重新嘗試。
樂(lè)觀鎖通常使用版本號(hào)或時(shí)間戳來(lái)實(shí)現(xiàn)。每個(gè)數(shù)據(jù)項(xiàng)都會(huì)包含一個(gè)表示當(dāng)前版本的標(biāo)識(shí)符。在讀取數(shù)據(jù)時(shí),會(huì)將版本標(biāo)識(shí)符保存下來(lái)。在提交更新時(shí),會(huì)檢查數(shù)據(jù)的當(dāng)前版本是否與保存的版本匹配。如果匹配,則更新成功;否則,表示數(shù)據(jù)已被其他事務(wù)修改,需要處理沖突。
樂(lè)觀鎖適用于讀操作頻率較高、寫(xiě)操作沖突較少的場(chǎng)景。它減少了鎖的使用,提高了并發(fā)性能,但需要處理沖突和重試的情況。
悲觀鎖: 悲觀鎖的基本思想是,在數(shù)據(jù)訪問(wèn)期間假設(shè)會(huì)發(fā)生沖突,因此在訪問(wèn)數(shù)據(jù)之前就會(huì)對(duì)其進(jìn)行鎖定,阻止其他事務(wù)對(duì)該數(shù)據(jù)進(jìn)行修改。
悲觀鎖使用排他鎖(Exclusive Lock)來(lái)實(shí)現(xiàn)。當(dāng)一個(gè)事務(wù)對(duì)數(shù)據(jù)進(jìn)行修改時(shí),它會(huì)請(qǐng)求排他鎖,并且其他事務(wù)無(wú)法獲取相同的鎖直到該事務(wù)釋放鎖。這樣可以確保在任何時(shí)候只有一個(gè)事務(wù)能夠修改數(shù)據(jù),避免了沖突。
悲觀鎖適用于寫(xiě)操作頻率較高、寫(xiě)操作沖突較多的場(chǎng)景。它確保了數(shù)據(jù)的一致性和完整性,但可能降低并發(fā)性能,因?yàn)槠渌聞?wù)需要等待鎖的釋放。
選擇樂(lè)觀鎖還是悲觀鎖取決于具體的應(yīng)用場(chǎng)景和并發(fā)控制需求。樂(lè)觀鎖適合讀多寫(xiě)少、沖突較少的情況,而悲觀鎖適合寫(xiě)多讀少、沖突較多的情況。
Spring Data JPA 樂(lè)觀鎖
@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);
}
}
在上面的示例中,當(dāng)兩個(gè)線程同時(shí)嘗試更新同一產(chǎn)品的價(jià)格時(shí),第一個(gè)線程將成功更新該產(chǎn)品。但第二個(gè)線程將失敗,因?yàn)榘姹静黄ヅ洌瑨伋鯫bjectOptimisticLockingFailureException。
updatePrice方法生成的 SQL如下:
SELECT id, name, price, version FROM product WHERE id = ?
UPDATE product SET name = ?, price = ?, version = ? WHERE id = ? AND version = ?
原理如下:
- 在Product實(shí)體的version字段添加@Version注解。
- 讀取操作(如findById)是非阻塞的,可以由多個(gè)線程并行完成。他們不檢查也不關(guān)心版本列。
- 寫(xiě)入操作(如save)將檢查版本列,以確保數(shù)據(jù)自讀取以來(lái)未發(fā)生更改。如果另一個(gè)線程同時(shí)更新了數(shù)據(jù)(因此增加了版本號(hào)),則保存操作將失敗并顯示 ObjectOptimisticLockingFailureException。
如果你想確保讀取操作是最新的或在讀取時(shí)阻止其他操作,則需要采用悲觀鎖定策略,例如 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) 注解確保在調(diào)用 findByIdLocked 時(shí)獲得寫(xiě)鎖。
此處,@Transactional 注釋在調(diào)用 updatePrice 時(shí)啟動(dòng)新事務(wù)。如果該方法成功完成,則事務(wù)提交,如果拋出異常,則回滾。
生成的SQL:
用鎖獲取:
SELECT id, name, price FROM product WHERE id = ? FOR UPDATE
updatePrice SQL如下:
UPDATE product SET name = ?, price = ? WHERE id = ?
悲觀鎖提供了一種通過(guò)在事務(wù)的整個(gè)持續(xù)時(shí)間內(nèi)獲取鎖定來(lái)防止并發(fā)數(shù)據(jù)訪問(wèn)沖突的方法。此方法在高爭(zhēng)用場(chǎng)景中特別有用。然而,必須意識(shí)到死鎖的可能性以及對(duì)系統(tǒng)吞吐量的影響。正確的事務(wù)管理(如 @Transactional 所示)可確保操作的原子性。
結(jié)論:
在 Spring Data JPA 的事務(wù)管理和數(shù)據(jù)一致性方面,我們有兩種主要的鎖定策略可供使用:
- @Transactional+@Lock(LockModeType.PESSIMISTIC_WRITE):這種組合實(shí)現(xiàn)了悲觀鎖定方法。當(dāng)使用此配置執(zhí)行讀取操作時(shí),應(yīng)用程序?qū)㈡i定數(shù)據(jù)庫(kù)中的特定行,以防止其他事務(wù)修改它,直到當(dāng)前事務(wù)完成。雖然這確保了嚴(yán)格的一致性并防止沖突,但在某些情況下,由于等待釋放鎖的時(shí)間可能會(huì)降低吞吐量。
- @Version:該注解采用樂(lè)觀鎖定策略。這里,當(dāng)讀取數(shù)據(jù)時(shí),不應(yīng)用鎖。相反,在嘗試更新時(shí),Spring Data JPA 會(huì)檢查自上次讀取以來(lái)數(shù)據(jù)的版本是否已被另一個(gè)事務(wù)修改。如果發(fā)生此類修改,則會(huì)拋出 ObjectOptimisticLockingFailureException 。該策略假設(shè)沖突很少,并且大多數(shù)交易將在不受干擾的情況下進(jìn)行。
根據(jù)特定的用例和性能要求,開(kāi)發(fā)人員可以在悲觀鎖定和樂(lè)觀鎖定之間進(jìn)行選擇。每種方法都有其獨(dú)特的優(yōu)點(diǎn)和挑戰(zhàn)。該決定取決于并發(fā)數(shù)據(jù)訪問(wèn)的預(yù)期頻率以及管理數(shù)據(jù)一致性所需的嚴(yán)格程度。