從庫存超賣問題分析鎖和分布式鎖的應用(二)

本文從一個經典的庫存超賣問題分析說明常見鎖的應用,假設庫存資源存儲在Redis里面。

假設我們的減庫存代碼如下:

@Autowired
StringRedisTemplate redisTemplate;public void deduct(){String stock = redisTemplate.opsForValue().get("stock");if(StringUtils.hasLength(stock)){Integer st = Integer.valueOf(stock);if(st>0){redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}
}

此時方法操作是先讀后寫,非原子性操作,是存在并發問題的。如何解決該問題,有三種方案:

  • JVM本地鎖
  • Redis樂觀鎖
  • Redis實現分布式鎖

JVM本地鎖的實現與優缺點在從庫存超賣問題分析鎖和分布式鎖的應用(一)已經分析過了,這里不再贅述。

【1】Redis樂觀鎖

也就是watchmultiexec組合指令的使用。

watch可以監控一個或多個key的值,如果在事務(exec)執行之前,key的值發生變化則取消事務執行。

multi用來開啟事務,exec用來提交/執行事務。

watch stock
multi
set stock 5000
exec

代碼修改如下:

public void deduct(){this.redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {operations.watch("stock");// 1. 查詢庫存信息Object stock = operations.opsForValue().get("stock");// 2. 判斷庫存是否充足int st = 0;if (stock != null && (st = Integer.parseInt(stock.toString())) > 0) {// 3. 扣減庫存operations.multi();//開啟事務operations.opsForValue().set("stock", String.valueOf(--st));List exec = operations.exec();//執行事務if (exec == null || exec.size() == 0) {try {// 這里睡眠一下,降低競爭,提高樂觀鎖的吞吐量Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}//再次遞歸deduct();}return exec;}return null;}});
}

這種方式確實可以解決并發問題,但也可能在高并發的情況下由于不斷重試(CAS思想)出現性能問題、連接被耗盡的情況。

【2】Redis分布式鎖

① 基于setnx思想簡單實現

借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同時有多個客戶端發送setnx命令,只有一個客戶端可以成功,返回1(true);其他的客戶端返回0(false)。

// 遞歸思想
public void deduct(){//獲取鎖Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1111");//如果獲取不到則遞歸重試if(!lock){deduct();}else{try{String stock = redisTemplate.opsForValue().get("stock");if(StringUtils.hasLength(stock)){Integer st = Integer.valueOf(stock);if(st>0){redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}}finally {//釋放鎖redisTemplate.delete("lock");}}}

或者使用while思想:

public void deduct(){
//當setnx剛剛獲取到鎖,當前服務器宕機,導致del釋放鎖無法執行,進而導致鎖無法鎖無法釋放(死鎖)while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx"))){try {Thread.sleep(100);}catch (Exception e){e.printStackTrace();}}try{String stock = redisTemplate.opsForValue().get("stock");if(StringUtils.hasLength(stock)){Integer st = Integer.valueOf(stock);if(st>0){redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}}finally {//釋放鎖redisTemplate.delete("lock");}}

這種方式存在問題:當setnx剛剛獲取到鎖,當前服務器宕機,導致del釋放鎖無法執行,進而導致鎖無法鎖無法釋放(死鎖)

解決方案:給鎖設置過期時間,自動釋放鎖。

設置過期時間兩種方式:

  1. 通過expire設置過期時間(缺乏原子性:如果在setnx和expire之間出現異常,鎖也無法釋放)
  2. 使用set指令設置過期時間:set key value ex 3 nx(既達到setnx的效果,又設置了過期時間)

② 防死鎖優化

修改while中獲取鎖的邏輯如下所示:

while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx",3, TimeUnit.SECONDS)){try {Thread.sleep(100);}catch (Exception e){e.printStackTrace();}
}

這種方式解決了死鎖問題但是可能會釋放其他服務器的鎖。

場景:如果業務邏輯的執行時間是7s。執行流程如下

  1. index1業務邏輯沒執行完,3秒后鎖被自動釋放。
  2. index2獲取到鎖,執行業務邏輯,3秒后鎖被自動釋放。
  3. index3獲取到鎖,執行業務邏輯
  4. index1業務邏輯執行完成,開始調用del釋放鎖,這時釋放的是index3的鎖,導致index3的業務只
    執行1s就被別人釋放。最終等于沒鎖的情況。

解決:setnx獲取鎖時,設置一個指定的唯一值(例如:uuid);釋放前獲取這個值,判斷是否自己的鎖

③ 防誤刪優化

如下這里設置鎖的密鑰為UUID,加鎖者持有。

public void deduct(){String uuid = UUID.randomUUID().toString();while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS)){try {Thread.sleep(100);}catch (Exception e){e.printStackTrace();}}try{String stock = redisTemplate.opsForValue().get("stock");if(StringUtils.hasLength(stock)){Integer st = Integer.valueOf(stock);if(st>0){redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}}finally {//釋放鎖if(uuid.equals(redisTemplate.opsForValue().get("lock"))){redisTemplate.delete("lock");}}
}

這種方式仍舊存在問題:刪除操作缺乏原子性。

場景:

  1. index1執行刪除時,查詢到的lock值確實和uuid相等
  2. index1執行刪除前,lock剛好過期時間已到,被redis自動釋放
  3. index2獲取了lock
  4. index1執行刪除,此時會把index2的lock刪除

解決方案:沒有一個命令可以同時做到判斷 + 刪除,所有只能通過其他方式實現(LUA腳本

④ lua腳本保證刪除原子性

redis采用單線程架構,可以保證單個命令的原子性,但是無法保證一組命令在高并發場景下的原子性。

如下AB兩個進程示例:

在這里插入圖片描述
在串行場景下:A和B的值肯定都是3。在并發場景下:A和B的值可能在0-6之間。

極限情況下1:則A的結果是0,B的結果是3

在這里插入圖片描述

極限情況下2:則A和B的結果都是6

在這里插入圖片描述

如果redis客戶端通過lua腳本把3個命令一次性發送給redis服務器,那么這三個指令就不會被其他客戶端指令打斷。Redis 也保證腳本會以原子性(atomic)的方式執行: 當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行。 這和使用 MULTI/ EXEC 包圍的事務很類似。

但是MULTI/ EXEC方法來使用事務功能,將一組命令打包執行,無法進行業務邏輯的操作。這期間有某一條命令執行報錯(例如給字符串自增),其他的命令還是會執行,并不會回滾。

優化代碼如下所示:

public void deduct(){String uuid = UUID.randomUUID().toString();while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS))){try {Thread.sleep(100);}catch (Exception e){e.printStackTrace();}}try{String stock = redisTemplate.opsForValue().get("stock");if(StringUtils.hasLength(stock)){int st = Integer.parseInt(stock);if(st>0){redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}}finally {// 先判斷是否自己的鎖,再解鎖String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +"then " +"   return redis.call('del', KEYS[1]) " +"else " +"   return 0 " +"end";this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Collections.singletonList("lock"), uuid);
//            //釋放鎖
//            if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
//                redisTemplate.delete("lock");
//            }}}

到這里似乎完美解決了我們考慮到的幾點問題,那么結束了嗎?

并沒有,目前這種方式不支持可重入性、并且集群環境下也存在失效情況。更甚者如果由于異常情況,獲取鎖后服務邏輯未執行完畢,鎖就自動釋放了呢

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

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

相關文章

JavaSE從零開始到精通

1.前置知識 JVM&#xff1a;java virtrual machine, java虛擬機, 專門用于執行java代碼的一款軟件。JRE&#xff1a;java runtime enviroment, java運行時環境, java官方提供的核心類庫. jre中包含了核心類庫和jvm。JDK: java development kit, java開發工具包, javac.exe, ja…

LVS+Keepalive高可用

1、keepalive 調度器的高可用 vip地址主備之間的切換&#xff0c;主在工作時&#xff0c;vip地址只在主上&#xff0c;vip漂移到備服務器。 在主備的優先級不變的情況下&#xff0c;主恢復工作&#xff0c;vip會飄回到住服務器 1、配優先級 2、配置vip和真實服務器 3、主…

我想做信號通路分析,但我就是不想學編程

“我想做信號通路分析&#xff0c;但我就是不想學編程。” “我又不是生信狗&#xff0c;學代碼會死。” “你們這些做生信的&#xff0c;整天把數據分析搞得神神秘秘&#xff0c;不就是怕被人搶飯碗而已嘛。” “這都沒分析出我想要的結果&#xff0c;不靠譜。” “你們做…

【自學安全防御】二、防火墻NAT智能選路綜合實驗

任務要求&#xff1a; &#xff08;銜接上一個實驗所以從第七點開始&#xff0c;但與上一個實驗關系不大&#xff09; 7&#xff0c;辦公區設備可以通過電信鏈路和移動鏈路上網(多對多的NAT&#xff0c;并且需要保留一個公網IP不能用來轉換) 8&#xff0c;分公司設備可以通過總…

使用Docker創建并運行一個create-react-app應用(超簡單)

創建并運行一個使用 Create React App (CRA) 創建的應用程序的 Docker 容器涉及幾個步驟。以下是一個詳細的過程&#xff0c;包括創建一個簡單的 React 應用、編寫 Dockerfile、構建鏡像以及運行容器。 步驟 1: 創建一個新的 React 應用 如果你還沒有一個 React 應用&#xf…

Java爬蟲安全策略:防止TikTok音頻抓取過程中的請求被攔截

摘要 在當今互聯網時代&#xff0c;數據采集已成為獲取信息的重要手段。然而&#xff0c;隨著反爬蟲技術的不斷進步&#xff0c;爬蟲開發者面臨著越來越多的挑戰。本文將探討Java爬蟲在抓取TikTok音頻時的安全策略&#xff0c;包括如何防止請求被攔截&#xff0c;以及如何提高…

RK3568 安卓12 EC20模塊NOCONN沒有ip的問題(已解決)

從網上東拼西湊找了不少教程&#xff0c;但是里面沒有提到rillib.so需要替換&#xff0c;替換掉就可以上網了&#xff0c;系統也有4G圖標了。 注意&#xff0c;這個rillib.so是移遠提供的。把他們提供的文件放到rk3568_android_sdk/vendor/rockchip/common/phone/lib下&#x…

Andriod Stdio新建Kotlin的Jetpack Compose簡單項目

1.選擇 No Activity 2.選擇kotlin 4.右鍵選擇 在目錄MyApplication下 New->Compose->Empty Project 出現下面的畫面 Finish 完成

C++——類和對象(中)

文章目錄 一、類的默認成員函數二、構造函數三、析構函數四、拷?構造函數五、賦值運算符重載1. 運算符重載2. 賦值運算符重載 六、取地址運算符重載const成員函數取地址運算符重載 七、應用&#xff1a;?期類實現Date.hDate.cpptest.cpp 一、類的默認成員函數 默認成員函數就…

技術成神之路:設計模式(七)狀態模式

1.介紹 狀態模式&#xff08;State Pattern&#xff09;是一種行為設計模式&#xff0c;它允許一個對象在其內部狀態改變時改變其行為。這個模式將狀態的相關行為封裝在獨立的狀態類中&#xff0c;并將不同狀態之間的轉換邏輯分離開來。 2.主要作用 狀態模式的主要作用是讓一個…

數據結構—鏈式二叉樹-C語言

代碼位置&#xff1a;test-c-2024: 對C語言習題代碼的練習 (gitee.com) 一、前言&#xff1a; 在現實中搜索二叉樹為常用的二叉樹之一&#xff0c;今天我們就要通過鏈表來實現搜索二叉樹。實現的操作有&#xff1a;建二叉樹、前序遍歷、中序遍歷、后序遍歷、求樹的節點個數、求…

SMU Summer 2024 Contest Round 4

SMU Summer 2024 Contest Round 4 2024.7.16 9:00————11:00 過題數3/7 補題數6/7 Made Up H and V Moving Piece Sum of Divisors Red and Green Apples Rem of Sum is Num Keep Connect A - Made Up 題解&#xff1a; 給定三個數組a&#xff0c;b&#xff0c;c&#xf…

MySQL日期和時間相關函數

目錄 1. 獲取當前時間和日期 2. 獲取當前日期 3. 獲取當前時間 4. 獲取單獨的年/月/日/時/分/秒 5. 添加時間間隔 date_add ( ) 6. 格式化日期 date_format ( ) 7. 字符串轉日期 str_to_date () 8. 第幾天 dayofxx 9. 當月最后一天 last_day ( ) 10. 日期差 datedif…

H. Beppa and SwerChat【雙指針】

思路分析&#xff1a;運用雙指針從后往前掃一遍&#xff0c;兩次分別記作數組a&#xff0c;b&#xff0c;分別使用雙指針i和j來掃&#xff0c;如果一樣就往前&#xff0c;如果不一樣&#xff0c;i–,ans #include<iostream> #include<cstring> #include<string…

SQL server 練習題2

課后作業 作業 1&#xff1a;自己查找方法&#xff0c;將 homework_1.xls 文件數據導入到 SQLServer 的 homework 數據庫中。數據導入完成后&#xff0c;把表名統一改為&#xff1a;外賣表 如下所示&#xff1a; 作業 2&#xff1a;找出所有在 2020 年 5 月 1 日至 5 月 31 …

Zookeeper之CAP理論及分布式一致性算法

CAP理論 CAP理論告訴我們&#xff0c;一個分布式系統不可能同時滿足以下三種 一致性&#xff08;C:consistency&#xff09;可用性&#xff08;A:Available&#xff09;分區容錯性&#xff08;P:Partition Tolerance&#xff09; 這三個基本要求&#xff0c;最多只能同時滿足…

python 語法學習 day2

python有七大數據類型, 數據類型轉換, 多變量賦值與print間隔, split函數, int用法總結python有七大數據類型&#xff1a; &#xff08;1&#xff09;數字&#xff08;Number&#xff09;&#xff1a;int(整型&#xff0c;長整型)&#xff0c;float(浮點型)&#xff0c;com…

部署k8s 1.28.9版本

繼上篇通過vagrant與virtualBox實現虛擬機的安裝。筆者已經將原有的vmware版本的虛擬機卸載掉了。這個場景下&#xff0c;需要重新安裝k8s 相關組件。由于之前寫的一篇文章本身也沒有截圖。只有命令。所以趁著現在。寫一篇&#xff0c;完整版帶截圖的步驟。現在行業這么卷。離…

SpringBoot中常用的注解及其用法

1. 常用類注解 RestController和Controller是Spring中用于定義控制器的兩個類注解. 1.1 RestController RestController是一個組合類注解,是Controller和ResponseBody兩個注解的組合,在使 用 RestController 注解標記的類中&#xff0c;每個方法的返回值都會以 JSON 或 XML…

【Android安全】Ubuntu 下載、編譯 、刷入Android-8.1.0_r1

0. 環境準備 Ubuntu 16.04 LTS&#xff08;預留至少95GB磁盤空間&#xff0c;實測占94.2GB&#xff09; Pixel 2 XL 要買歐版的&#xff0c;不要美版的。 歐版能解鎖BootLoader、能刷機。 美版IMEI里一般帶“v”或者"version"&#xff0c;這樣不能解鎖BootLoader、…