黑馬點評-樂觀鎖/悲觀鎖/synchronized/@Transactional

文章目錄

          • 全局ID生成器
          • 超賣
            • 樂觀鎖
          • 一人一單
            • 悲觀鎖

當我們確認訂單時,系統需要給我們返回我們的訂單編號。這個時候就會出現兩個大問題。

1.訂單id采用數據庫里的自增的話,安全性降低。比如今天我的訂單是10,我明天的訂單是100,那么我就可以知道在昨天總共有多少訂單,從而給惡意用戶鉆空子。

2.訂單數很多時,訂單id增長到幾百上千萬,單張表無法存儲大量數據,那就需要將這些數據分到多張表,同時要重新設計訂單id,避免出現相同ID。

所以,這里我們使用全局ID生成器

全局ID生成器

在分布式系統下用來生成全局唯一ID的工具。

其基本核心就是ID的生成:

在這里插入圖片描述

符號位:正負數

時間戳:當前時間減初始時間

序列號:基于redis自增INCR命令

對存儲在指定鍵中的整數值進行原子性遞增的核心命令.

當key不存在時,redis自動創建一個新key,并設置其value為0.然后執行incr操作,將value遞增為1并返回。

key存在時,直接將value遞增。

@Component
public class RedisIdWorker {/*** 開始時間戳*/private static final long BEGIN_TIMESTAMP = 1640995200L;//2022年1月1日0點0分0秒/*** 序列號的位數*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {// 1.生成時間戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列號// 2.1.獲取當前日期,精確到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增長long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

超賣

在簡單的優惠券秒殺下單中,我們的基本步驟:

1.根據優惠券id查詢是否存在

2.確認搶購時間在時間范圍內

3.確認優惠券庫存>0

4.根據RedisIdWorker生成訂單id,優惠券數量減1

在這里插入圖片描述

這在簡單的場景下是沒有問題的,但是在實際場景中,我們要考慮到多線程導致的超賣問題。

現在有100張優惠券,有200人來搶

理論上來說,應賣出100張,有100人搶到,但事實卻是多賣出了9張

在這里插入圖片描述

當涉及多線程時,各個線程的運行順序我們是無法肯定的。

當線程1查詢庫存為1時,線程2插進來了,也查到為1,線程1按照邏輯扣減庫存,線程2也按照邏輯扣除庫存,這樣就導致最終庫存為-1。更多個線程,可能導致庫存更低,這就是超賣。

在這里插入圖片描述

樂觀鎖

悲觀鎖和樂觀鎖都只是一種思想!

樂觀鎖先操作,提交時再檢查沖突

認為并發操作很少發生沖突,只在提交操作時檢查是否沖突,比如CAS操作,數據庫的樂觀鎖和Java中的Atomic類。

舉個例子:

1.購物車結算時才檢查庫存(默認沒人搶購)

2.或者在網上訂票,系統顯示還有1個座位,你點擊預訂,系統會先讓你填寫信息,然后提交的時候檢查是否還有座位。如果有,預訂成功;如果沒有,提示你重新選擇

這里就以樂觀鎖為核心解決方法:判斷之前查詢到的數據是否有被修改過。

  • 版本號法

    給優惠券再設置一個字段“版本號”,初始值為1,每次被修改就加1。

    這樣每個線程在查詢到庫存和版本號時,要想修改數據,必須在當前版本號基礎上實現,否則不成功。

    在這里插入圖片描述

  • CAS法

    本質還是版本思想,做了簡化,每個線程在查詢到庫存后,要想修改數據,必須在當前庫存基礎上實現,否則不成功。

    在這里插入圖片描述

但是樂觀鎖同樣存在問題,當其他線程發現數據被修改后,他就不再執行,導致優惠券沒有賣完。

所以這里其他線程只需要在將修改條件改為stock>0。只要有庫存,我就可以減。

這樣會不會恍然中帶點疑惑:這跟最初有什么區別?都是判斷庫存是否>0。

NO,最初的問題出現在先判斷,再修改;而現在是要修改的時候才做判斷

@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 5.一人一單Long userId = UserHolder.getUser().getId();// 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);}}
一人一單
悲觀鎖

悲觀鎖:提前加鎖

認為并發操作一定會發生沖突,因此每次訪問數據時都會加鎖,比如synchronized和ReentrantLock

舉個例子:出門時鎖門(默認有小偷)

上面的解決中,還存在一個問題:一個用戶不可以買多張優惠券

那如果我們直接簡單的判斷該用戶是否下過單來處理的話:

// 5.1.查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判斷是否存在if (count > 0) {// 用戶已經購買過了log.error("不允許重復下單!");return;}// 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) {// 扣減失敗log.error("庫存不足!");return;}// 7.創建訂單save(voucherOrder);

假設用戶A同時發起多個請求,每個請求都執行這段代碼。這時候,可能會出現多個線程同時通過第5.1步的查詢(count=0),然后都進入扣減庫存和創建訂單的步驟,導致用戶A創建了多個訂單,違反了“一人一單”的要求。

為什么會這樣?

兩個線程同時執行查詢時,此時數據庫中還沒有該用戶的訂單,所以兩個線程都認為可以繼續執行。然后它們都會去扣減庫存,假設庫存足夠,兩個線程都成功扣減,然后各自創建訂單。

所以我們最終的解決方法就是再加上一個悲觀鎖:

一個用戶加一把鎖(確保不會重復下單),不同用戶加不同鎖

@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 5.一人一單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);}}

synchronized (userId.toString().intern())

這里為什么通過用戶ID來加鎖,為什么是userId.toString().intern()?

synchronized:實現線程同步,確保同一時刻只有一個線程可以執行某個代碼塊或方法。

toString()userId轉換為字符串,雖然是同一個userId,但是會新生成不同的字符串對象。

public static String toString(long i) {int size = stringSize(i);if (COMPACT_STRINGS) {byte[] buf = new byte[size];getChars(i, size, buf);return new String(buf, LATIN1);} else {byte[] buf = new byte[size * 2];StringUTF16.getChars(i, size, buf);return new String(buf, UTF16);}}

intern()方法會返回該字符串在常量池中的引用,確保相同值的字符串引用同一個對象,從而正確同步。

  • 如果常量池已存在相同值的字符串,直接返回該引用;
  • 如果不存在,將該字符串加入常量池后再返回引用。

不過又發現一個問題:這里用戶加鎖-操作-釋放鎖,但如果此時事務還沒有提交上去,其他線程來了,依然可能出現并發問題。

在這里插入圖片描述

我們希望整個事務提交上去后再釋放鎖

也就是給這個函數加上鎖。

當函數1(無事務)調用這個函數2時(有事務),事務是否還生效?

在這里插入圖片描述

事務

當我們在一個類的方法上使用 @Transactional注解時,Spring會為該類創建一個代理對象。這個代理對象在調用方法時會處理事務的開啟、提交或回滾等操作。

如果在一個類內部的方法A調用另一個有@Transactional注解的方法B,這時候方法A調用的是實際的實例方法,而不是通過代理對象調用的。因此,事務不會生效,因為代理對象沒有被使用到。

解決:

在這里插入圖片描述

在這里插入圖片描述

不斷學習中,感謝大家的觀看>W<

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

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

相關文章

python下通過wmic設置程序的優先級~~~

在開發過程中&#xff0c;經常會碰到需要設置程序優先級&#xff0c;這時候可以手動到任務管理器中調整&#xff0c;但是這多多少少有些不方便&#xff0c;那么這時候我們就可以通過subprocess調用wmic命令來實現&#xff0c;方法如下: step 1 必要的引用: import subprocess…

在Mac中使用pyenv管理Python版本:從安裝到虛擬環境的全流程指南

# 在Mac中使用pyenv管理Python版本&#xff1a;從安裝到虛擬環境的全流程指南 ## 一、為什么選擇pyenv&#xff1f; 在開發過程中&#xff0c;不同項目往往需要不同的Python版本&#xff08;如3.8 vs 3.10&#xff09;&#xff0c;而系統默認的Python環境難以滿足靈活切換的需…

FFT Shift

在頻域圖像處理中,交換四個象限實現FFT移位(也稱為FFT Shift)是一種將頻域圖像的低頻成分移動到中心的標準化操作。 1. 為什么需要FFT移位? 原始FFT輸出特性: 二維FFT的直接計算結果中: 低頻分量(圖像的整體亮度和平滑部分)位于頻譜圖的四個角落 高頻分量(邊緣、細節…

python打卡day34@浙大疏錦行

知識點回歸&#xff1a; CPU性能的查看&#xff1a;看架構代際、核心數、線程數GPU性能的查看&#xff1a;看顯存、看級別、看架構代際GPU訓練的方法&#xff1a;數據和模型移動到GPU device上類的call方法&#xff1a;為什么定義前向傳播時可以直接寫作self.fc1(x) ①CPU性能查…

Windows 配置 ssh 秘鑰登錄 Ubuntu

在 Windows 上推送 SSH 公鑰到遠程服務器&#xff08;類似于 Linux 上的 ssh-copy-id&#xff09;可以通過以下幾種方法實現&#xff1a; ** 手動復制公鑰內容** 查看本地公鑰內容&#xff1a;type $env:USERPROFILE\.ssh\id_rsa.pub登錄遠程服務器&#xff0c;將公鑰內容粘貼…

SAP全面轉向AI戰略,S/4HANA悄然隱身

在2025年SAP Sapphire大會上&#xff0c;SAP首席執行官Christian Klein提出了一個雄心勃勃的愿景&#xff1a;讓人工智能&#xff08;AI&#xff09;無處不在&#xff0c;推動企業數字化轉型。SAP的AI戰略核心是將AI深度融入其業務應用生態&#xff0c;包括推出全新版本的AI助手…

Athena 執行引擎:在線服務計算的效率王者

引言 在在線服務領域&#xff0c;計算任務呈現出獨特的特性&#xff1a;一方面&#xff0c;數據量通常不會過于龐大&#xff0c;因為在線服務對耗時和響應速度有著嚴苛要求&#xff1b;另一方面&#xff0c;計算任務具有可控性&#xff0c;其大多并非由用戶實時輸入動態生成&a…

傳奇各種怪物一覽/圖像/爆率/產出/刷新地/刷新時間/刷怪時間

名稱圖像顯示名等級血量攻擊可召喚產出刷新蝙蝠蝙蝠530-22,0,0可誘惑回城卷(1.00%) 金幣(1.00%*500)雞雞551-1,0,0可誘惑雞肉(100.00%)比奇省(29550,62550)5分鐘35只 比奇省(35025,20025)5分鐘25只 比奇省(34025,31025)5分鐘25只 比奇省(40525,24025)5分鐘25只 比奇省(28025,26…

MySQL--day7--聚合函數

&#xff08;以下內容全部來自上述課程&#xff09; 聚合函數 1. 介紹 聚合函數作用于一組數據&#xff0c;并對一組數據返回一個值。 聚合函數類型 AVG&#xff08;&#xff09;SUM&#xff08;&#xff09;MAX&#xff08;&#xff09;MIN&#xff08;&#xff09;COU…

[Java] 封裝

目錄 1. 什么是封裝 2. 訪問修飾符 3. 封裝的好處 4. 封裝的步驟 5. 包 5.1 什么是包 5.2 導入包中的類 5.3 自定義包 5.4 常用的包 6. static關鍵字 6.1 static修飾成員變量 6.2 static修飾成員方法 6.3 Static修飾成員變量初始化 7. 代碼塊 7.1 普通代碼塊 …

Axure元件動作五:設置列表選中項

親愛的小伙伴,在您瀏覽之前,煩請關注一下,在此深表感謝!如有幫助請訂閱專欄! Axure產品經理精品視頻課已登錄CSDN可點擊學習https://edu.csdn.net/course/detail/40420 演示視頻: Axure設置列表選中項 課程主題:設置列表選中項 主要內容:下拉列表選項、值、變量值、焦…

Spring框架--IOC技術

一、Spring框架的介紹 1、Spring框架的概述 Spring 是一個開放源代碼的設計層面框架&#xff0c;它解決的是業務邏輯層和其他各層的松耦合問題&#xff0c;因此它將面向接口的編程思想貫穿整個系統應用。Spring是于2003年興起的一個輕量級的Java開發框架&#xff0c;由 Rod Jo…

Flannel后端為UDP模式下,分析數據包的發送方式——tun設備(三)

在分析 Kubernetes 環境中 Flannel UDP 模式的數據包轉發時&#xff0c;我們提到 flannel.1 是一個 TUN 設備&#xff0c;它在數據包處理中起到了關鍵作用。 什么是 TUN 設備&#xff1f; TUN 設備&#xff08;Tunnel 設備&#xff09;是 Linux 系統中一種虛擬網絡接口&#x…

2025深圳國際無人機展深度解析:看點、廠商與創新亮點

2025深圳國際無人機展深度解析&#xff1a;看點、廠商與創新亮點 1.背景2.核心看點&#xff1a;技術突破與場景創新2.1 eVTOL&#xff08;飛行汽車&#xff09;的規模化展示2.2 智能無人機與無人值守平臺2.3 新材料與核心零部件革新2.4 動態演示與賽事活動 3.頭部無人機廠商4.核…

【Jitsi Meet】(騰訊會議的平替)Docker安裝Jitsi Meet指南-使用內網IP訪問

Docker安裝Jitsi Meet指南-使用內網IP訪問 下載官方代碼配置環境變量復制示例環境文件并修改配置&#xff1a;編輯 .env 文件&#xff1a; 修改 docker-compose.yml 文件生成自簽名證書啟動服務最終驗證 騰訊會議的平替。我們是每天開早晚會的&#xff0c;都是使用騰訊會議。騰…

使用Spring Boot和Spring Security結合JWT實現安全的RESTful API

使用Spring Boot和Spring Security結合JWT實現安全的RESTful API 引言 在現代Web應用中&#xff0c;安全性是至關重要的。Spring Boot和Spring Security提供了強大的工具來保護我們的應用程序&#xff0c;而JWT&#xff08;JSON Web Token&#xff09;則是一種輕量級的認證和…

對神經正切核的理解和推導(1)

聲明&#xff1a; 本文是對Neural Tangent Kernel &#xff08;NTK&#xff09;基礎推導 - Gearlesskai - 博客園文章內容的理解與推導&#xff0c;里面很多東西對我這種新手來說不太好理解&#xff0c;所以我力求通過這種方式理解文章的大部分內容。希望我的理解可以幫助你更…

基于 STC89C52 的養殖場智能溫控系統設計與實現

摘要 本文提出一種基于 STC89C52 單片機的養殖場環境溫度智能控制系統,通過集成高精度溫度傳感器、智能執行機構及人機交互模塊,實現對養殖環境的實時監測與自動調控。系統具備溫度閾值設定、超限報警及多模式控制功能,可有效提升養殖環境穩定性,降低能耗與人工成本。 一…

微信小程序調試

一、尋找答案 1. 創建小程序 https://zhuanlan.zhihu.com/p/1906013675883561860 2. 若有后端接口&#xff0c;需要調試 https://blog.csdn.net/animatecat/article/details/126949749 3. 比較細教程, 搭建修改配置 https://zhuanlan.zhihu.com/p/1893281527112136235 4. 查找…

使用DeepSeek實現數據處理

一、核心能力全景圖 Ctrl+/ 喚醒智能助手,支持以下數據處理場景: ?? 數據清洗與預處理?? 統計分析與可視化?? 機器學習建模?? 大數據性能優化?? 自動化報告生成? 實時流數據處理二、高頻場景實戰(附魔法口令) 場景1:數據清洗自動化(Python示例) 口令: 處…