Redis分布式鎖核心原理源碼

文章目錄

  • 概述
  • 一、Redis實現分布式鎖
    • 1.1、第一版
    • 1.2、第二版
    • 1.3、第三版
    • 1.3、第四版
  • 二、Redisson實現分布式鎖核心源碼分析
    • 2.1、加鎖核心源碼
    • 2.2、鎖續期核心源碼
    • 2.3、重試機制核心源碼
    • 2.4、解鎖核心源碼
  • 總結


概述

??傳統的單機鎖(Synchronized,ReentrantLock)都是進程級別的鎖,無法應對服務多實例部署的場景(每個服務實例都有自己的進程),如果需要跨進程加鎖,則需要引入第三方工具對于進程統一管理。
??使用Redis可以實現簡易的分布式鎖,而最常見的成熟的分布式鎖方案是Redisson

一、Redis實現分布式鎖

??案例工程:減庫存,沒有加鎖控制,在高并發的場景下必然會出現超賣的問題。如果是在單點部署的情況下,可以通過本地鎖解決,但是目前服務多點部署,本地鎖的方案無法進行控制。

@Service
public class DistributedLockDemo {@Resourceprivate StringRedisTemplate stringRedisTemplate;private final String STOCK_PREFIX = "stock:";private final String STOCK_LOCK = "stock:lock:";public void deduceStock(int orderNum,int orderId){//業務代碼int stockNumber = Integer.parseInt(stringRedisTemplate.opsForValue().get(STOCK_PREFIX + orderId));if (stockNumber > 0){stockNumber = stockNumber - orderNum;}stringRedisTemplate.opsForValue().set(STOCK_PREFIX + orderId, String.valueOf(stockNumber));}
}

1.1、第一版

??以stock:lock:前綴加上orderId作為key,使用setIfAbsent進行加鎖,setIfAbsent命令是Redis原生的setNx命令在客戶端的體現,setNx命令是僅僅當設置的key不存在時,才可以成功,保證互斥性。
??這樣做存在的問題是,如果在執行業務代碼的過程中,出現了異常,那么解鎖的代碼則永遠無法執行,造成死鎖

@Service
public class DistributedLockDemo {@Resourceprivate StringRedisTemplate stringRedisTemplate;private final String STOCK_PREFIX = "stock:";private final String STOCK_LOCK = "stock:lock:";public void deduceStock(int orderNum,int orderId){Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(STOCK_LOCK + orderId, "lock");if (lock){//業務代碼int stockNumber = Integer.parseInt(stringRedisTemplate.opsForValue().get(STOCK_PREFIX + orderId));if (stockNumber > 0){stockNumber = stockNumber - orderNum;}stringRedisTemplate.opsForValue().set(STOCK_PREFIX + orderId, String.valueOf(stockNumber));stringRedisTemplate.delete(STOCK_LOCK + orderId);}}
}

1.2、第二版

??針對第一版的問題進行改造,將解鎖的代碼放到finally代碼塊中。這種方案依舊會存在問題,因為finally代碼塊只能保證程序出錯時最終執行,無法保證服務器宕機造成的死鎖,所以最好在加鎖時設置一個超時時間,到期自動釋放。

    public void deduceStock(int orderNum,int orderId){try {Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(STOCK_LOCK + orderId, "lock");if (lock){//業務代碼int stockNumber = Integer.parseInt(stringRedisTemplate.opsForValue().get(STOCK_PREFIX + orderId));if (stockNumber > 0){stockNumber = stockNumber - orderNum;}stringRedisTemplate.opsForValue().set(STOCK_PREFIX + orderId, String.valueOf(stockNumber));stringRedisTemplate.delete(STOCK_LOCK + orderId);}} catch (Exception e) {}finally {stringRedisTemplate.delete(STOCK_LOCK + orderId);}}

1.3、第三版

??設置超時時間,可以使用stringRedisTemplate.expire方法,但是這樣寫:

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(STOCK_LOCK + orderId, "lock");
stringRedisTemplate.expire(STOCK_LOCK + orderId, 10, TimeUnit.SECONDS);

??是不具有原子性的,需要分為兩條命令執行,如果在執行兩條命令之間出現問題,依舊會造成死鎖的問題。在Redis的層面提供了一條命令 set NX EX,保證設置超時時間和加鎖是原子性操作:

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(STOCK_LOCK + orderId, "lock",10, TimeUnit.SECONDS);

??加鎖時存在的問題看似是解決了,但是解鎖的代碼:

stringRedisTemplate.delete(STOCK_LOCK + orderId);

??會存在一種情況:

  • 線程一獲取到了鎖,然后在執行業務代碼的時候陷入了阻塞。
  • 線程一的鎖到期自動釋放。
    • 線程二獲取到了鎖,執行業務代碼
  • 線程一從阻塞狀態恢復,執行完業務代碼,要執行最終的解鎖邏輯
  • 線程一將線程二的鎖解鎖。

1.3、第四版

??為了避免當前線程將其他線程的鎖誤解鎖,需要在加鎖時加入自己的線程唯一標識,并且在解鎖時進行判斷
??注意,不要用當前Thread.currentThread().getId()方法去獲取線程ID,因為不同機器上的線程ID可能會重復。Redisson底層也不是直接用上述的API獲取的線程ID,而是和UUID進行了拼接。

//加鎖
String threadId = UUID.randomUUID().toString().replace("-","");
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(STOCK_LOCK + orderId, threadId,10, TimeUnit.SECONDS);//解鎖
String threadIdFromRedis = stringRedisTemplate.opsForValue().get("STOCK_LOCK + orderId");
if (threadId.equals(threadIdFromRedis)){stringRedisTemplate.delete(STOCK_LOCK + orderId);
}

??但是這樣寫, 解鎖和之前的加鎖設置超時時間有同樣的問題,都是操作分為了兩步,不能保證原子性。 在解鎖的判斷上,Redis并沒有提供原子性的命令,需要自己去通過lua腳本實現。


??經過四版改動,自己用Redis實現的分布式鎖已經基本可用了,但是深究下來依舊存在一些問題或不足:

  1. 如果執行業務代碼的時間,超過了設置的鎖超時時間,當前邏輯是沒有自動續期的。
  2. 當前的邏輯不支持鎖重入。
  3. 當前的邏輯沒有實現重試機制,獲取不到鎖的線程無法進行重試。

二、Redisson實現分布式鎖核心源碼分析

??相比較于自己通過set NX EX + lua腳本實現的分布式緩存鎖,Redisson是更為成熟的方案,也推薦在生產環境使用。Redisson分布式鎖在API層面是非常簡單的:

public class RedissonLockDemo {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate Redisson redisson;private final String STOCK_PREFIX = "stock:";private final String STOCK_LOCK = "stock:lock:";public void deduceStock(int orderNum, int orderId) {//獲取分布式鎖RLock lock = redisson.getLock(STOCK_LOCK + orderId);try {//加分布式鎖,可以指定超時時間,沒有指定超時時間默認30s,底層會自動續期。lock.lock();//業務代碼int stockNumber = Integer.parseInt(stringRedisTemplate.opsForValue().get(STOCK_PREFIX + orderId));if (stockNumber > 0) {stockNumber = stockNumber - orderNum;}stringRedisTemplate.opsForValue().set(STOCK_PREFIX + orderId, String.valueOf(stockNumber));} catch (Exception e) {} finally {lock.unlock();}}
}

??關鍵代碼:

//獲取分布式鎖
RLock lock = redisson.getLock(STOCK_LOCK + orderId);
//加分布式鎖,可以指定超時時間,沒有指定超時時間默認30s,底層會自動續期。
lock.lock();
//解鎖
lock.unlock();

2.1、加鎖核心源碼

??跟蹤lock.lock();,進入lockInterruptibly
在這里插入圖片描述
??首先第一次加鎖,進入的是tryAcquire方法,最終的核心邏輯是:
在這里插入圖片描述
??底層執行的是一段lua腳本,lua腳本和pipeline類似,也是可以將命令批量執行。雖然腳本中分了很多條命令,但是其他客戶端要等到當前客戶端的lua腳本全部執行完,才能執行腳本。

  • KEYS[1]:是作為當前分布式鎖的Key,也就是用戶在redisson.getLock時傳入的。
  • ARGV[1]:是默認的超時時間,30s。
  • ARGV[2]:是當前線程的唯一標識,用線程ID拼接上UUID。
# 加鎖的邏輯
# 當前分布式鎖的key不存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
#	調用hset命令,key是分布式鎖的key,value的key是當前線程的唯一標識,用線程ID拼接上UUID。 value是1(重入次數)
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
#  設置超時時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
# 返回null
"return nil; " +
"end; " +
# 重入的邏輯
# 當前分布式鎖的key存在,并且是當前線程持有
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
# 重入次數 + 1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
# 設置超時時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
# 返回null 
"return nil; " +
"end; " +
# 查詢指定鍵的剩余生存時間,并且返回
"return redis.call('pttl', KEYS[1]);"

??上面這個腳本的執行,包含了可重入鎖初次加鎖的邏輯,最終還會返回當前鎖的剩余時間

2.2、鎖續期核心源碼

??Redisson鎖的續期,也稱為看門狗機制。如果要實現鎖續期,常見的設計思想是在業務線程執行時,開啟一個守護線程,對業務線程進行監控,如果鎖到期,業務線程還沒有執行完,就執行續期的邏輯
??在Redisson中的實現,調用完tryLockInnerAsync方法后,會回調operationComplete,通過future.getNow();獲取到加鎖的結果,上面的lua腳本,在加鎖成功和重入成功后,都會返回null。
在這里插入圖片描述
??進入scheduleExpirationRenewal方法,該方法就是實現續期的核心方法實現,類似于一個延遲任務的線程池,延遲30/3 = 10s執行,整個方法分為兩部分
??首先依舊是執行一段lua腳本**(KEYS[1],ARGV[2],ARGV[1] 和第一段加鎖時的lua腳本參數含義相同)**

# 當前分布式鎖的key存在,并且是當前線程持有
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
# 進行續期30s
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
# 續期成功就返回1
"return 1; " +
"end; " +
# 否則返回 0 
"return 0;",

??第二部分則是拿到lua腳本執行的結果,遞歸調用scheduleExpirationRenewal方法,延遲10s執行,最終的結果是每隔10s進行一次續期,每次續期30s
在這里插入圖片描述

2.3、重試機制核心源碼

??加鎖時的lua腳本,如果沒有加鎖或者重入成功,那么最終返回的是key的剩余生存時間
在這里插入圖片描述

??返回到方法的最外層,在lockInterruptibly中執行自旋重試的邏輯。這里的自旋重試并非是在while循環中不斷地循環,而是有一定的間隔時間。
??在進入while循環后首先會再次嘗試獲取鎖,如果失敗了,就通過Semaphore的API,在規定的TTL毫秒內嘗試獲取許可,如果有其他線程釋放(即喚醒),當前線程就會繼續執行。如果超時仍未獲取到許可,則返回 false。
在這里插入圖片描述
??如果業務代碼執行的時間短于設置的鎖超時時間,那么其他等待鎖的線程并不會阻塞到超時時間后再去競爭鎖,在執行while循環之前,會通過redis的發布訂閱模型,將自身存入一個隊列中。
在這里插入圖片描述
??喚醒隊列中元素的邏輯,在解鎖中。

2.4、解鎖核心源碼

??解鎖同樣是通過lua腳本,將判斷線程標識和解鎖組成原子性的操作,解鎖的lua腳本在unlockInnerAsync方法中:

  • KEYS[1]:當前分布式鎖的key
  • KEYS[2]:當前分布式鎖的key 拼接上 redisson_lock__channel
  • ARGV[1]:解鎖消息標識,默認0L
  • ARGV[2]:鎖超時釋放時間
  • ARGV[3]:當前線程的唯一標識,用線程ID拼接上UUID。

??主線程在解鎖的時候會往隊列中發送給一條消息,喚醒等待線程:

  • 當前鎖不存在,超時釋放了
  • 存在并且解鎖成功
# 當前key對應的分布式鎖不存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
# 發布解鎖消息標識到當前分布式鎖的key 拼接上 redisson_lock__channel
"redis.call('publish', KEYS[2], ARGV[1]); " +
# 返回1
"return 1; " +
"end;" +
# 當前key對應的分布式鎖非本線程持有
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
# 返回null
"return nil;" +
"end; " +
# 可重入鎖的解鎖,重入次數 - 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
# 重入次數>0
"if (counter > 0) then " +
# 重新設置超時時間
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
# 返回
"return 0; " +
"else " +
# 刪除當前key對應的分布式鎖
"redis.call('del', KEYS[1]); " +
# 發布解鎖消息標識到當前分布式鎖的key 拼接上 redisson_lock__channel
"redis.call('publish', KEYS[2], ARGV[1]); " +
# 返回1
"return 1; "+
"end; " +
"return nil;",

??消費者 (正在阻塞等待的線程) 接受到了消息,會回調LockPubSubonmessage方法,被喚醒然后重新爭搶鎖。
在這里插入圖片描述

總結

在這里插入圖片描述

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

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

相關文章

關于vue2使用elform的rules校驗

在使用vue2開發項目的時候使用element組件的el-form大多數情況都需要用到必填項校驗 舉個栗子&#xff1a; <el-form :model"ruleForm" :rules"rules" ref"ruleForm" label-width"100px" class"demo-ruleForm"><e…

langchain從入門到精通(二十六)——RAG優化策略(四)問題分解策略提升負責問題檢索準確率

1. LangChain 少量示例提示模板 在與 LLM 的對話中&#xff0c;提供少量的示例被稱為 少量示例&#xff0c;這是一種簡單但強大的指導生成的方式&#xff0c;在某些情況下可以顯著提高模型性能&#xff08;與之對應的是零樣本&#xff09;&#xff0c;少量示例可以降低 Prompt…

Nuxt.js基礎(Tailwind基礎)

??1. 按鈕組件實現?? ??傳統 CSS <!-- HTML --> <button class"btn-primary">提交</button><!-- CSS --> <style>.btn-primary {background-color: #3490dc;padding: 0.5rem 1rem;border-radius: 0.25rem;color: white;transi…

[C語言]存儲結構詳解

C語言存儲結構總結 在C語言中&#xff0c;數據根據其類型和聲明方式被存儲在不同的內存區域。以下是各類數據存儲位置的詳細總結&#xff1a; 內存五大分區 存儲區存儲內容生命周期特點代碼區(.text)程序代碼(機器指令)整個程序運行期只讀常量區(.rodata)字符串常量、const全…

【實戰】 容器中Spring boot項目 Graphics2D 畫圖中文亂碼解決方案

場景 架構&#xff1a;spring boot 容器技術&#xff1a;docker 服務器&#xff1a;阿里云 開發環境&#xff1a;windows10 IDEA 一、問題 服務器中出現Graphics2D 畫圖中文亂碼 本地環境運行正常 二、原因 spring boot 容器中沒有安裝中文字體 三、解決方案 安裝字體即可 …

深入淺出:Vue2 數據劫持原理剖析

目錄 一、什么是數據劫持&#xff1f; 二、核心 API&#xff1a;Object.defineProperty 三、Vue2 中的數據劫持實現 1. 對象屬性的劫持 2. 嵌套對象的處理 3. 數組的特殊處理 四、結合依賴收集的完整流程 五、數據劫持的局限性 六、Vue3 的改進方案 總結 一、什么是數…

數據湖 vs 數據倉庫:數據界的“自來水廠”與“瓶裝水廠”?

數據湖 vs 數據倉庫&#xff1a;數據界的“自來水廠”與“瓶裝水廠”&#xff1f; 說起“數據湖”和“數據倉庫”&#xff0c;很多剛入行的朋友都會覺得&#xff1a; “聽起來好高大上啊&#xff01;但到底有啥區別啊&#xff1f;是湖更大還是倉庫更高端&#xff1f;” 我得說…

Node.js-path模塊

Path 模塊 path 模塊提供了 操作路徑 的功能&#xff0c;我們將介紹如下幾個較為常用的幾個 API ??path.resolve([…paths]) 將路徑片段??解析為絕對路徑??&#xff08;從右向左拼接&#xff0c;遇到絕對路徑停止&#xff09; // 若參數為空&#xff0c;返回當前工作目…

Java面試題029:一文深入了解MySQL(1)

歡迎大家關注我的專欄,該專欄會持續更新,從原理角度覆蓋Java知識體系的方方面面。 一文吃透JAVA知識體系(面試題)https://blog.csdn.net/wuxinyan123/category_7521898.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=7521898&

vue3.0所采用得Composition Api與Vue2.XOtions Api有什么不同?

Vue 3.0 引入的 Composition API 相較于 Vue 2.x 的 Options API 有顯著的不同。下面從幾個方面對比這兩者的差異&#xff1a; ? 1. 代碼組織方式不同 Vue 2.x — Options API 使用 data、methods、computed、watch 等分散的選項組織邏輯。 每個功能點分散在不同的選項中&am…

【IP 潮玩行業深度研究與學習】

潮玩行業發展趨勢分析&#xff1a;全球市場格局與中國政策支持體系 潮玩產業正經歷從"小眾收藏"到"大眾情緒消費"的深刻轉型&#xff0c;2025年中國潮玩市場規模已達727億元&#xff0c;預計2026年將突破1100億元&#xff0c;年復合增長率高達26%。這一千…

進程通信-消息隊列

消息隊列允許一個進程將一個消息發送到一個隊列中&#xff0c;另一個進程從該隊列中接收這個消息。 使用流程&#xff1a; 寫端&#xff1a; 使用結構體 mq_attr 設置消息隊列屬性&#xff0c;有四個選項&#xff1a; long mq_flags; // 隊列屬性: 0 表示阻塞 long …

串行通信接口USART,printf重定向數據發送,輪詢和中斷實現串口數據接收

目錄 UART通信協議的介紹 實現串口數據發送 CubeMX配置 printf重定向代碼編寫 實現串口數據接收 輪詢方式實現串口數據接收 接收單個字符 接收不定長字符串&#xff08;接收的字符串以\n結尾&#xff09; 中斷方式實現串口數據接收 CubeMX配置 UART中斷方式接收數據…

Kafka 生產者和消費者高級用法

Kafka 生產者和消費者高級用法 1 生產者的事務支持 Kafka 從版本0.11開始引入了事務支持&#xff0c;使得生產者可以實現原子操作&#xff0c;確保消息的可靠性。 // 示例代碼&#xff1a;使用 Kafka 事務 producer.initTransactions(); try {producer.beginTransaction();pr…

k8s中crictl命令常報錯解決方法

解決使用crictl命令時報默認端點棄用的報錯 報錯核心原因 默認端點棄用&#xff1a; crictl 會默認嘗試多個容器運行時端點&#xff08;如 dockershim.sock、containerd.sock 等&#xff09;&#xff0c;但這種 “自動探測” 方式已被 Kubernetes 棄用&#xff08;官方要求手動…

回轉體水下航行器簡單運動控制的奧秘:PID 控制和水動力方程的運用

在水下航行器的控制領域中&#xff0c;回轉體水下航行器的運動控制是一個關鍵課題。 今天&#xff0c;就來深入探討一下其簡單運動控制中&#xff0c;PID 控制以及水動力方程的相關運用。 PID 控制的基本原理PID 控制&#xff08;比例 - 積分 - 微分控制&#xff09;是一種廣…

從入門到精通:npm、npx、nvm 包管理工具詳解及常用命令

目錄 1. 引言2. npm (Node Package Manager)2.1 定義與用途2.2 常見命令2.3 使用示例 3. npx (Node Package Execute)3.1 定義與用途3.2 常見命令3.3 使用示例3.4 npm 與 npx 的區別 4. nvm (Node Version Manager)4.1 定義與用途4.2 安裝 nvm4.3 常見命令4.4 使用示例 5. 工具…

es6特性-第二部分

Promise 介紹和基本使用 Promise是ES6引入的異步編程的新解決方案&#xff0c;主要用來解決回調地獄問題。語法上 Promise是一個構造函數,用來封裝異步操作并可以獲取其成功或失敗的結果。 Promise構造函數:new Promise() Promise.prototype.then方法 Promise.prototype.ca…

java:如何用 JDBC 連接 TDSQL 數據庫

要使用JDBC連接TDSQL數據庫&#xff08;騰訊云分布式數據庫&#xff0c;兼容MySQL協議&#xff09;&#xff0c;請按照以下步驟編寫Java程序&#xff1a; 1. 添加MySQL JDBC驅動依賴 在項目的pom.xml中添加依賴&#xff08;Maven項目&#xff09;&#xff1a; <dependenc…

2025年四川省高考志愿填報深度分析與專業導向策略報告——基于599分/24000位次考生-AI

2025年四川省高考志愿填報深度分析與專業導向策略報告——基于599分/24000位次考生 摘要 本報告旨在為預估高考成績599分、全省物理類位次在24,000名左右的2025年四川考生&#xff0c;提供一份兼具科學性、前瞻性與專業深度的志愿填報策略方案。報告嚴格遵循“位次法”為核心…