成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

高并發場景下Spring事務與JPA樂觀鎖重試機制導致的死鎖

開發 前端
在高并發場景中,庫存扣減是最典型的業務挑戰之一。當多個線程同時爭奪同一商品的庫存資源時,稍有不慎就會引發超賣(庫存扣減為負值)或死鎖(線程相互阻塞無法推進)等嚴重問題。

環境:SpringBoot3.4.0

1. 簡介

在高并發場景中,庫存扣減是最典型的業務挑戰之一。當多個線程同時爭奪同一商品的庫存資源時,稍有不慎就會引發超賣(庫存扣減為負值)或死鎖(線程相互阻塞無法推進)等嚴重問題。為解決超賣,開發者常采用JPA樂觀鎖(基于版本號機制)配合重試策略,然而這一方案在實際應用中暗藏陷阱——若事務管理與重試邏輯設計不當,反而會觸發更隱蔽的死鎖問題。

以一段典型的Spring Data JPA代碼為例:當多個線程同時扣減庫存時,樂觀鎖沖突會觸發ObjectOptimisticLockingFailureException,而開發者試圖通過遞歸重試恢復操作。然而,這種設計可能因以下原因導致系統崩潰:

  • 事務傳播機制缺陷
    重試未開啟獨立事務,導致行鎖長期未釋放,線程相互阻塞。
  • JPA一級緩存干擾
    未清理的緩存使重試讀取到臟數據,與數據庫真實狀態不一致。
  • 無限遞歸風險
    持續沖突引發棧溢出或資源耗盡。

本文將以代碼實踐為切入點,逐步分析在高并發場景下樂觀鎖引發的各種問題。

2. 實戰案例

2.1 準備環境

準備JPA對應的實體類

@Entity
@Table(name = "c_product")
@DynamicUpdate
public class Product {
  @Id
  private Long id;
  private String name;
  private Integer stock;
  @Version
  private Integer version;
  // getters, setters
}

c_product表準備如下數據

INSERT INTO `mall`.`c_product` (`id`, `name`, `stock`, `version`) 
  VALUES (1, 'Spring Boot3實戰案例100例', 2, 1);

圖片圖片

準備Repository接口

public interface ProductRepository extends 
  JpaRepository<Product, Long> {
}

扣減庫存Service初始方法

@Transactional
public void deductStock(Long productId, int quantity) {
  this.productRepository.findById(productId).ifPresentOrElse(p -> {
    if (p.getStock() >= quantity) {
      p.setStock(p.getStock() - quantity);
      productRepository.save(p);
    } else {
      throw new RuntimeException("庫存不足");
    }
  }, () -> {
    throw new RuntimeException("商品不存在");
  });
}

在這初始扣減庫存中我們還并沒有加入重試的邏輯。

單元測試用例

@Test
public void testDeductStock() throws Exception {
  final int MAX = 10 ;
  CountDownLatch cdl = new CountDownLatch(MAX) ;
  CyclicBarrier cb = new CyclicBarrier(MAX) ;
  for (int i = 0; i < MAX; i++) {
    new Thread(() -> {
      try {
        cb.await() ;
        this.productService.deductStock(1L, 1) ;
      } catch(Exception e) {
      } finally {
        cdl.countDown() ;
      }
    }, "T" + i).start() ;
  }
  cdl.await() ; 
  System.err.println("執行完成...") ;
}

測試用例中模擬了10個線程進行并發庫存扣減操作。

2.2 初始代碼測試

首先,我們測試初始代碼結果如下:

圖片圖片

執行更新動作時自動加入了version版本字段;數據庫中的數據如下:

圖片圖片

也就是只有一個線程扣減庫存成功,其它10個線程都發生了樂觀鎖異常。可是控制臺并沒有輸出異常啊,這是因為我們在單元測試中將異常吞了,修改測試用例代碼在catch中輸出異常,如下:

圖片圖片

控制臺輸出。

圖片圖片

9個線程都發生了并發修改樂觀鎖異常。

思考:我們在測試用例中進行了異常捕獲,那么我們能否在ProductService#deductStock方法中進行捕獲呢?以當前的代碼來看是不行的,也就是你想通過如下方式捕獲是不行的:

圖片圖片

或者是你將deductStock方法都進行try...catch也是不能捕獲的。因為樂觀鎖是在事務提交的時候進行檢查的。

接下來,我們要加入樂觀鎖異常后重試機制。

2.3 樂觀鎖重試版本1

我們將代碼修改如下:

圖片圖片

首先,我將原來的save方法修改為saveAndFlush;其次,catch捕獲了樂觀鎖異常并在其中自調用進行重試。

將方法修改為saveAndFlush會立即將update語句發送給db進行執行,這樣就能捕獲到樂觀鎖異常了。

如上代碼是否能改進行正確的庫存扣減呢?執行測試用例輸出如下:

圖片圖片

庫存不足,那么是不是數據庫中已經都扣減完了?

圖片圖片

查看數據庫,庫存還有,但控制臺確輸出的庫存不足,這又是為什么呢?我們在代碼中加入如下輸出:

圖片圖片

再次運行,輸出結果。

圖片圖片

這里我們以T5線程來說:

  • T5線程首次查詢stock=2,versinotallow=1;
  • 當update時由于其它線程已經修改了數據,版本發生了變化,所以T5線程拋出了樂觀鎖異常;
  • 接著,再次調用deductStock方法,但是并沒有執行select語句,這是由于JPA一級緩存導致(同一個線程EntityManager使用的同一個),但是輸出的stock=1,versinotallow=1,這是因為update雖然沒有成功,但是我們的代碼中在上一步中確對stock進行了減1操作。
  • 繼續執行update語句,由于已經有線程執行成功將version變成了2,所以這次還是拋出了樂觀鎖異常(此時,緩存中的stock進行了2次減1操作,已經變成了0)。
  • 最后,第三次調用的時候一級緩存中的stock已經變為0了,所以最終拋出了庫存不足;這時候的庫存不足是內存中沒有了,可數據庫真實的數據還是有的。
     

以上是當前版本1中出現的問題。接下來,我們將繼續修改代碼。

2.4 樂觀鎖重試版本2

通過上面的說明,我們知道了在同一個線程中查詢相同的數據第二次將會從緩存中直接返回,那是不是我們將緩存清理了就可以再次從數據庫中查詢最新的數據呢?修改代碼如下:

圖片

首先,我們注入了EntityManager對象;接著,在catch中進行了clear清理一級緩存。

測試結果如下:

圖片圖片

所有的線程無限循環(死循環了),并且通過輸出日志也確實每次都執行了SQL語句,那為什么輸出的stock=1,versinotallow=1呢?先來查看數據真實數據:

圖片

程序中查詢的與數據庫為什么不符?

原因很簡單,因為我們當前事務的隔離級別是可重復讀(REPEATABLE_READ),那么每次查詢的數據都將是快照讀(事務第一次查詢到的數據)。

接下來,我們將繼續修改代碼進行第三個版本的嘗試。

2.5 樂觀鎖重試版本3

既然知道了原因,那么我們是不是修改事務的隔離級別就可以了呢?代碼修改如下:

圖片圖片

在事務注解上將事務的隔離級別設置為讀已提交(默認是你當前數據庫中默認的隔離級別)。如上修改后再次運行測試用例:

圖片圖片

這次程序正常結束并且每次都從數據庫中查詢了最新的數據,但是又多了一個異常,意思是:"事務已被標記為僅回滾,因此已靜默回滾"。為什么?

這里我們以T3線程為例進行分析:

  • 首次執行由于已經被其它線程更新,所以拋出樂觀鎖異常。
  • 進入重試,重新查詢數據得到最新的stock=1,versinotallow=2,庫存大于0,進行扣減并執行update操作。
  • T3線程進行方法結束開始提交事務,這個異常就是在提交事務時拋出的。當第一次發生樂觀鎖異常的時候JPA內部就已經將當前的事務狀態設置為回滾,所以最終提交的時候當然執行回滾操作。
     

這時候又該如何解決呢?接下來,我們繼續修改代碼。

2.6 樂觀鎖重試版本4

既然知道了原因,那么我們是不是在每次重試的時候只要開啟一個新的事務就可以了呢?修改代碼如下:

圖片圖片

在上面的代碼中,我們修改了下面3點:

  • 將事務的傳播屬性設置REQUIRES_NEW,每次都開始一個新的;并且刪除了事務的隔離級別,每次都是新的也就沒有必要了。
  • 刪除了clear清理緩存的操作。
  • 自己注入自己調用deductStock方法,這是為了防止事務失效。
     

測試結果如下:

圖片圖片

死鎖了,等待默認的超時時間后結束,為什么?

這是因為當我們任何一個線程發生樂觀鎖異常進入重試階段時,雖然重試時開啟的是一個新的事務,但是之前的事務update操作還沒有結束(已經將id為1的記錄鎖定了,行鎖),當你再次開啟一個新事務執行update操作時那必須等待前一個事務結束,前一個事務肯定結束不了,因為我們是內部自己調用自己(遞歸調用),所以這里就產生了死鎖現象。

這該如何解決呢?接下來,我們將介紹幾種解決辦法。

2.7 樂觀鎖重試版本5

我們將代碼修改如下:

public void deductStock(Long productId, int quantity) {
  this.productRepository.findById(productId).ifPresentOrElse(p -> {
    if (p.getStock() >= quantity) {
      p.setStock(p.getStock() - quantity);
      try {
        this.productRepository.saveAndFlush(p) ;
      } catch (ObjectOptimisticLockingFailureException e) {
        System.err.println(Thread.currentThread().getName() + " - 樂觀鎖異常, " + e.getMessage()) ;
        deductStock(productId, quantity) ;
      }
    } else {
      throw new RuntimeException("庫存不足");
    }
  }, () -> {
    throw new RuntimeException("商品不存在");
  }) ;
}

注意觀察上面的代碼,我們做了如下的修改:

  • 方法上的事務注解刪除了。
  • 樂觀鎖異常處理中,我們直接調用了當前的方法。(因為當前的方法一級不是事務方法,不需要自己注入自己進行調用)

執行結果如下:

圖片圖片

數據庫庫存及版本變化:

圖片圖片

數據也正確了。

上面的代碼中不管你用save還是saveAndFlush都是可以的。因為每一次的重試調用都是開啟的一個新事務。

責任編輯:武曉燕 來源: Spring全家桶實戰案例源碼
相關推薦

2024-01-04 18:01:55

高并發SpringBoot

2021-02-20 10:02:22

Spring重試機制Java

2022-11-14 08:19:59

重試機制Kafka

2017-07-02 16:50:21

2017-06-16 15:16:15

2023-07-18 09:24:04

MySQL線程

2024-09-25 08:32:05

2025-05-28 01:15:00

Golang重試機制

2020-07-06 08:03:32

Java悲觀鎖樂觀鎖

2020-07-19 15:39:37

Python開發工具

2025-02-26 10:49:14

2024-01-05 18:01:17

高并發策略程序

2022-05-06 07:44:10

微服務系統設計重試機制

2023-10-27 08:20:12

springboot微服務

2025-01-03 08:44:37

kafka消息發送策略

2023-12-26 08:59:52

分布式場景事務機制

2025-06-05 01:22:00

SpringGateway高并發

2023-07-05 08:18:54

Atomic類樂觀鎖悲觀鎖

2024-01-05 16:43:30

數據庫線程

2021-01-15 05:12:14

Java并發樂觀鎖
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美日在线 | 四虎在线视频 | 亚洲电影一级片 | 日本久久一区二区三区 | 久草免费电影 | 婷婷激情综合 | 国产成人av免费看 | 久久久成人一区二区免费影院 | 久久成人一区 | 精品一级毛片 | 一区二区三区四区国产 | 久在线| 国产精品久久久久久婷婷天堂 | 国产精品久久久久久久久久免费 | 国产乱码精品一区二三赶尸艳谈 | 91久久精品一区二区二区 | 成人三级网址 | 国产日韩久久 | 亚洲视频二区 | 欧美在线一区视频 | 亚洲精品视频在线播放 | 亚洲视频一区在线 | 韩日一区二区 | 国产激情视频网站 | 福利片在线观看 | 视频三区 | 久久精品亚洲 | 九九久久免费视频 | 午夜久草 | 日韩av电影院 | 成人不卡视频 | 人人草人人干 | 不卡一区| 亚洲国产aⅴ成人精品无吗 国产精品永久在线观看 | 日韩在线精品强乱中文字幕 | 一级特黄在线 | 国产欧美久久一区二区三区 | 精品香蕉一区二区三区 | 精品成人免费一区二区在线播放 | 国产精品日韩欧美一区二区三区 | 青青草网 |