悲觀鎖 樂觀鎖

悲觀鎖 樂觀鎖

在沒有加鎖的秒殺場景下 每秒打進來的請求是巨大的 高并發場景下 我們發現不僅異常率高的可怕 庫存竟然還變成了負數 這產生的結果肯定是很大損失的 那為什么會出現超賣問題呢

我們假設有下面兩個線程

線程1查詢庫存,發現庫存充足,創建訂單,然后準備對庫存進行扣減,但此時線程2和線程3也進行查詢,同樣發現庫存充足,然后線程1執行完扣減操作后,庫存變為了0,線程2和線程3同樣完成了庫存扣減操作,最終導致庫存變成了負數!這就是超賣問題的完整流程

因此超賣產生了 那我們應該如何解決呢 很簡單 直接加鎖不就好了 加個互斥鎖 一個一個來 那么會出現所有人都在堵塞 秒殺變成小時殺了 肯定不行 所以我先介紹兩種鎖的機制

  1. 悲觀鎖,認為線程安全問題一定會發生,因此操作數據庫之前都需要先獲取鎖,確保線程串行執行。常見的悲觀鎖有:synchronized、lock
  2. 樂觀鎖,認為線程安全問題不一定發生,因此不加鎖,只會在更新數據庫的時候去判斷有沒有其它線程對數據進行修改,如果沒有修改則認為是安全的,直接更新數據庫中的數據即可,如果修改了則說明不安全,直接拋異常或者等待重試。常見的實現方式有:版本號法、CAS操作、樂觀鎖算法

接下來我們詳細分析樂觀鎖的實現方式:

版本號機制

版本號機制是樂觀鎖最常見的實現方式。每條數據都有一個版本號,每次更新數據時版本號加1。當線程A要更新數據時,先檢查當前版本號是否與自己獲取時的版本號一致,如果一致則更新,否則說明數據已被其他線程修改,更新失敗。

例如我們可以在商品表中增加一個version字段:

UPDATE product SET stock = stock - 1, version = version + 1 
WHERE id = #{id} AND version = #{version}

CAS (Compare And Swap)

CAS是樂觀鎖的另一種實現方式,它包含三個操作數:內存位置、預期原值和新值。執行CAS操作時,將內存位置的值與預期原值比較,如果相匹配,則將內存位置的值更新為新值。否則,不做任何操作。

public boolean decreaseStock(Long productId, Integer version) {// 查詢商品當前庫存和版本號Product product = productMapper.selectById(productId);if (product.getStock() <= 0) {return false; // 庫存不足}// 使用CAS更新庫存和版本號int result = productMapper.decreaseStockWithVersion(productId, product.getVersion(), product.getVersion() + 1);return result > 0;
}

兩種鎖的適用場景

悲觀鎖適用于:

  • 并發寫入多、臨界資源爭搶激烈的場景
  • 讀少寫多的場景
  • 要求數據強一致性的場景

樂觀鎖適用于:

  • 并發寫入少、沖突較少的場景
  • 讀多寫少的場景
  • 允許短時間數據不一致的場景

樂觀鎖解決超賣問題

首先我們要為 tb_seckill_voucher 表新增一個版本號字段 version ,線程1查詢完庫存,在進行庫存扣減操作的同時將版本號+1,線程2在查詢庫存時,同時查詢出當前的版本號,發現庫存充足,也準備執行庫存扣減操作,但是需要判斷當前的版本號是否是之前查詢時的版本號,結果發現版本號發生了改變,這就說明數據庫中的數據已經發生了修改,需要進行重試(或者直接拋異常中斷)

       **boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>().eq(SeckillVoucher::getVoucherId, voucherId).gt(SeckillVoucher::getStock, 0).setSql("stock = stock -1"));**

注意到這里**.gt(SeckillVoucher::getStock, 0)而不是eq(SeckillVoucher::getStock,voucher.getStock())**

其實是因為樂觀鎖的弊端 可能鎖住正常訂單 例如大家一起獲取庫存100 一個線程執行成功 其他線程扣減時 發現庫存為99 直接不干了 多個線程直接斷了 因此我們直接寫成第一種即可

一人一單超賣

很容易發現 在判斷訂單前加上邏輯即可

 int count = this.count(new LambdaQueryWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, ThreadLocalUtls.getUser().getId()));if (count >= 1) {// 當前用戶不是第一單return Result.fail("用戶已購買");}

通過測試,發現并沒有達到我們想象中的目標,一個人只能購買一次,但是發現一個用戶居然能夠購買8次。這說明還是存在超賣問題

問題原因:出現這個問題的原因和前面庫存為負數數的情況是一樣的,線程1查詢當前用戶是否有訂單,當前用戶沒有訂單準備下單,此時線程2也查詢當前用戶是否有訂單,由于線程1還沒有完成下單操作,線程2同樣發現當前用戶未下單,也準備下單,這樣明明一個用戶只能下一單,結果下了兩單,也就出現了超賣問題

解決方案:一般這種超賣問題可以使用下面兩種常見的解決方案

  1. 悲觀鎖
  2. 樂觀鎖

悲觀鎖解決超賣問題

    /*** 搶購秒殺券** @param voucherId* @return*/@Transactional@Overridepublic Result seckillVoucher(Long voucherId) {// 1、查詢秒殺券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2、判斷秒殺券是否合法if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 秒殺券的開始時間在當前時間之后return Result.fail("秒殺尚未開始");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 秒殺券的結束時間在當前時間之前return Result.fail("秒殺已結束");}if (voucher.getStock() < 1) {return Result.fail("秒殺券已搶空");}// 3、創建訂單Long userId = ThreadLocalUtls.getUser().getId();synchronized (userId.toString().intern()) {// 創建代理對象,使用代理對象調用第三方事務方法, 防止事務失效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(userId, voucherId);}}/*** 創建訂單** @param userId* @param voucherId* @return*/@Transactionalpublic Result createVoucherOrder(Long userId, Long voucherId) {
//        synchronized (userId.toString().intern()) {// 1、判斷當前用戶是否是第一單int count = this.count(new LambdaQueryWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, userId));if (count >= 1) {// 當前用戶不是第一單return Result.fail("用戶已購買");}// 2、用戶是第一單,可以下單,秒殺券庫存數量減一boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>().eq(SeckillVoucher::getVoucherId, voucherId).gt(SeckillVoucher::getStock, 0).setSql("stock = stock -1"));if (!flag) {throw new RuntimeException("秒殺券扣減失敗");}// 3、創建對應的訂單,并保存到數據庫VoucherOrder voucherOrder = new VoucherOrder();long orderId = redisIdWorker.nextId(SECKILL_VOUCHER_ORDER);voucherOrder.setId(orderId);voucherOrder.setUserId(ThreadLocalUtls.getUser().getId());voucherOrder.setVoucherId(voucherOrder.getId());flag = this.save(voucherOrder);if (!flag) {throw new RuntimeException("創建秒殺券訂單失敗");}// 4、返回訂單idreturn Result.ok(orderId);}}

這里有很多值得注意的小問題

  1. 鎖的范圍盡量小。synchronized盡量鎖代碼塊,而不是方法,鎖的范圍越大性能越低
  2. 鎖的對象一定要是一個不變的值。我們不能直接鎖 Long 類型的 userId,每請求一次都會創建一個新的 userId 對象,synchronized 要鎖不變的值,所以我們要將 Long 類型的 userId 通過 toString()方法轉成 String 類型的 userId,toString()方法底層(可以點擊去看源碼)是直接 new 一個新的String對象,顯然還是在變,所以我們要使用 intern() 方法從常量池中尋找與當前 字符串值一致的字符串對象,這就能夠保障一個用戶 發送多次請求,每次請求的 userId 都是不變的,從而能夠完成鎖的效果(并行變串行)
  3. 我們要鎖住整個事務,而不是鎖住事務內部的代碼。如果我們鎖住事務內部的代碼會導致其它線程能夠進入事務,當我們事務還未提交,鎖一旦釋放,仍然會存在超賣問題
  4. Spring的@Transactional注解要想事務生效,必須使用動態代理。Service中一個方法中調用另一個方法,另一個方法使用了事務,此時會導致@Transactional失效,所以我們需要創建一個代理對象,使用代理對象來調用方法。

讓代理對象生效的步驟:

①引入AOP依賴,動態代理是AOP的常見實現之一

    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

②暴露動態代理對象,默認是關閉的

@EnableAspectJAutoProxy(exposeProxy = true)

樂觀鎖解決

ALTER TABLE tb_voucher_order
ADD CONSTRAINT UNIQUE (user_id)

樂觀鎖解決方案其實更加優雅。我們可以通過在數據庫表中添加唯一約束來防止一人多單的問題。通過在user_id字段上添加唯一約束,當多個線程嘗試為同一用戶創建訂單時,數據庫會自動拒絕重復記錄,只有第一個提交的事務能夠成功。這種方式比使用悲觀鎖性能更好,因為它不需要額外的加鎖操作,而是利用了數據庫自身的特性來保證數據一致性。

要記得捕獲異常返回前端

try {save(order); // 可能會拋出 DuplicateKeyException} catch (DuplicateKeyException e) {return Result.fail("你已經搶過了");}

這樣即可

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

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

相關文章

如何使用Cisco DevNet提供的免費ACI學習實驗室(Learning Labs)?(Grok3 回答)

Cisco DevNet 提供的免費 ACI&#xff08;Application Centric Infrastructure&#xff09;學習實驗室&#xff08;Learning Labs&#xff09;是幫助用戶學習和實踐 Cisco ACI 技術&#xff08;包括 APIC 控制器&#xff09;的優秀資源&#xff0c;適合網絡工程師、開發者和準備…

Combine的介紹與使用

目錄一、Combine 框架介紹二、核心概念三、基礎使用示例3.1、創建 Publisher & 訂閱3.2、操作符鏈式調用3.3、Subject 使用&#xff08;手動發送值&#xff09;3.4、網絡請求處理3.5、組合多個 Publisher3.6、錯誤處理四、核心操作符速查表 Operator五、UIKit 綁定示例六、…

【Java筆記】七大排序

目錄1. 直接插入排序2. 希爾排序3. 選擇排序4. 堆排序(重要)5. 冒泡排序6. 快速排序&#xff08;重要&#xff09;6.1 Hoare 法6.1.1 Hoare 法優化6.2 挖坑法&#xff08;重點&#xff09;6.3 快速排序的非遞歸寫法7. 歸并排序海量數據的排序問題8. 總結1. 直接插入排序 時間復…

H.264編解碼(NAL)

在我們的日常生活中&#xff0c;比如有緩存電影或者是發送視頻的需求。如果沒有視頻壓縮&#xff0c;一部手機只能存幾分鐘視頻&#xff0c;1TB 硬盤也裝不下幾部電影&#xff0c;用 4G 網絡發一段 1 分鐘視頻&#xff0c;可能需要幾十分鐘&#xff08;甚至傳不完&#xff09;&…

新手向:Python自動化辦公批量重命名與整理文件系統

本文將詳細介紹如何使用Python實現一個強大的文件批量重命名與整理工具&#xff0c;幫助開發者自動化這一繁瑣過程。本教程面向Python初學者&#xff0c;通過一個完整的項目案例&#xff0c;講解文件系統操作的核心技術。我們將構建的工具將具備以下功能&#xff1a;基于正則表…

C++ 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例

C 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例一、左值右值1.左值2.右值二、左值引用右值引用1.左值引用2.右值引用總結三、integral_constant四、integral_constant的元模板使用案例1.求最大整數2.內存對齊alignof關鍵字元模板計算內存…

c++算法一

1.雙指針總結&#xff1a;1.復寫0這道題&#xff0c;告訴我們要正難其反&#xff0c;我們從后向前進行重寫&#xff0c;刪除某些數字的時候&#xff0c;我們可以從前向后遍歷&#xff0c;但是增加一些數字的時候會對后面的數據進行覆蓋&#xff0c;所以要從后向前進行2.快樂數涉…

LeetCode-283. 移動零(Java)

283. 移動零 給定一個數組 nums&#xff0c;編寫一個函數將所有 0 移動到數組的末尾&#xff0c;同時保持非零元素的相對順序。 請注意 &#xff0c;必須在不復制數組的情況下原地對數組進行操作。 示例 1: 輸入: nums [0,1,0,3,12] 輸出: [1,3,12,0,0] 示例 2: 輸入: n…

【數據庫】慢SQL優化 - MYSQL

一、數據庫故障的關鍵點 引起數據庫故障的因素有操作系統層面、存儲層面&#xff0c;還有斷電斷網的基礎環境層面&#xff08;以下稱為外部因素&#xff09;&#xff0c;以及應用程序操作數據庫和人為操作數據庫這兩個層面&#xff08;以下稱內部因素&#xff09;。這些故障中外…

桶排序算法深度剖析

&#x1f50d; 桶排序算法深度剖析 &#x1f3af; 核心原理圖解?? 完整算法流程 #mermaid-svg-LyB6SpaZ132X4Wtn {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-LyB6SpaZ132X4Wtn .error-icon{fill:#552222;}#mer…

對S32K144做的BMS安裝快速開發Simulink庫及BMS例程介紹

前言 本章介紹BMS硬件功能及SimuLink庫為主&#xff0c;捎帶介紹一些例程內容 注意&#xff1a;例程所用的協議均是自定義的 自做的SimuLink庫也會不定期更新 BMS例程的內容不定期維護添加 當前的BMS沒有主動均衡功能&#xff0c;這個有考慮后期加上&#xff0c;當前還處于…

urlencode、html實體編碼、unicode

目錄 urlencode html實體編碼 Unicode編碼 urlencode URL編碼也稱為百分號編碼&#xff0c;用于將URL中的特殊字符轉換為安全傳輸的格式。英文數字一般不編碼 特點&#xff1a; 使用%后跟兩個十六進制數字表示字符 空格編碼為或%20 保留字符&#xff08;; / ? : & …

【HarmonyOS】元服務概念詳解

【HarmonyOS】元服務概念詳解 最近幾年&#xff0c;我們手里的設備越來越多——手機、平板、手表、車機……光是管理這些設備上的APP就夠頭疼了&#xff1a;下載要流量、安裝占內存、換個設備又得重新弄一遍。有沒有更簡單的方式&#xff1f;HarmonyOS推出的“元服務”&#xf…

vscode/cursor怎么自定義文字、行高、顏色

JetBrains Mono: A free and open source typeface for developers | JetBrains: Developer Tools for Professionals and Teams 首先下載上面的文字&#xff0c;然后右鍵全選&#xff0c;安裝 然后重啟cursor 下載插件Apc Customize UI 點擊設置 把下面的代碼復制進去&…

JavaScript 與 C語言基礎知識差別

一&#xff0c; 變量聲明對比 C語言&#xff1a; int age 20; // 必須指定類型 float price 9.99; char grade A; const double PI 3.14; // 常量JavaScript&#xff1a; let age 20; // 數字 var price 9.99; // 現在不用&#xff0c;有缺點 co…

無縫矩陣支持音頻分離帶畫面分割功能的全面解析

一、技術原理與實現方式1. 音頻分離技術核心功能&#xff1a;HDMI無縫矩陣通過硬件或軟件實現音頻加嵌與分離功能&#xff0c;支持多設備音頻的獨立處理與增強。實現方式&#xff1a;音頻加嵌&#xff1a;將外部音頻信號&#xff08;如麥克風、調音臺&#xff09;嵌入HDMI信號中…

AI創作系列第18篇:海貍IM移動端UI統一大升級 - 從混亂到規范的技術重構之路

AI創作系列第18篇&#xff1a;海貍IM移動端UI統一大升級 - 從混亂到規范的技術重構之路本文是海貍IM AI創作系列的第18篇文章&#xff0c;記錄7月11日-13日周末期間對移動端的UI統一升級工作。這次重構不是功能性的&#xff0c;而是架構性的 - 我們重新設計了整個UI架構&#x…

八、nginx搭建,實現vue跳轉nginx跳轉gateway

基本的調用鏈路: vue調用nginx,nginx反向代理gateway,gateway看用戶是否登錄,沒有登錄的話,就創建驗證碼并先輸入密碼后獲取token。 截止現在我們創建了兩個項目能夠通過feign調用,并且創建好了gateway,且能調用對應的項目。 這一章節,我們搭建好nginx,通過反向代理,…

C++ 中常見的字符串定義方式及其用法

引言 最近在學習C&#xff0c;下面將從基礎到進階的順序&#xff0c;列出一些 C 中常見的字符串定義方式及其用法&#xff0c;包含完整代碼和詳細注釋&#xff0c;加深對代碼的理解。 C 風格字符串&#xff08;char*或 char[]&#xff09; 定義方式 #include <iostream>i…

下一代防火墻-防范DOS攻擊、IPS防護、web防護實驗

一、實驗拓撲二、實驗設備1.山石網科系列下一代防火墻2.三層交換機一臺3.windows兩臺4.各種工具&#xff0c;如hyenae、小旋風服務器、永恒之藍等等三、實驗目的1.掌握網絡攻擊防護策略配置2.通過下一代防火墻來防護服務器免受DOS攻擊四、防范Dos攻擊實驗1.將一臺windows配置為…