做過庫存扣款、積分更新這種涉及併發的操作沒有?一開始以為很簡單,後來才發現如果不懂鎖機制,資料一定會爆。悲觀鎖和樂觀鎖是兩種完全不同的思路,沒搞懂選錯一個,後期真的有夠慘。

悲觀鎖(Pessimistic Lock)

悲觀鎖的思路就是:「我相信一定會有人跟我搶」,所以先佔著位子再說。

SQL 寫法

最常見就是 SELECT … FOR UPDATE:

1
2
3
4
5
6
7
8
9
10
11
@Repository
public class InventoryRepository {

@Autowired
private JdbcTemplate jdbcTemplate;

public Integer getInventoryWithLock(Long productId) {
String sql = "SELECT quantity FROM inventory WHERE product_id = ? FOR UPDATE";
return jdbcTemplate.queryForObject(sql, new Object[]{productId}, Integer.class);
}
}

執行這條 SQL 時,DB 會對這一列上 exclusive lock,其他人想讀或寫都得等。等你 commit 或 rollback 了,鎖才會解開。

JPA 寫法

JPA 提供了 @Lock annotation:

1
2
3
4
5
6
7
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findByIdWithLock(@Param("id") Long id);
}

用 PESSIMISTIC_WRITE 就會生成 FOR UPDATE 語句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class OrderService {

@Autowired
private ProductRepository productRepository;

@Transactional
public void placeOrder(Long productId, Integer quantity) {
// 先鎖住這個商品
Product product = productRepository.findByIdWithLock(productId);

if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
}

悲觀鎖的優缺點

優點:

  • 簡單直接,邏輯清楚
  • 對寫入頻繁的場景效能最好,因為避免了大量衝突重試
  • 保證資料一致性

缺點:

  • 效能差,特別是高併發時,大量請求會排隊等 lock
  • 容易造成 deadlock(特別是多個業務同時鎖多列時)
  • 吞吐量低,DB 連線容易滿

實務上,如果你的場景是「大量使用者搶同一個資源」(比如搶票),悲觀鎖會變成瓶頸。

樂觀鎖(Optimistic Lock)

樂觀鎖的思路就是:「衝突很少啦,先改再檢查」。不依賴 DB 的 lock 機制,而是用版本號或時間戳來檢查。

版本號方案

最常用的就是加一個 version 欄位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
@Table(name = "product")
public class Product {

@Id
private Long id;

private String name;
private Integer stock;

@Version // JPA 會自動處理版本檢查
private Long version;

// getter, setter
}

當你用 JPA 的 @Version 時,框架會自動:

  1. SELECT 時取出 version
  2. UPDATE 時檢查 WHERE version = 原本的值
  3. 如果有人搶先改了,WHERE 不會匹配,拋 OptimisticLockException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class OrderService {

@Autowired
private ProductRepository productRepository;

@Transactional
public void placeOrder(Long productId, Integer quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));

if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
try {
productRepository.save(product);
} catch (OptimisticLockException e) {
// 版本衝突,重試或回應使用者
log.warn("Stock update conflict, please retry");
throw new StockUpdateException("Concurrent update detected");
}
}
}
}

時間戳方案(備選)

有些場景用 updated_at 時間戳當版本檢查:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "account")
public class Account {

@Id
private Long id;

private BigDecimal balance;

@Column(name = "updated_at")
@Temporal(TemporalType.TIMESTAMP)
private Date updatedAt;
}

更新時:

1
2
UPDATE account SET balance = balance - 100, updated_at = NOW()
WHERE id = 123 AND updated_at = '2025-09-08 10:00:00'

如果 updatedAt 不符,代表被人改過,return 0 rows,你知道衝突了。

樂觀鎖的優缺點

優點:

  • 效能好,不需要 DB lock,高併發時吞吐量大
  • 不會 deadlock
  • 讀操作完全不受影響

缺點:

  • 衝突時要自己處理 retry 邏輯
  • 如果衝突很頻繁(寫入很多),會陷入重試地獄
  • 需要維護 version 欄位

實務比較表

項目 悲觀鎖 樂觀鎖
DB Lock 有(FOR UPDATE) 無(靠版本號)
效能(讀多寫少)
效能(寫多讀少) 差(衝突多)
Deadlock 風險
實現難度 簡單 中等(要處理衝突)
吞吐量

選擇建議

用悲觀鎖的場景

  1. 寫入非常頻繁 - 樂觀鎖會瘋狂重試
  2. 資料高度競爭 - 比如搶票、秒殺
  3. 簡單業務 - 邏輯簡單,不想處理衝突

用樂觀鎖的場景

  1. 讀多寫少 - 大部分業務都是這樣
  2. 高併發 - 吞吐量需求大
  3. 分散式系統 - 跨機器時悲觀鎖更難
  4. 能接受衝突重試 - 業務邏輯能容忍失敗重來

實務踩坑

坑 1:樂觀鎖的衝突重試不是自動的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 錯誤做法 - 不會自動重試
try {
product.setStock(product.getStock() - 1);
productRepository.save(product);
} catch (OptimisticLockException e) {
// 拋 exception,該請求失敗
throw e;
}

// 正確做法 - 手動重試
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
product = productRepository.findById(productId).get();
product.setStock(product.getStock() - 1);
productRepository.save(product);
return;
} catch (OptimisticLockException e) {
if (i == maxRetries - 1) throw e;
Thread.sleep(10 * (i + 1)); // 指數退避
}
}

坑 2:長交易 + 樂觀鎖 = 衝突地獄

如果一個交易超級久,從 SELECT 到 UPDATE 隔了幾秒,中間版本變了很多次,衝突機率超高。

最後的話

大多數互聯網應用場景,用樂觀鎖就夠了。庫存扣款、積分更新這類,讀的人遠多於改的人,樂觀鎖真的香。但如果真的是高頻寫入的業務(比如行情報價更新每秒千次),悲觀鎖反而更穩。

關鍵是要根據自己的場景選擇,不要盲目跟風。線上踩坑一次真的會疼。