【Redis】分布式鎖的實現

目錄

一、本地鎖存在的問題

二、redis實現分布式鎖原理

三、使用示例

四、鎖誤刪問題

?解決思路

獲取鎖和釋放鎖代碼優化

五、鎖釋放的原子性問題

解決思路(Lua腳本)

使用流程

總結


? ? ? ? 大家好,我是千語。上期給大家講了使用悲觀鎖來解決“一人一單”的并發場景。但上期使用的是一個本地鎖,本地鎖在集群模式下會失效。具體可以看一下我上一篇博客。


【并發問題】一人一單(悲觀鎖解決)-CSDN博客


一、本地鎖存在的問題

在集群模式下,該項目會啟動多個實例,且每個實例都會有各種的jvm。我們上面使用到的鎖其實都是本地鎖,所以就可能會出現這樣的情況:

張三在進行并發地判斷自己是否滿足一人一單時,第一個請求被分配到了實例A,獲取鎖并判斷到數據庫中還沒有改商品的訂單,可以搶購,但當還沒有完全提交事務到數據庫時,即使還沒有釋放鎖。

張三發送第二個請求被分配到了實例B,那么用戶嘗試獲取鎖時,是可以獲取到的。然后判斷到數據庫沒有訂單,可以搶單的操作,這樣又造成了一個用戶搶到了多個訂單的操作。

解析:因為每個實例都會有自己的JVM,而JVM里面都會有自己的鎖監視器,并且每個實例的鎖都是存儲在它自己的jvm里面的,所以請求分配到不同的實例,鎖監視器監視到的鎖都是打開的狀態。也就是說我們上面應用鎖的方式只是在單機的情況下適用,集群模式下就不適用了。



二、redis實現分布式鎖原理

? ? ? ? 原理就是使用redis的setnx命令,這個命令是給redis里面set值,但是只有這個鍵不存在的時候才set,所以我們要獲取鎖時,setnx一個固定的鍵,獲取鎖成功;當其他線程也想要獲取鎖時,也使用setnx命令,這時候是set不到的,所以這個線程就獲取鎖失敗。當業務執行完釋放鎖時,就把這個鍵刪除就可以了。

圖例:



三、使用示例

@Component
public class RedisLock {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 嘗試獲取分布式鎖* @param lockKey 鎖的鍵* @param expireTime 過期時間* @param timeUnit 時間單位* @return 獲取鎖成功與否*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用setIfAbsent方法嘗試獲取鎖(對應Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);//設置鎖超時時間,避免死鎖return locked != null && locked;  //set成功表明獲取鎖成功}/*** 釋放分布式鎖* @param lockKey 鎖的鍵* @return 是否釋放成功*/public boolean releaseLock(String lockKey) {return redisTemplate.delete(lockKey)}
}

業務中實際加鎖操作:


public String lockTest(){String lockKey = "product_stock_lock";try {// 嘗試獲取鎖,超時時間10秒,鎖持有時間30秒lockValue = redisLockHelper.tryLock(lockKey, 30, TimeUnit.SECONDS);if (lockValue != null) {// 獲取鎖成功,執行業務邏輯System.out.println("獲取鎖成功,處理庫存扣減...");// 模擬業務處理Thread.sleep(5000); return "庫存扣減成功";} else {// 獲取鎖失敗return "系統繁忙,請稍后重試";}} catch (InterruptedException e) {Thread.currentThread().interrupt();return "操作被中斷";} finally {// 釋放鎖(只有持有鎖的線程才能釋放)if (lockValue != null) {boolean released = redisLockHelper.releaseLock(lockKey, lockValue);System.out.println("鎖釋放結果: " + released);}}}



四、鎖誤刪問題

? ? ? ? 在上述的使用示例當中,實際上會存在鎖誤刪的問題。具體如下:

  1. 線程1獲取鎖成功,執行業務代碼后阻塞,未執行到手動釋放鎖的操作,鎖超時后自動釋放了
  2. 由于鎖超時被釋放,線程2獲取鎖成功,執行業務
  3. 線程1阻塞過后,繼續執行任務,執行了釋放鎖操作。但此時鎖其實是線程2的,由于沒有做判斷,線程1執行了釋放鎖的操作。
  4. 由于鎖已經被線程1釋放,線程3可以獲取鎖,執行業務。
  5. 結果:線程2和線程3都同時在執行了只能單個線程執行的業務。

圖例:


?解決思路

獲取鎖時,判斷一下標識是否一致;

setnx時,value的值可以設置成當前線程的name或者id

因為線程idjvm里面是自增的,所以在集群模式下,多個jvm可能會存在id相同的線程,所以也是會沖突的,所以id不可行,往下看。

所以可以使用uuid+線程id作為鎖的標識

當要釋放鎖時,先獲取鎖的值,如果是自己當前的線程id,再進行釋放鎖


獲取鎖和釋放鎖代碼優化

@Component
public class RedisLockHelper {@Autowiredprivate RedisTemplate<String, String> redisTemplate;//生成當前鎖持有者的唯一標識的uuid前綴private static final String ID_PREFIX= UUID.randomUUID().toString(true) + "-";/*** 嘗試獲取分布式鎖* @param lockKey 鎖的鍵* @param expireTime 過期時間* @param timeUnit 時間單位* @return 鎖的唯一標識,獲取失敗時為null*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用UUID前綴+當前線程id作為鎖持有者的唯一標識String lockValue = ID_PREFIX + Thread.currentThread().getid();// 使用setIfAbsent方法嘗試獲取鎖(對應Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);return locked != null && locked ? lockValue : null;}/*** 釋放分布式鎖* @param lockKey 鎖的鍵* @return 是否釋放成功*/public boolean releaseLock(String lockKey) {//獲取當前線程的標識String currentThreadLock = ID_PREFIX + Thread.currentThread().getid();// 獲取分布式鎖內的鎖標識String lockValue = redisTemplate.opsForValue().get(lockKey)  //釋放鎖時,先判斷該鎖是不是當前線程持有的      if(currentThreadLock.equals(lockValue)) {//如果當前線程是鎖的持有者,就釋放鎖return redisTemplate.delete(lockKey);}else{return false;}}
}

?業務層使用鎖的代碼不需要修改



五、鎖釋放的原子性問題

上一個問題是執行業務時線程阻塞,阻塞結束后誤刪了鎖。

所以我們在釋放鎖前先判斷一下標識,看是否是當前線程的鎖再釋放就可以解決

但是,當我們判斷完標識是一致后,線程1在進行釋放鎖之前被阻塞了(由于這兩者不是原子性)

等到鎖過期,其他線程成功獲取鎖執行業務,那么線程1又誤刪了鎖:

圖例


解決思路(Lua腳本)

使用Lua腳本,在腳本里面寫一系列操作,然后使用redis客戶端調用該腳本,這些操作就會一次性執行,滿足原子性。


使用流程

(1)創建并填寫Lua腳本文件:

注意:Lua腳本是使用lua語言來寫的。具體可以去看一下語法內容,下面只給出一種解決思路和大概的解決流程。后續可以使用redission來簡化這些操作


(2)讀取lua腳本,形成一個RedisScript,便于后續調用api


(3)執行Lua腳本,釋放鎖


(4)鎖使用:

業務中使用鎖的方法都不需要邊



總結

  1. 分布式鎖利用set nx ex的原理。(set nx的互斥性,ex保證超時釋放鎖,避免死鎖)
  2. 釋放鎖時要看看鎖是不是該線程的持有者,避免誤刪
  3. 使用Lua腳本滿足一組操作的原子性

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

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

相關文章

Unity3D對象池設計與實現詳解

前言 在Unity3D中&#xff0c;對象池&#xff08;Object Pooling&#xff09;是一種優化技術&#xff0c;用于減少頻繁實例化和銷毀對象帶來的性能開銷。以下是對象池的詳細設計和實現步驟&#xff1a; 對惹&#xff0c;這里有一個游戲開發交流小組&#xff0c;希望大家可以點…

[Spring]-組件的生命周期

組件生命周期 認識組件的聲明周期 實驗1 通過Bean指定組件的生命周期 package com.guigu.spring.ioc.bean;Data public class User {private String username;private String password;private Car car;Autowiredpublic void setCar(Car car) {System.out.println("自動…

【golang】網絡數據包捕獲庫 gopacket

詳解 github.com/google/gopacket/pcap 包 github.com/google/gopacket/pcap 是 Go 語言中一個強大的網絡數據包捕獲庫&#xff0c;它是 gopacket 項目的一部分&#xff0c;提供了對 libpcap&#xff08;Linux/Unix&#xff09;和 WinPcap&#xff08;Windows&#xff09;的 G…

RBTree的模擬實現

1&#xff1a;紅黑樹的概念 紅?樹是?棵?叉搜索樹&#xff0c;他的每個結點增加?個存儲位來表?結點的顏?&#xff0c;可以是紅?或者??。通過對任何?條從根到葉?的路徑上各個結點的顏?進?約束&#xff0c;紅?樹確保沒有?條路徑會?其他路徑?出2倍&#xff0c;因…

React 第三十九節 React Router 中的 unstable_usePrompt Hook的詳細用法及案例

React Router 中的 unstable_usePrompt 是一個用于在用戶嘗試離開當前頁面時觸發確認提示的自定義鉤子&#xff0c;常用于防止用戶誤操作導致數據丟失&#xff08;例如未保存的表單&#xff09;。 一、unstable_usePrompt用途 防止意外離開頁面&#xff1a;當用戶在當前頁面有…

OSI 7層模型

OSI 7層模型&#xff1a; 1、物理層&#xff08;光纖等把電腦連接起來的物理手段&#xff09; 2、數據鏈路層&#xff08;以太網&#xff0c;確認0和1電信號的分組方式&#xff0c;負責MAC地址&#xff0c;MAC地址用于在網絡中唯一標示一個網卡&#xff0c;相當于網卡的身份證…

視頻編解碼學習十一之視頻原始數據

一、視頻未編碼前的原始數據是怎樣的&#xff1f; 視頻在未編碼前的原始數據被稱為 原始視頻數據&#xff08;Raw Video Data&#xff09;&#xff0c;主要是按照幀&#xff08;Frame&#xff09;來組織的圖像序列。每一幀本質上就是一張圖片&#xff0c;通常采用某種顏色格式…

Redis學習打卡-Day1-SpringDataRedis、有狀態無狀態

Redis的Java客戶端 Jedis 以 Redis 命令作為方法名稱&#xff0c;學習成本低&#xff0c;簡單實用。Jedis 是線程不安全的&#xff0c;并且頻繁的創建和銷毀連接會有性能損耗&#xff0c;因此推薦使用 Jedis 連接池代替Jedis的直連方式。 lettuce Lettuce是基于Netty實現的&am…

告別靜態配置!Spring Boo動態線程池實戰指南:Nacos+Prometheus全鏈路監控

一、引言 1.1 動態線程池的必要性 傳統線程池的參數&#xff08;如核心線程數、隊列容量&#xff09;通常通過配置文件靜態定義&#xff0c;無法根據業務負載動態調整。例如&#xff0c;在電商大促場景中&#xff0c;流量可能瞬間激增&#xff0c;靜態線程池容易因配置不合理導…

Flask如何讀取配置信息

目錄 一、使用 app.config 讀取配置 二、設置配置的幾種方式 1. 直接設置 2. 從 Python 文件加載 3. 從環境變量加載 4. 從字典加載 5. 從 .env 文件加載&#xff08;推薦開發環境用&#xff09; 三、讀取配置值 四、最佳實踐建議 在 Flask 中讀取配置信息有幾種常見方…

【React中useCallback鉤子詳解】

useCallback 是 React 中的一個性能優化 Hook,用于緩存函數引用,避免在組件重新渲染時重復創建相同的函數,從而減少不必要的子組件渲染或副作用執行。以下是其核心要點: 1. 核心作用 函數記憶化:返回一個記憶化的回調函數,僅在依賴項變化時重新創建函數,否則復用之前的函…

【!!!!終極 Java 中間件實戰課:從 0 到 1 構建億級流量電商系統全鏈路解決方案!!!!保姆級教程---超細】

終極 Java 中間件實戰課:電商系統架構實戰教程 電商系統架構實戰教程1. 系統架構設計1.1 系統模塊劃分1.2 技術選型2. 環境搭建2.1 開發環境準備2.2 基礎設施部署3. 用戶服務開發3.1 創建Maven項目3.2 創建用戶服務模塊3.3 配置文件3.4 實體類與數據庫設計3.5 DAO層實現3.6 Se…

C#異步Task,await,async和Unity同步協程

標題 TaskawaitasyncUnity協程 Task Task是聲明異步任務的必要關鍵字&#xff0c;也可以使用Task<>泛型來定義Task的返回值。 await await是用于等待一個Task結束&#xff0c;否則讓出該線程控制權&#xff0c;讓步給其他線程&#xff0c;直到該Task結束才往下運行。 …

【USRP】在linux下安裝python API調用

UHD 源碼安裝 安裝庫 sudo apt-get install autoconf automake build-essential ccache cmake cpufrequtils doxygen ethtool \ g git inetutils-tools libboost-all-dev libncurses5 libncurses5-dev libusb-1.0-0 libusb-1.0-0-dev \ libusb-dev python3-dev python3-mako …

什么是 NoSQL 數據庫?它與關系型數據庫 (RDBMS) 的主要區別是什么?

我們來詳細分析一下 NoSQL 數據庫與關系型數據庫 (RDBMS) 的主要區別。 什么是 NoSQL 數據庫&#xff1f; NoSQL (通常指 “Not Only SQL” 而不僅僅是 “No SQL”) 是一類數據庫管理系統的總稱。它們的設計目標是解決傳統關系型數據庫 (RDBMS) 在某些場景下的局限性&#xf…

藍橋杯題庫經典題型

1、數列排序&#xff08;數組 排序&#xff09; 問題描述 給定一個長度為n的數列&#xff0c;將這個數列按從小到大的順序排列。1<n<200 輸入格式 第一行為一個整數n。 第二行包含n個整數&#xff0c;為待排序的數&#xff0c;每個整數的絕對值小于10000。 輸出格式 輸出…

wordpress自學筆記 第三節 獨立站產品和類目的三種展示方式

wordpress自學筆記 摘自 超詳細WordPress搭建獨立站商城教程-第三節 獨立站產品和類目的三種展示方式&#xff0c;2025 WordPress搭建獨立站教程#WordPress建站教程https://www.bilibili.com/video/BV1rwcteuETZ?spm_id_from333.788.videopod.sections&vd_sourcea0af3b…

智能手表藍牙 GATT 通訊協議文檔

以下是一份適用于智能手表的 藍牙 GATT 通訊協議文檔&#xff0c;適用于 BLE 5.0 及以上標準&#xff0c;兼容 iOS / Android 平臺&#xff1a; 智能手表藍牙 GATT 通訊協議文檔 文檔版本&#xff1a;V1.0 編寫日期&#xff1a;2025年xx月xx日 產品型號&#xff1a;Aurora Wat…

Linux PCI 驅動開發指南

注&#xff1a;本文為 “Linux PCI Drivers” 相關文章合輯。 英文引文&#xff0c;機翻未校。 中文引文&#xff0c;略作重排。 如有內容異常&#xff0c;請看原文。 How To Write Linux PCI Drivers 翻譯: 司延騰 Yanteng Si siyantengloongson.cn 1. 如何寫 Linux PCI 驅動 …

Python 接入DeepSeek

不知不覺DeepSeek已經火了半年左右&#xff0c;沖浪都趕不上時代了。 今天開始學習。 本文旨在使用Python調用DeepSeek的接口&#xff08; 這里寫目錄標題 一、環境準備1.1 DeepSeek1.2 Python 二、接入DeepSeek2.1 參數2.2 requests2.3 openai2.4 返回示例 一、環境準備 1.1…