markdown-it
demo
Delete
Submit
clear
permalink
在 Spring Data JPA 中,如果你有「唯一資源(如序號)」要讓多個使用者同時競爭,必須採用**資料一致性與鎖的策略**來保護資源,否則會出現兩個使用者搶到同一筆資料的問題。 --- ## ✅ 解法總覽 | 解法方式 | 說明 | 適用情境 | | ---------------------------------- | --------------- | --------- | | 1️⃣ 悲觀鎖(Pessimistic Lock) | 直接在 DB 層鎖定資料 | 高併發、強一致性 | | 2️⃣ 樂觀鎖(Optimistic Lock) | 利用版本欄位做衝突檢查 | 衝突不頻繁 | | 3️⃣ 唯一性約束(Unique Constraint)+ 例外處理 | DB 保證唯一,應用層捕捉衝突 | 最終一致性即可 | | 4️⃣ 資料庫的 `SELECT FOR UPDATE` | 與悲觀鎖類似,使用原生 SQL | 自訂 SQL 邏輯 | 以下是具體實作方式 👇 --- ## 1️⃣ 使用「悲觀鎖」保護資源(最推薦) Spring Data JPA 支援 `@Lock(LockModeType.PESSIMISTIC_WRITE)`: ```java @Entity public class SerialNumber { @Id private Long id; private String code; private boolean used; // getters/setters } ``` ```java public interface SerialNumberRepository extends JpaRepository<SerialNumber, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT s FROM SerialNumber s WHERE s.used = false") List<SerialNumber> findAvailableSerialNumbers(Pageable pageable); } ``` ### 使用方式: ```java @Transactional public SerialNumber grabSerialNumber() { // 取得一筆未使用序號並上鎖(使用 LIMIT 1) List<SerialNumber> list = serialNumberRepository.findAvailableSerialNumbers(PageRequest.of(0, 1)); if (list.isEmpty()) throw new RuntimeException("No available serial numbers"); SerialNumber sn = list.get(0); sn.setUsed(true); return serialNumberRepository.save(sn); } ``` ### ✅ 優點: * 確保這筆序號在處理期間無法被別人搶走(DB 鎖定) * 適合 **搶號碼牌、搶票** 等業務場景 --- ## 2️⃣ 使用「樂觀鎖」加上版本控制 在 Entity 中加入 `@Version` 欄位: ```java @Entity public class SerialNumber { @Id private Long id; private String code; private boolean used; @Version private int version; } ``` ### 使用方式: 你只要正常更新 `SerialNumber`,JPA 會自動在更新時加上: ```sql UPDATE serial_number SET used = true, version = ? WHERE id = ? AND version = ? ``` 如果同時有兩人修改同一筆資料,第二人會拋出 `OptimisticLockingFailureException`。 ### 適合: * 衝突少(但不能容忍重複搶到) * 願意在應用層重試操作 --- ## 3️⃣ 靠唯一性約束阻擋重複搶奪 例如,建立一張「搶到序號的記錄表」,加上唯一索引(如 serial\_id): ```sql CREATE TABLE serial_claim ( serial_id BIGINT PRIMARY KEY, user_id BIGINT ); ``` 讓資料庫幫你保證「一個序號只能被插入一次」,然後應用層捕捉 `DuplicateKeyException`。 ### 適合: * 不需強一致性,但需保證唯一 * 可容忍寫入失敗後重試 --- ## 4️⃣ 使用原生 SQL + `FOR UPDATE`(更強控制) ```java @Query(value = "SELECT * FROM serial_number WHERE used = false LIMIT 1 FOR UPDATE", nativeQuery = true) SerialNumber lockAvailableSerialNumber(); ``` 搭配 `@Transactional` 使用,效果等同悲觀鎖。 --- ## 🧠 小總結 | 目標 | 做法 | | ------------- | --------------------- | | 確保序號被唯一取得且不重複 | 使用悲觀鎖或樂觀鎖 | | 想要可擴展、簡單錯誤處理 | 唯一鍵 + try-catch | | 極致控制(如鎖定順序) | 原生 SQL + `FOR UPDATE` | --- `@Lock(LockModeType.PESSIMISTIC_WRITE)` 是 Spring Data JPA 提供的一種**悲觀鎖**機制,可以在**查詢資料時同時對資料加鎖**,防止多個交易同時修改同一筆資料。 這種鎖定會轉換成資料庫的 `SELECT ... FOR UPDATE` 語句,只有當前交易完成(commit/rollback)後,其他交易才能讀寫被鎖的資料。 --- ## ✅ 基本用法 ### 1️⃣ Entity 範例 ```java @Entity public class SerialNumber { @Id private Long id; private String code; private boolean used; // getter/setter } ``` --- ### 2️⃣ Repository 中加上 @Lock ```java public interface SerialNumberRepository extends JpaRepository<SerialNumber, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT s FROM SerialNumber s WHERE s.used = false") List<SerialNumber> findFirstAvailable(Pageable pageable); } ``` --- ### 3️⃣ Service 層使用 ```java @Service public class SerialService { @Autowired private SerialNumberRepository repository; @Transactional public SerialNumber grabSerial() { // 限制只拿一筆未被使用的序號 List<SerialNumber> list = repository.findFirstAvailable(PageRequest.of(0, 1)); if (list.isEmpty()) { throw new RuntimeException("No available serial number."); } SerialNumber sn = list.get(0); sn.setUsed(true); // 標記為已使用 return repository.save(sn); } } ``` --- ## 🔒 注意事項 | 注意點 | 說明 | | -------------------- | ----------------------------------------------------------- | | 必須用 `@Transactional` | 否則鎖不會發生作用 | | 悲觀鎖會鎖整筆資料(行鎖) | 直到 transaction 結束前其他交易無法更新或讀取(depending on isolation level) | | 大量併發要小心死鎖 | 使用前先評估資料庫支援與設計結構 | | 非必要勿使用在全表查詢 | 因為可能導致鎖表或效能問題 | --- ## ✅ SQL 轉換說明 Spring Data JPA 在執行以下程式碼: ```java @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT s FROM SerialNumber s WHERE s.used = false") ``` 會在底層執行轉成: ```sql SELECT * FROM serial_number WHERE used = false FOR UPDATE; ``` --- ## 🧪 小測試情境(高併發搶序號) 你可以用 JMeter 或多執行緒測試同時叫用 `grabSerial()` 方法,確保每個人都只能搶到一組不重複的序號。 --- 如果你希望的是根據 ID 查詢後加鎖,範例如下: ```java @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<SerialNumber> findById(Long id); ``` 這也會轉成: ```sql SELECT * FROM serial_number WHERE id = ? FOR UPDATE; ``` ---
html
source
debug
Fork me on GitHub