3.優惠券秒殺

3.1 全局唯一 ID

當用戶搶購時,就會生成訂單并保存到 tb_voucher_order 這張表中,而訂單表如果使用數據庫自增 ID 就存在一些問題:

  • id 的規律性太明顯

  • 受單表數據量的限制

場景分析一:如果我們的 id 具有太明顯的規則,用戶或者說商業對手很容易猜測出來我們的一些敏感信息,比如商城在一天時間內,賣出了多少單,這明顯不合適。

場景分析二:隨著我們商城規模越來越大,mysql 的單表的容量不宜超過 500W,數據量過大之后,我們要進行拆庫拆表,但拆分表了之后,他們從邏輯上講他們是同一張表,所以他們的 id 是不能一樣的,于是乎我們需要保證 id 的唯一性

全局 ID 生成器,是一種在分布式系統下用來生成全局唯一 ID 的工具,一般要滿足下列特性:
在這里插入圖片描述

為了增加 ID 的安全性,我們可以不直接使用 Redis 自增的數值,而是拼接一些其它信息:

在這里插入圖片描述

ID 的組成部分:

  • 符號位:1bit,永遠為 0

  • 時間戳:31bit,以秒為單位,可以使用 69 年

  • 序列號:32bit,秒內的計數器,支持每秒產生 2^32 個不同 ID

3.2 添加優惠券

每個店鋪都可以發布優惠券,分為平價券和特價券。平價券可以任意購買,而特價券需要秒殺搶購:

  • tb_voucher:優惠券的基本信息,優惠金額、使用規則等

  • tb_seckill_voucher:優惠券的庫存、開始搶購時間,結束搶購時間。特價優惠券才需要填寫這些信息

  • 平價卷由于優惠力度并不是很大,所以是可以任意領取

而代金券由于優惠力度大,所以像第二種卷,就得限制數量,從表結構上也能看出,特價卷除了具有優惠卷的基本信息以外,還具有庫存,搶購時間,結束時間等等字段

3.3 秒殺下單

秒殺下單應該思考的內容:

  • 下單時需要判斷兩點:

    • 秒殺是否開始或結束,如果尚未開始或已經結束則無法下單

    • 庫存是否充足,不足則無法下單

  • 下單核心邏輯分析:

    • 當用戶開始進行下單,我們應當去查詢優惠卷信息,查詢到優惠卷信息,判斷是否滿足秒殺條件

    • 比如時間是否充足,如果時間充足,則進一步判斷庫存是否足夠,如果兩者都滿足,則扣減庫存,創建訂單,然后返回訂單 id,如果有一個條件不滿足則直接結束。

在這里插入圖片描述

3.4 超賣問題

有關超賣問題分析:在我們原有代碼中是這么寫的

 if (voucher.getStock() < 1) {// 庫存不足return Result.fail("庫存不足!");}//5,扣減庫存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣減庫存return Result.fail("庫存不足!");}
  • 假設線程 1 過來查詢庫存判斷出來庫存大于 1

  • 正準備去扣減庫存,但是還沒有來得及去扣減

  • 此時線程 2 過來,線程 2 也去查詢庫存,發現這個數量一定也大于 1

  • 那么這兩個線程都會去扣減庫存,最終多個線程相當于一起去扣減庫存

  • 此時就會出現庫存的超賣問題。

在這里插入圖片描述

3.5 解決方案

超賣問題是典型的多線程安全問題,針對這一問題的常見解決方案就是加鎖:

而對于加鎖,我們通常有兩種解決方案:見下圖:

在這里插入圖片描述

悲觀鎖:

悲觀鎖可以實現對于數據的串行化執行,比如 syn,和 lock 都是悲觀鎖的代表,同時,悲觀鎖中又可以再細分為公平鎖,非公平鎖,可重入鎖,等等

樂觀鎖:

會有一個版本號,每次操作數據會對版本號 +1,再提交回數據時,會去校驗是否比之前的版本大 1,如果大 1,則進行操作成功,這套機制的核心邏輯在于,如果在操作過程中,版本號只比原來大 1,那么就意味著操作過程中沒有人對他進行過修改,他的操作就是安全的,如果不大 1,則數據被修改過,當然樂觀鎖還有一些變種的處理方式比如 cas

樂觀鎖的典型代表:就是 cas,利用 cas 進行無鎖化機制加鎖,var5 是操作前讀取的內存值,while 中的 var1+var2 是預估值,如果預估值 == 內存值,則代表中間沒有被人修改過,此時就將新值去替換 內存值

課程中的使用方式是沒有像 cas 一樣帶自旋的操作,也沒有對 version 的版本號 +1,他的操作邏輯是在操作時,對版本號進行 +1 操作,然后要求 version 如果是 1 的情況下,才能操作,那么第一個線程在操作后,數據庫中的 version 變成了 2,但是他自己滿足 version=1,所以沒有問題,此時線程 2 執行,線程 2 最后也需要加上條件 version =1,但是現在由于線程 1 已經操作過了,所以線程 2,操作時就不滿足 version=1 的條件了,所以線程 2 無法執行成功

在這里插入圖片描述

boolean success = seckillVoucherService.update().setSql("stock= stock -1") //set stock = stock -1.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

以上邏輯的核心含義是:只要我扣減庫存時的庫存和之前我查詢到的庫存是一樣的,就意味著沒有人在中間修改過庫存,那么此時就是安全的,但是以上這種方式通過測試發現會有很多失敗的情況,失敗的原因在于:在使用樂觀鎖過程中假設 100 個線程同時都拿到了 100 的庫存,然后大家一起去進行扣減,但是 100 個人中只有 1 個人能扣減成功,其他的人在處理時,他們在扣減時,庫存已經被修改過了,所以此時其他線程都會失敗

之前的方式要修改前后都保持一致,但是這樣我們分析過,成功的概率太低,所以我們的樂觀鎖需要變一下,改成 stock 大于 0 即可

boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update().gt("stock",0); 
//where id = ? and stock > 0

CAS:

  • 針對 cas 中的自旋壓力過大,我們可以使用 Longaddr 這個類去解決

  • Java8 提供的一個對 AtomicLong 改進后的一個類,LongAdder

  • 大量線程并發更新一個原子性的時候,天然的問題就是自旋,會導致并發性問題,當然這也比我們直接使用 syn 來的好

  • 所以利用這么一個類,LongAdder 來進行優化

  • 如果獲取某個值,則會對 cell 和 base 的值進行遞增,最后返回一個完整的值

在這里插入圖片描述

3.6 一人一單

需求:修改秒殺業務,要求同一個優惠券,一個用戶只能下一單

現在的問題在于:

優惠卷是為了引流,但是目前的情況是,一個人可以無限制的搶這個優惠卷,所以我們應當增加一層邏輯,讓一個用戶只能下一個單,而不是讓一個用戶下多個單

具體操作邏輯如下

  1. 比如時間是否充足,如果時間充足

  2. 則進一步判斷庫存是否足夠

  3. 然后再根據優惠卷 id 和用戶 id 查詢是否已經下過這個訂單

  4. 如果下過這個訂單,則不再下單,否則進行下單

在這里插入圖片描述

// 一人一單邏輯
Long userId = UserHolder.getUser().getId();
Long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 判斷是否存在
if (count > 0) {// 用戶已經購買過了return Result.fail("用戶已經購買過一次!");
}

問題:

多線程情況下(該用戶)可能多個線程同時判斷到 count == 0,然后都去執行下面的邏輯增加訂單…

解決:

封裝方法,加 synchronized 鎖

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// 5.1.查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判斷是否存在if (count > 0) {// 用戶已經購買過了return Result.fail("用戶已經購買過一次!");}// 6.扣減庫存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣減失敗return Result.fail("庫存不足!");}// 7.創建訂單VoucherOrder voucherOrder = new VoucherOrder();// 7.1.訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用戶idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回訂單idreturn Result.ok(orderId);
}

但是這樣添加鎖,鎖的粒度太粗了,在使用鎖過程中,控制**鎖粒度** 是一個非常重要的事情,因為如果鎖的粒度太大,會導致每個線程進來都會鎖住,所以我們需要去控制鎖的粒度,以下這段代碼需要修改為:

intern() 這個方法是從常量池中拿到數據,如果我們直接使用 userId.toString() 他拿到的對象實際上是不同的對象,new 出來的對象,我們使用鎖必須保證鎖必須是同一把,所以我們需要使用 intern() 方法

@Transactional
public  Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()){// 5.1.查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判斷是否存在if (count > 0) {// 用戶已經購買過了return Result.fail("用戶已經購買過一次!");}// 6.扣減庫存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣減失敗return Result.fail("庫存不足!");}// 7.創建訂單VoucherOrder voucherOrder = new VoucherOrder();// 7.1.訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用戶idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回訂單idreturn Result.ok(orderId);}
}

但是以上代碼還是存在問題,問題的原因在于當前方法被 spring 的事務控制,如果你在方法內部加鎖,可能會導致當前方法事務還沒有提交,但是鎖已經釋放也會導致問題,所以我們選擇將當前方法整體包裹起來,確保事務不會出現問題:如下:

在 seckillVoucher 方法中,添加以下邏輯,這樣就能保證事務的特性,同時也控制了鎖的粒度

3.7 并發問題

通過加鎖可以解決在單機情況下的一人一單安全問題,但是在集群模式下就不行了。

1、我們將服務啟動兩份,端口分別為 8081 和 8082:

NjU6MTc0Njg2OTg2NV9WNA&pos_id=img-0n0b7Wnj-1746866267631)

2、然后修改 nginx 的 conf 目錄下的 nginx.conf 文件,配置反向代理和負載均衡:

在這里插入圖片描述

有關鎖失效原因分析

由于現在我們部署了多個 tomcat,每個 tomcat 都有一個屬于自己的 jvm,那么假設在服務器 A 的 tomcat 內部,有兩個線程,這兩個線程由于使用的是同一份代碼,那么他們的鎖對象是同一個,是可以實現互斥的,但是如果現在是服務器 B 的 tomcat 內部,又有兩個線程,但是他們的鎖對象寫的雖然和服務器 A 一樣,但是鎖對象卻不是同一個,所以線程 3 和線程 4 可以實現互斥,但是卻無法和線程 1 和線程 2 實現互斥,這就是 集群環境下,syn 鎖失效的原因,在這種情況下,我們就需要使用分布式鎖來解決這個問題。

在這里插入圖片描述

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

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

相關文章

AI日報 · 2025年5月07日|谷歌發布 Gemini 2.5 Pro 預覽版 (I/O 版本),大幅提升編碼與視頻理解能力

1、谷歌發布 Gemini 2.5 Pro 預覽版 (I/O 版本)&#xff0c;大幅提升編碼與視頻理解能力 谷歌于5月6日提前發布 Gemini 2.5 Pro 預覽版 (I/O 版本)&#xff0c;為開發者帶來更強編碼能力&#xff0c;尤其優化了前端與UI開發、代碼轉換及智能體工作流構建&#xff0c;并在WebDe…

Python+ffmpeg 實現給視頻添加字幕

創作靈感 孩子學校經常留作業&#xff0c;需要提交一段錄制的視頻&#xff0c;視頻上要求添加學校、班級、姓名等信息的字幕&#xff0c;手機自帶的相機軟件字幕添加位置要么只能添加在視頻正中&#xff0c;要么無法添加多行文本&#xff0c;要么只能添加在片頭或者片尾&#…

OpenLayers 精確經過三個點的曲線繪制

OpenLayers 精確經過三個點的曲線繪制 根據您的需求&#xff0c;我將提供一個使用 OpenLayers 繪制精確經過三個指定點的曲線解決方案。對于三個點的情況&#xff0c;我們可以使用 二次貝塞爾曲線 或 三次樣條插值&#xff0c;確保曲線精確通過所有控制點。 實現方案 下面是…

Django緩存框架API

這里寫自定義目錄標題 訪問緩存django.core.cache.cachesdjango.core.cache.cache 基本用法cache.set(key, value, timeoutDEFAULT_TIMEOUT, versionNone)cache.get(key, defaultNone, versionNone)cache.add(key, value, timeoutDEFAULT_TIMEOUT, versionNone)cache.get_or_se…

Linux系統管理與編程17:自動化部署ftp服務

蘭生幽谷&#xff0c;不為莫服而不芳&#xff1b; 君子行義&#xff0c;不為莫知而止休。 #virtual用戶管理&#xff1a;passerbyA、captain和admin三個虛擬用戶 # passerbyA只能看&#xff0c;captain可看讀寫上傳&#xff0c;但不能刪除。admin全部權限 [rootshell shell]…

2025python學習筆記

一.Python語言基礎入門 第一章 01.初識Python Python的起源&#xff1a; 1989年&#xff0c;為了打發圣誕節假期&#xff0c;Gudio van Rossum吉多范羅蘇姆&#xff08;龜叔&#xff09;決心開發一個新的解釋程序&#xff08;Python維形&#xff09;1991年&#xff0c;第一個…

STM32單片機的快速成長路徑規劃

一、基礎準備階段&#xff08;1-2周&#xff09; C語言核心技能 重點掌握&#xff1a;指針操作、結構體、枚舉、位操作、函數指針&#xff08;回調函數基礎&#xff09;實踐項目&#xff1a;通過51單片機或STM8完成LED控制、按鍵檢測等基礎項目&#xff0c;熟悉寄存器配置和調試…

torch.nn.init.uniform_

nn.init.uniform_ 是 PyTorch 中用于初始化張量&#xff08;tensor&#xff09;的一個函數&#xff0c;它的作用是將張量的值填充為從均勻分布中采樣的隨機數。 詳細說明&#xff1a; 函數&#xff1a; torch.nn.init.uniform_(tensor, a0., b1.)tensor&#xff1a;需要被初始…

Spring MVC中跨域問題處理

在Spring MVC中處理跨域問題可以通過以下幾種方式實現&#xff0c;確保前后端能夠正常通信&#xff1a; 方法一&#xff1a;使用 CrossOrigin 注解 適用于局部控制跨域配置&#xff0c;直接在Controller或方法上添加注解。 示例代碼&#xff1a; RestController CrossOrigin…

基本句子結構

以下是英語句子五種基本結構的詳細解釋&#xff0c;并附上系動詞的全面分類及示例&#xff1a; ?1. 主謂結構&#xff08;SV&#xff09;? ?結構&#xff1a;主語&#xff08;Subject&#xff09; 不及物動詞&#xff08;Intransitive Verb&#xff09;?核心&#xff1a;…

游戲引擎學習第264天:將按鈕添加到分析器

回顧并為今天的工作做鋪墊 隨著時間的推移&#xff0c;我們的分析器&#xff08;profiler&#xff09;變得越來越強大。我通常會問大家是否記得我們要做什么&#xff0c;今天我們要做的似乎是按鈕相關的功能。 今天的目標是實現按鈕功能。我們從昨天留下的地方繼續&#xff0…

大節點是選擇自建機房還是托管機房

選擇PCDN大節點自建機房還是托管機房&#xff0c;需綜合考量資金實力、技術能力、運維需求、業務規模及合規要求。以下為具體分析&#xff1a; 自建機房的適用場景與考量因素 資金與技術門檻高 自建機房需投入服務器、存儲、網絡設備等硬件&#xff0c;以及機房建設、電力、散…

【SpringBoot】SpringBoot中使用AOP實現日志記錄功能

前言一、AOP基本概念二、項目準備三、實現日志記錄切面1、創建自定義日志注解2、實現日志切面3、配置AOP 四、使用示例1. 在Controller中使用2. 在Service中使用 六、高級配置1. 日志內容格式化2. 異步日志記錄3. 日志脫敏處理 七、代理類生成的核心邏輯問題1&#xff1a; 既然…

linux中的常用命令(一)

目錄 常用的快捷鍵 1- tab鍵:命令或者路徑提示及補全&#xff1b; 2-ctrlc:放棄當前輸入&#xff0c;終止當前任務或程序 3-ctrll;清屏 4-ctrlinsert:復制 5-鼠標右鍵:粘貼&#xff1b; 6-altc:斷開連接/ctrlshift r 重新連接 7-alt1/2/3/等&#xff1a;切換回話窗口 8-上下鍵…

Pycharm(十九)深度學習

一、深度學習概述 1.1 什么是深度學習 深度學習是機器學習中的一種特殊方法,它使用稱為神經網絡的復雜結構,特別是“深層”的神經網絡,來學習和做出預測。深度學習特別適合處理大規模和高維度的數據,如圖像、聲音和文本。深度學習、機器學習和人工智能之間的關系如下圖所…

多視圖密集對應學習:細粒度3D分割的自監督革命

原文標題&#xff1a;Multi-view Dense Correspondence Learning (MvDeCor) 引言 在計算機視覺與圖形學領域&#xff0c;3D形狀分割一直是一個基礎且具有挑戰性的任務。如何在標注稀缺的情況下&#xff0c;實現對3D模型的細粒度分割&#xff1f;近期&#xff0c;斯坦福大學視覺…

Vue——前端vue3項目使用漢字轉拼音

在 Vue3 項目中&#xff0c;可以通過以下 第三方 JavaScript 包 實現漢字轉拼音。這些包均兼容 Vue3&#xff0c;且無需依賴后端處理&#xff1a; 推薦方案 1. pinyin-pro 特點&#xff1a;功能強大、支持多音字、聲調、拼音匹配、輕量級&#xff08;~20KB&#xff09;。安裝…

批量統計PDF頁數,統計圖像屬性

軟件介紹&#xff1a; 1、支持批量統計PDF、doc\docx、xls\xlsx頁數 2、支持統計指定格式文件數量&#xff08;不填格式就是全部&#xff09; 3、支持統計JPG、JPEG、PNG圖像屬性 4、支持統計多頁TIF頁數、屬性 5、支持統計PDF、JPG畫幅 統計圖像屬性 「托馬斯的文件助手」…

LeetCode 每日一題 2025/5/5-2025/5/11

記錄了初步解題思路 以及本地實現代碼&#xff1b;并不一定為最優 也希望大家能一起探討 一起進步 目錄 5/5 790. 多米諾和托米諾平鋪5/6 1920. 基于排列構建數組5/7 3341. 到達最后一個房間的最少時間 I5/8 3342. 到達最后一個房間的最少時間 II5/9 3343. 統計平衡排列的數目5…

pytest自動化測試執行環境切換的兩種解決方案

&#x1f345; 點擊文末小卡片&#xff0c;免費獲取軟件測試全套資料&#xff0c;資料在手&#xff0c;漲薪更快 一、痛點分析 在實際企業的項目中&#xff0c;自動化測試的代碼往往需要在不同的環境中進行切換&#xff0c;比如多套測試環境、預上線環境、UAT環境、線上環…