Best practice-生產環境中加鎖的最佳實踐

請添加圖片描述

什么是死鎖?

場景:圖書館有兩個相鄰的儲物柜(柜子A和柜子B),小明和小紅需要同時使用這兩個柜子才能完成借書流程。

  1. 互斥資源
    每個柜子只有一把鑰匙,且一次只能被一人使用(資源不可共享)。
  2. 持有并等待
    • 小明拿到了柜子A的鑰匙,但他說:“我要等小紅用完柜子B的鑰匙,才能繼續操作。”
    • 小紅拿到了柜子B的鑰匙,但她說:“我要等小明用完柜子A的鑰匙,才能繼續操作。”
  1. 僵局形成
    兩人都死死攥著已有的鑰匙,同時等待對方手里的另一把鑰匙。結果兩人卡在原地,誰也無法完成借書流程。

死鎖的定義:線程A獲取到資源1,需要再次獲取到資源2被釋放以獲取到該資源,此時線程B獲取到了資源2,等待獲取資源1,兩線程進入了互相等待的狀態形成死鎖。

請添加圖片描述

業務中的死鎖

讓我們以電商系統中的「購物車庫存鎖定」場景為例,具體分析死鎖的觸發機制:當用戶A嘗試同時鎖定商品X和Y的庫存,而用戶B以相反順序(先鎖Y再鎖X)發起操作時,這兩個并發請求可能因資源競爭進入相互等待狀態——此時系統既無法完成庫存扣減,也無法釋放已占用的資源,形成典型的死鎖僵局。

MockData類初始化了商品的庫存,使用ConcurrentHashp模擬購物車和商品庫存信息,并提供了添加至購物車、清空購物車、扣減庫存等方法。

public class MockData {// 模擬購物車:用戶ID -> 商品列表 + 總價public static final ConcurrentHashMap<Long, Cart> Carts = new ConcurrentHashMap<>();// 模擬庫存:商品ID -> 庫存數量public static final ConcurrentHashMap<Long, AtomicInteger> Inventory = new ConcurrentHashMap<>();static {// 初始化庫存(商品1和2各有1件)Inventory.put(1L, new AtomicInteger(1));Inventory.put(2L, new AtomicInteger(1));}// 添加商品到購物車public static void addToCart(Long userId, Long productId, int quantity) {Carts.computeIfAbsent(userId, k -> new Cart()).addProduct(productId, quantity);}// 清空購物車public static void clearCart(Long userId) {Carts.remove(userId);}// 獲取庫存數量public static int getStock(Long productId) {return Inventory.getOrDefault(productId, new AtomicInteger(0)).get();}// 扣減庫存(原子操作)public static boolean decreaseStock(Long productId, int quantity) {return Inventory.getOrDefault(productId, new AtomicInteger(0)).compareAndSet(getStock(productId), getStock(productId) - quantity);}// 購物車類static class Cart {private final ConcurrentHashMap<Long, Integer> items = new ConcurrentHashMap<>();@Getterprivate int totalPrice = 0;/*** 購物車添加商品* @param productId 商品id* @param quantity 數量*/public void addProduct(Long productId, int quantity) {items.put(productId, items.getOrDefault(productId, 0) + quantity);totalPrice += quantity;}public void removeProduct(Long productId) {items.remove(productId);totalPrice -= items.getOrDefault(productId, 0);}}
}

以下代碼模擬庫存不足造成死鎖的場景:

@Slf4j
@Service
@EnableAsync
public class OrderService {// 死鎖場景(模擬庫存不足)public void createOrderDeadLock(Long userId, Long productId) {log.info("用戶:{},開始下單商品:{}", userId, productId);// 模擬購物車添加商品cartLock.lock();try {// 步驟2:檢查庫存(此時可能有其他線程扣減)if (MockData.getStock(productId) < 1) {log.error("用戶:{} 庫存不足,放棄訂單", userId);return;}// 模擬長時間業務操作(人為制造時間差)try {Thread.sleep(5000);} catch (Exception e) {log.error("異常", e);}// 步驟3:扣減庫存(實際業務場景需要原子操作)if (!MockData.decreaseStock(productId, 1)) {log.error("用戶:{} 庫存已被搶光,放棄訂單", userId);return;}log.info("用戶 {},下單成功", userId);MockData.clearCart(userId);} catch (Exception e) {log.error("下單失敗", e);} finally {cartLock.unlock();}}
}

在Controller層調用該方法,同時進行場景分析:

死鎖場景分析:

  • 核心邏輯:兩個用戶同時搶購同一商品,庫存僅剩1件。
  • 死鎖原因
    1. 線程1持有購物車鎖,等待庫存鎖。
    2. 線程2持有購物車鎖,等待庫存鎖。
    3. 雙方互相等待對方釋放鎖,形成循環等待。
    @GetMapping("/wrong/cert/lock")public void wrongCertLock() throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(2);// 用戶A嘗試購買商品1(庫存1)executor.submit(() -> orderService.createOrderLockBySequence(1L, 1L));// 用戶B嘗試購買商品1(庫存已不足)executor.submit(() -> orderService.createOrderLockBySequence(2L, 1L));// 等待觀察結果(死鎖表現為長時間無輸出)executor.shutdown();executor.awaitTermination(20, TimeUnit.SECONDS);}
}

使用工具調用該接口,并查看接口的輸出結果,接口響應時間5.08秒:

請添加圖片描述

在下單時使用購物車的全局鎖certLock時,存在兩個問題:

一.單鎖阻塞堆積(隱性"假死鎖")

當使用全局鎖 cartLock 時,所有下單請求必須串行執行。在高并發場景下:

  1. 第一個線程獲得鎖后執行5秒休眠
  2. 后續所有線程在 cartLock.lock()處排隊阻塞
  3. 線程堆積導致系統吞吐量驟降,最終表現類似"死鎖"

數據示例:

  • 假設QPS=100,5秒內會堆積500個等待線程
  • 實際業務處理能力被壓縮到0.2 TPS(每秒處理0.2個請求)

使用Arthasthread -b命令分析服務存在線程“死鎖”的情況和線程阻塞情況,同時Arthas支持查看阻塞位置的源碼。

請添加圖片描述

使用jad --jad --source-only命令查看源碼,如例子中展示第40行附近存在線程阻塞的問題,我們可以通過反編譯查看源碼:

jad --source-only com.codetree.business_error.chapter.chapter02.shop.OrderService createOrderDeadLock

請添加圖片描述

二、鎖粒度錯位導致的競態條件
隱患根源:
// 非原子操作
if (MockData.getStock(productId) < 1) {return;
}
// 非原子操作
MockData.decreaseStock(productId, 1)    

即使有全局鎖保護:

  1. 庫存檢查與扣減分離:其他系統(如支付系統)可能同時修改庫存
  2. 超賣風險:檢查時庫存充足,但扣減時已被其他通道(API/后臺)修改

如何避免死鎖?

避免死鎖一般有兩種方案:

方案實現方式優點缺點
一次性獲取資源使用全局鎖(globalLock簡單粗暴,徹底避免死鎖并發性能差,所有請求串行執行
按順序獲取資源固定鎖順序(先購物車 → 再庫存)兼顧并發性能,適用于復雜業務需全局統一鎖順序策略
方案一、一次性獲取所有資源

一次性獲取所有資源可以視為將多個非原子性操作封裝成一個大的原子性操作,強制實現線程“串行化”訪問,該方案能夠徹底消除持有并等待條件,同時保證臨界區操作的原子一致性。

    public void createOrderLockAllResource(Long userId, Long productId) {log.info("優化:一次性獲取所有的資源,用戶:{},開始下單商品:{}", userId, productId);// 模擬購物車添加商品globalLock.lock();try {// 步驟2:檢查庫存(此時可能有其他線程扣減)if (MockData.getStock(productId) < 1) {log.error("用戶:{} 庫存不足,放棄訂單", userId);return;}// 模擬長時間業務操作(人為制造時間差)try {Thread.sleep(10000);} catch (Exception e) {log.error("異常", e);}// 步驟3:扣減庫存(實際業務場景需要原子操作)if (!MockData.decreaseStock(productId, 1)) {log.error("用戶:{} 庫存已被搶光,放棄訂單", userId);return;}log.info("用戶 {},下單成功", userId);MockData.clearCart(userId);} catch (Exception e) {log.error("下單失敗", e);} finally {globalLock.unlock();}}

未出現阻塞問題,串行化執行成功,接口響應10.10秒。

請添加圖片描述

方案二、按順序獲取資源

順序化獲取資源可以有效規避死鎖產生的必要條件(之一)——循環等待條件,同時消除進程間非原子操作的競爭沖突,從而避免競態條件的發生。

    public void createOrderLockBySequence(Long userId, Long productId) {log.info("優化:按順序獲取鎖,用戶:{},開始下單商品:{}", userId, productId);// 模擬購物車添加商品cartLock.lock();try {// 步驟2:獲取庫存鎖inventoryLock.lock();try {// 快速失敗if (MockData.getStock(productId) < 1) {System.out.println("用戶 " + userId + " 庫存不足,方案二無效");return;}// 執行所有操作MockData.addToCart(userId, productId, 1);MockData.decreaseStock(productId, 1);System.out.println("用戶 " + userId + " 方案二下單成功!");MockData.clearCart(userId);} catch (Exception e) {log.error("異常", e);throw new RuntimeException(e);} finally {inventoryLock.unlock();}} catch (Exception e) {log.error("下單失敗", e);} finally {cartLock.unlock();}}

請添加圖片描述

總結

在并發系統的設計與優化中,死鎖預防始終是確保系統穩定性的核心命題,我介紹的兩種死鎖的處理方式:

  1. "一刀切"的原子化方案
    通過全局鎖強制串行化操作,犧牲了并發性能,以最簡單的方式徹底消除死鎖風險。這種"粗暴但可靠"的設計思路,特別適合對數據一致性要求極高、容錯成本較大的業務場景,保證了基本的安全性。
  2. 精細化控制的順序化策略
    訪問資源順序化,投機取巧的利用了業務場景優勢,方案適合于對接口響應時間敏感的業務場景(下單搶購)。

實踐啟示錄:

  • 沒有銀彈的解決方案:兩種方案各有利弊,需根據業務特性進行取舍。高頻小事務場景宜用原子化方案,長流程多步驟業務則更適合順序化控制。
  • 死鎖預防≠完全消除:即使采取最優策略,仍需通過監控(如JVM線程Dump分析)、日志埋點(死鎖檢測)、壓力測試等手段持續驗證系統穩定性。

優秀的設計永遠是在理論模型與實際需求之間尋找精妙的平衡點。希望本文的分析能為你提供一些新的思路。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/897072.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/897072.shtml
英文地址,請注明出處:http://en.pswp.cn/news/897072.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

極狐GitLab 17.9 正式發布,40+ DevSecOps 重點功能解讀【四】

GitLab 是一個全球知名的一體化 DevOps 平臺&#xff0c;很多人都通過私有化部署 GitLab 來進行源代碼托管。極狐GitLab 是 GitLab 在中國的發行版&#xff0c;專門為中國程序員服務。可以一鍵式部署極狐GitLab。 學習極狐GitLab 的相關資料&#xff1a; 極狐GitLab 官網極狐…

黃昏時間戶外街拍人像Lr調色教程,手機濾鏡PS+Lightroom預設下載!

調色介紹 黃昏時分有著獨特而迷人的光線&#xff0c;使此時拍攝的人像自帶一種浪漫、朦朧的氛圍 。通過 Lr 調色&#xff0c;可以進一步強化這種特質并根據不同的風格需求進行創作。Lr&#xff08;Lightroom&#xff09;作為專業的圖像后期處理軟件&#xff0c;提供了豐富的調色…

Spring Boot 項目中 Redis 常見問題及解決方案

目錄 緩存穿透緩存雪崩緩存擊穿Redis 連接池耗盡Redis 序列化問題總結 1. 緩存穿透 問題描述 緩存穿透是指查詢一個不存在的數據&#xff0c;由于緩存中沒有該數據&#xff0c;請求會直接打到數據庫上&#xff0c;導致數據庫壓力過大。 解決方案 緩存空值&#xff1a;即使…

信息系統項目管理師--整合管理

信息系統項目管理師–整合管理

關于tomcat使用中瀏覽器打開index.jsp后中文顯示不正常是亂碼,但英文正常的問題

如果是jsp文件就在首行加 “<% page language"java" contentType"text/html; charsetUTF-8" pageEncoding"UTF-8" %>” 如果是html文件 在head標簽加入&#xff1a; <meta charset"UTF-8"> 以jsp為例子&#xff0c;我們…

微服務的春天:基于Spring Boot的架構設計與實踐

微服務的春天:基于Spring Boot的架構設計與實踐 在如今的技術領域,微服務架構儼然成為了解決復雜系統開發與運維挑戰的關鍵利器。作為一名資深運維和自媒體創作者,筆名Echo_Wish,我將深入探討基于Spring Boot的微服務架構設計,結合實例代碼說明觀點,希望能為大家帶來啟發…

JVM參數調整

一、內存相關參數 1. 堆內存控制 -Xmx&#xff1a;最大堆內存&#xff08;如 -Xmx4g&#xff0c;默認物理內存1/4&#xff09;。-Xms&#xff1a;初始堆內存&#xff08;建議與-Xmx相等&#xff0c;避免動態擴容帶來的性能波動&#xff09;。-Xmn&#xff1a;新生代大小&…

AVM 環視拼接 魚眼相機

https://zhuanlan.zhihu.com/p/651306620 AVM 環視拼接方法介紹 從內外參推導IPM變換方程及代碼實現&#xff08;生成AVM環視拼接圖&#xff09;_avm拼接-CSDN博客 經典文獻閱讀之--Extrinsic Self-calibration of the Surround-view System: A Weakly... (環視系統的外參自…

【哇! C++】類和對象(三) - 構造函數和析構函數

目錄 一、構造函數 1.1 構造函數的引入 1.2 構造函數的定義和語法 1.2.1 無參構造函數&#xff1a; 1.2.2 帶參構造函數 1.3 構造函數的特性 1.4 默認構造函數 二、析構函數 2.1 析構函數的概念 2.2 特性 如果一個類中什么成員都沒有&#xff0c;簡稱為空類。 空類中…

【五.LangChain技術與應用】【11.LangChain少樣本案例模板:小數據下的AI訓練】

深夜的創業孵化器里,你盯著屏幕上的醫療AI項目,手里攥著僅有的97條標注數據——這是某三甲醫院心內科攢了三年的罕見病例。投資人剛剛發來最后通牒:“下周demo要是還分不清心肌炎和感冒,就撤資!” 這時你需要掌握的不是更多數據,而是讓每個樣本都變成會復制的孫悟空的毫毛…

2005-2019年各省城鎮人口數據

2005-2019年各省城鎮人口數據 1、時間&#xff1a;2005-2019年 2、來源&#xff1a;國家統計局、統計年鑒 3、指標&#xff1a;地區、年份、城鎮人口(萬人) 4、范圍&#xff1a;31省 5、指標解釋&#xff1a;?城鎮人口是指居住在城市、集鎮的人口&#xff0c;主要依據人群…

Anaconda 部署 DeepSeek

可以通過 Anaconda 環境部署 DeepSeek 模型&#xff0c;但需結合 PyTorch 或 TensorFlow 等深度學習框架&#xff0c;并手動配置依賴項。 一、Anaconda 部署 DeepSeek 1. 創建并激活 Conda 環境 conda create -n deepseek python3.10 # 推薦 Python 3.8-3.10 conda activate…

Python 面向對象高級編程-定制類

目錄 __str__ __iter__ __getitem__ __getattr__ __call__ 小結 看到類似__slots__這種形如__xxx__的變量或者函數名就要注意&#xff0c;這些在Python中是有特殊用途的。 __slots__我們已經知道怎么用了&#xff0c;__len__()方法我們也知道是為了能讓class作用于len()…

MCP與RAG:增強大型語言模型的兩種路徑

引言 近年來&#xff0c;大型語言模型&#xff08;LLM&#xff09;在自然語言處理任務中展現了令人印象深刻的能力。然而&#xff0c;這些模型的局限性&#xff0c;如知識過時、生成幻覺&#xff08;hallucination&#xff09;等問題&#xff0c;促使研究人員開發了多種增強技…

IDEA Generate POJOs.groovy 踩坑小計 | 生成實體 |groovy報錯

一、無法生成注釋或生成的注釋是null 問題可能的原因&#xff1a; 1.沒有從表里提取注釋信息&#xff0c;修改def calcFields(table)方法即可 def calcFields(table) {DasUtil.getColumns(table).reduce([]) { fields, col ->def spec Case.LOWER.apply(col.getDataType().…

ue5.5崩潰報gpu錯誤快速修復注冊表命令方法

網上已經有很多方法了&#xff0c;自己寫了個regedit比處理dos批處理命令&#xff0c;啟動時需要win 管理員身份拷貝后&#xff0c;將以下代碼&#xff0c;保存為 run.bat格式批處理文件&#xff0c;右鍵鼠標&#xff0c;在彈出菜單中&#xff0c;選擇用管理員身份運行。即可。…

能量石[算法題]

題目來源&#xff1a;第十五屆藍橋杯大賽軟件賽省賽Java 大學 B 組&#xff08;算法題&#xff09; 可以參考一下&#xff0c;本人也是比較菜 不喜勿噴&#xff0c;求求求 import java.util.Scanner;?public class Main {public static void main(String[] args) {Scanner s…

馬爾科夫不等式和切比雪夫不等式

前言 本文隸屬于專欄《機器學習數學通關指南》&#xff0c;該專欄為筆者原創&#xff0c;引用請注明來源&#xff0c;不足和錯誤之處請在評論區幫忙指出&#xff0c;謝謝&#xff01; 本專欄目錄結構和參考文獻請見《機器學習數學通關指南》 正文 統計概率的利劍&#xff1a;掌…

基于 STC89C52 的 8x8 點陣顯示漢字

一、引言 在電子信息顯示領域,漢字的直觀呈現為信息傳遞帶來極大便利。8x8 點陣雖顯示空間有限,但通過合理設計,能夠清晰展示一些常用、簡單的漢字,豐富電子設備的交互界面。STC89C52 單片機作為一款經典且應用廣泛的微控制器,以其成本低廉、易于開發的特性,成為驅動 8x…

二進制、八進制、十進制和十六進制間的轉換(原理及工程實現)

在計算機科學和編程中&#xff0c;進制轉換是一個非常重要的基礎知識。無論是二進制、八進制、十進制還是十六進制&#xff0c;它們在不同的場景中都有廣泛的應用。本文將詳細介紹常用進制之間的轉換方法&#xff0c;并附上C語言示例代碼&#xff0c;幫助大家更好地理解和掌握這…