做過庫存扣款、積分更新這種涉及併發的操作沒有?一開始以為很簡單,後來才發現如果不懂鎖機制,資料一定會爆。悲觀鎖和樂觀鎖是兩種完全不同的思路,沒搞懂選錯一個,後期真的有夠慘。
悲觀鎖(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 private Long version; }
當你用 JPA 的 @Version 時,框架會自動:
SELECT 時取出 version
UPDATE 時檢查 WHERE version = 原本的值
如果有人搶先改了,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:樂觀鎖的衝突重試不是自動的
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) { 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 隔了幾秒,中間版本變了很多次,衝突機率超高。
最後的話 大多數互聯網應用場景,用樂觀鎖就夠了。庫存扣款、積分更新這類,讀的人遠多於改的人,樂觀鎖真的香。但如果真的是高頻寫入的業務(比如行情報價更新每秒千次),悲觀鎖反而更穩。
關鍵是要根據自己的場景選擇,不要盲目跟風。線上踩坑一次真的會疼。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️