黑馬點評筆記 分布式鎖

文章目錄

    • 分布式鎖
      • 基本原理和實現方式對比
      • Redis分布式鎖的實現核心思路
      • 實現分布式鎖版本一
      • Redis分布式鎖誤刪情況說明
      • 解決Redis分布式鎖誤刪問題
      • 分布式鎖的原子性問題
      • 分布式鎖-Redission
      • 分布式鎖-redission可重入鎖原理
      • 分布式鎖-redission鎖重試和WatchDog機制
      • 分布式鎖-redission鎖的MutiLock原理

分布式鎖

基本原理和實現方式對比

分布式鎖:滿足分布式系統或集群模式下多進程可見并且互斥的鎖。

分布式鎖的核心思想就是讓大家都使用同一把鎖,只要大家使用的是同一把鎖,那么我們就能鎖住線程,不讓線程進行,讓程序串行執行,這就是分布式鎖的核心思路

在這里插入圖片描述

那么分布式鎖他應該滿足的條件呢?

可見性:多個線程都能看到相同的結果,注意:這個地方說的可見性并不是并發編程中指的內存可見性,只是說多個進程之間都能感知到變化的意思

互斥:互斥是分布式鎖的最基本的條件,使得程序串行執行

高可用:程序不易崩潰,時時刻刻都保證較高的可用性

高性能:由于加鎖本身就讓性能降低,所有對于分布式鎖本身需要他就較高的加鎖性能和釋放鎖性能

安全性:安全也是程序中必不可少的一環

常見的分布式鎖有三種

  • Mysql:mysql本身就帶有鎖機制,但是由于mysql性能本身一般,所以采用分布式鎖的情況下,其實使用mysql作為分布式鎖比較少見

  • Redis:redis作為分布式鎖是非常常見的一種使用方式,現在企業級開發中基本都使用redis或者zookeeper作為分布式鎖,利用setnx這個方法,如果插入key成功,則表示獲得到了鎖,如果有人插入成功,其他人插入失敗則表示無法獲得到鎖,利用這套邏輯來實現分布式鎖

  • Zookeeper:zookeeper也是企業級開發中較好的一個實現分布式鎖的方案

Redis分布式鎖的實現核心思路

實現分布式鎖時需要實現的兩個基本方法:

  • 獲取鎖:

    • 互斥:確保只能有一個線程獲取鎖
    • 非阻塞:嘗試一次,成功返回true,失敗返回false
  • 釋放鎖:

    • 手動釋放
    • 超時釋放:獲取鎖時添加一個超時時間

核心思路:

我們利用redis 的setNx 方法,當有多個線程進入時,我們就利用該方法,第一個線程進入時,redis 中就有這個key 了,返回了1,如果結果是1,則表示他搶到了鎖,那么他去執行業務,然后再刪除鎖,退出鎖邏輯,沒有搶到鎖的哥們,等待一定時間后重試即可

實現分布式鎖版本一

  • 加鎖邏輯

鎖的基本接口

在這里插入圖片描述

SimpleRedisLock

利用setnx方法進行加鎖,同時增加過期時間,防止死鎖,此方法可以保證加鎖和增加過期時間具有原子性
我們的方法,是把存在線程中的用戶的id作為redis中的中的鍵,這樣我們就可以作為為每一個用戶設置單獨的鎖,而且我們也會為每個鎖設置單的過期時間從而防止死鎖,具體代碼,可以看下面:

private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {// 獲取線程標示String threadId = Thread.currentThread().getId()// 獲取鎖Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

Redis分布式鎖誤刪情況說明

邏輯說明:

持有鎖的線程在鎖的內部出現了阻塞,導致他的鎖自動釋放,這時其他線程,線程2來嘗試獲得鎖,就拿到了這把鎖,然后線程2在持有鎖執行過程中,線程1反應過來,繼續執行,而線程1執行過程中,走到了刪除鎖邏輯,此時就會把本應該屬于線程2的鎖進行刪除,這就是誤刪別人鎖的情況說明

解決方案:解決方案就是在每個線程釋放鎖的時候,去判斷一下當前這把鎖是否屬于自己,如果屬于自己,則不進行鎖的刪除,假設還是上邊的情況,線程1卡頓,鎖自動釋放,線程2進入到鎖的內部執行邏輯,此時線程1反應過來,然后刪除鎖,但是線程1,一看當前這把鎖不是屬于自己,于是不進行刪除鎖邏輯,當線程2走到刪除鎖邏輯時,如果沒有卡過自動釋放鎖的時間點,則判斷當前這把鎖是屬于自己的,于是刪除這把鎖。

在這里插入圖片描述

解決Redis分布式鎖誤刪問題

需求:修改之前的分布式鎖實現,滿足:在獲取鎖時存入線程標示(可以用UUID表示)
在釋放鎖時先獲取鎖中的線程標示,判斷是否與當前線程標示一致

  • 如果一致則釋放鎖
  • 如果不一致則不釋放鎖

核心邏輯:在存入鎖時,放入自己線程的標識,在刪除鎖時,判斷當前這把鎖的標識是不是自己存入的,如果是,則進行刪除,如果不是,則不進行刪除。

在這里插入圖片描述
具體代碼如下:加鎖

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {// 獲取線程標示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

釋放鎖

public void unlock() {// 獲取線程標示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖中的標示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判斷標示是否一致if(threadId.equals(id)) {// 釋放鎖stringRedisTemplate.delete(KEY_PREFIX + name);}
}

分布式鎖的原子性問題

更為極端的誤刪邏輯說明:

線程1現在持有鎖之后,在執行業務邏輯過程中,他正準備刪除鎖,而且已經走到了條件判斷的過程中,比如他已經拿到了當前這把鎖確實是屬于他自己的,正準備刪除鎖,但是此時他的鎖到期了,那么此時線程2進來,但是線程1他會接著往后執行,當他卡頓結束后,他直接就會執行刪除鎖那行代碼,相當于條件判斷并沒有起到作用,這就是刪鎖時的原子性問題,之所以有這個問題,是因為線程1的拿鎖,比鎖,刪鎖,實際上并不是原子性的,我們要防止剛才的情況發生,

在這里插入圖片描述
這個問題可以使用lua腳本實現,但是在java中我們一般會用redission這個第三方庫。

分布式鎖-Redission

引入依賴:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>

配置Redisson客戶端:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");// 創建RedissonClient對象return Redisson.create(config);}
}

使用Redission的分布式鎖

@Resource
private RedissionClient redissonClient;@Test
void testRedisson() throws Exception{//獲取鎖(可重入),指定鎖的名稱RLock lock = redissonClient.getLock("anyLock");//嘗試獲取鎖,參數分別是:獲取鎖的最大等待時間(期間會重試),鎖自動釋放時間,時間單位boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);//判斷獲取鎖成功if(isLock){try{System.out.println("執行業務");          }finally{//釋放鎖lock.unlock();}}}

業務代碼更改
在 VoucherOrderServiceImpl

注入RedissonClient

@Resource
private RedissonClient redissonClient;@Override
public Result seckillVoucher(Long voucherId) {// 1.查詢優惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判斷秒殺是否開始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未開始return Result.fail("秒殺尚未開始!");}// 3.判斷秒殺是否已經結束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未開始return Result.fail("秒殺已經結束!");}// 4.判斷庫存是否充足if (voucher.getStock() < 1) {// 庫存不足return Result.fail("庫存不足!");}Long userId = UserHolder.getUser().getId();//創建鎖對象 這個代碼不用了,因為我們現在要使用分布式鎖//SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);RLock lock = redissonClient.getLock("lock:order:" + userId);//獲取鎖對象boolean isLock = lock.tryLock();//加鎖失敗if (!isLock) {return Result.fail("不允許重復下單");}try {//獲取代理對象(事務)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//釋放鎖lock.unlock();}}

分布式鎖-redission可重入鎖原理

在Lock鎖中,他是借助于底層的一個voaltile的一個state變量來記錄重入的狀態的,比如當前沒有人持有這把鎖,那么state=0,假如有人持有這把鎖,那么state=1,如果持有這把鎖的人再次持有這把鎖,那么state就會+1 ,如果是對于synchronized而言,他在c語言代碼中會有一個count,原理和state類似,也是重入一次就加一,釋放一次就-1 ,直到減少成0 時,表示當前這把鎖沒有被人持有。

在redission中,我們的也支持支持可重入鎖

在分布式鎖中,他采用hash結構用來存儲鎖,其中大key表示表示這把鎖是否存在,用小key表示當前這把鎖被哪個線程持有,所以接下來我們一起分析一下當前的這個lua表達式

這個地方一共有3個參數

KEYS[1] : 鎖名稱

ARGV[1]: 鎖失效時間

ARGV[2]: id + “:” + threadId; 鎖的小key

exists: 判斷數據是否存在 name:是lock是否存在,如果==0,就表示當前這把鎖不存在

redis.call(‘hset’, KEYS[1], ARGV[2], 1);此時他就開始往redis里邊去寫數據 ,寫成一個hash結構

Lock{

? id + “:” + threadId : 1

}

如果當前這把鎖存在,則第一個條件不滿足,再判斷

redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

此時需要通過大key+小key判斷當前這把鎖是否是屬于自己的,如果是自己的,則進行

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

將當前這個鎖的value進行+1 ,redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再對其設置過期時間,如果以上兩個條件都不滿足,則表示當前這把鎖搶鎖失敗,最后返回pttl,即為當前這把鎖的失效時間

分布式鎖-redission鎖重試和WatchDog機制

搶鎖過程中,獲得當前線程,通過tryAcquire進行搶鎖,該搶鎖邏輯和之前邏輯相同

1、先判斷當前這把鎖是否存在,如果不存在,插入一把鎖,返回null

2、判斷當前這把鎖是否是屬于當前線程,如果是,則返回null

所以如果返回是null,則代表著當前已經搶鎖完畢,或者可重入完畢,但是如果以上兩個條件都不滿足,則進入到第三個條件,返回的是鎖的失效時間,同學們可以自行往下翻一點點,你能發現有個while( true) 再次進行tryAcquire進行搶鎖

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {return;
}

接下來會有一個條件分支,因為lock方法有重載方法,一個是帶參數,一個是不帶參數,如果帶帶參數傳入的值是-1,如果傳入參數,則leaseTime是他本身,所以如果傳入了參數,此時leaseTime != -1 則會進去搶鎖,搶鎖的邏輯就是之前說的那三個邏輯

if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}

如果是沒有傳入時間,則此時也會進行搶鎖, 而且搶鎖時間是默認看門狗時間 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

ttlRemainingFuture.onComplete((ttlRemaining, e) 這句話相當于對以上搶鎖進行了監聽,也就是說當上邊搶鎖完畢后,此方法會被調用,具體調用的邏輯就是去后臺開啟一個線程,進行續約邏輯,也就是看門狗線程

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}// lock acquiredif (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}
});
return ttlRemainingFuture;

此邏輯就是續約邏輯,注意看commandExecutor.getConnectionManager().newTimeout() 此方法

Method( new TimerTask() {},參數2 ,參數3 )

指的是:通過參數2,參數3 去描述什么時候去做參數1的事情,現在的情況是:10s之后去做參數一的事情

因為鎖的失效時間是30s,當10s之后,此時這個timeTask 就觸發了,他就去進行續約,把當前這把鎖續約成30s,如果操作成功,那么此時就會遞歸調用自己,再重新設置一個timeTask(),于是再過10s后又再設置一個timerTask,完成不停的續約

那么大家可以想一想,假設我們的線程出現了宕機他還會續約嗎?當然不會,因為沒有人再去調用renewExpiration這個方法,所以等到時間之后自然就釋放了。

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {// reschedule itselfrenewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

分布式鎖-redission鎖的MutiLock原理

為了提高redis的可用性,我們會搭建集群或者主從,現在以主從為例

此時我們去寫命令,寫在主機上, 主機會將數據同步給從機,但是假設在主機還沒有來得及把數據寫入到從機去的時候,此時主機宕機,哨兵會發現主機宕機,并且選舉一個slave變成master,而此時新的master中實際上并沒有鎖信息,此時鎖信息就已經丟掉了。

在這里插入圖片描述

為了解決這個問題,redission提出來了MutiLock鎖,每個節點的地位都是一樣的, 這把鎖加鎖的邏輯需要寫入到每一個主叢節點上,只有所有的服務器都寫入成功,此時才是加鎖成功,假設現在某個節點掛了,那么他去獲得鎖的時候,只要有一個節點拿不到,都不能算是加鎖成功,就保證了加鎖的可靠性。

在這里插入圖片描述

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

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

相關文章

01、Tensorflow實現二元手寫數字識別

01、Tensorflow實現二元手寫數字識別&#xff08;二分類問題&#xff09; 開始學習機器學習啦&#xff0c;已經把吳恩達的課全部刷完了&#xff0c;現在開始熟悉一下復現代碼。對這個手寫數字實部比較感興趣&#xff0c;作為入門的素材非常合適。 基于Tensorflow 2.10.0 1、…

pandas獲取年月第一天、最后一天,加一秒、加一天、午夜時間

Timestamp對象 # ts = pandas.Timestamp(year=2023, month=10, day=15, # hour=15, minute=5, second=50, tz="Asia/Shanghai") ts = pandas.Timestamp("2023-10-15 15:05:50", tz="Asia/Shanghai") # 2023-10-15 15:05…

數據丟失預防措施包括什么

數據丟失預防措施是保護企業或個人重要數據的重要手段。以下是一些有效的預防措施&#xff1a; 可以通過域之盾軟件來實現數據防丟失&#xff0c;具體的功能包括&#xff1a; https://www.yuzhidun.cn/https://www.yuzhidun.cn/ 1、備份數據 定期備份所有重要數據&#xff0…

unittest指南——不拼花哨,只拼實用

&#x1f4e2;專注于分享軟件測試干貨內容&#xff0c;歡迎點贊 &#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff01;&#x1f4e2;交流討論&#xff1a;歡迎加入我們一起學習&#xff01;&#x1f4e2;資源分享&#xff1a;耗時200小時精選的「軟件測試」資…

centos7 docker開啟認證的遠程端口2376配置

docker開啟2375會存在安全漏洞 暴露了2375端口的Docker主機。因為沒有任何加密和認證過程&#xff0c;知道了主機IP以后&#xff0c;&#xff0c;任何人都可以管理這臺主機上的容器和鏡像&#xff0c;以前貪圖方便&#xff0c;只開啟了沒有認證的docker2375端口&#xff0c;后…

代碼隨想錄算法訓練營第五十三天|1143.最長公共子序列 1035.不相交的線 53. 最大子序和

文檔講解&#xff1a;代碼隨想錄 視頻講解&#xff1a;代碼隨想錄B站賬號 狀態&#xff1a;看了視頻題解和文章解析后做出來了 1143.最長公共子序列 class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:dp [[0] * (len(text2) 1) for _ i…

機器學習入門

簡介 https://huggingface.co/是一個AI社區&#xff0c;類似于github的地位。它開源了許多機器學習需要的基礎組件如&#xff1a;Transformers, Tokenizers等。 許多公司也在不斷地往上面提交新的模型和數據集&#xff0c;利用它你可以獲取以下內容&#xff1a; Datasets : 數…

hikariCP 數據庫連接池配置

springBoot 項目默認自動使用 HikariCP &#xff0c;HikariCP 的性能比 alibaba/druid快。 一、背景 系統中多少個線程在進行與數據庫有關的工作&#xff1f;其中&#xff0c;而多少個線程正在執行 SQL 語句&#xff1f;這可以讓我們評估數據庫是不是系統瓶頸。 多少個線程在…

基于法醫調查算法優化概率神經網絡PNN的分類預測 - 附代碼

基于法醫調查算法優化概率神經網絡PNN的分類預測 - 附代碼 文章目錄 基于法醫調查算法優化概率神經網絡PNN的分類預測 - 附代碼1.PNN網絡概述2.變壓器故障診街系統相關背景2.1 模型建立 3.基于法醫調查優化的PNN網絡5.測試結果6.參考文獻7.Matlab代碼 摘要&#xff1a;針對PNN神…

【學生成績管理】數據庫示例數據(MySQL代碼)

【學生成績管理】數據庫示例數據&#xff08;MySQL代碼&#xff09; 目錄 【學生成績管理】數據庫示例數據&#xff08;MySQL代碼&#xff09;一、創建數據庫二、創建dept&#xff08;學院&#xff09;表1、創建表結構2、添加示例數據3、查看表中數據 三、創建stu&#xff08;學…

35.邏輯運算符

目錄 一.什么是邏輯運算符 二.C語言中的邏輯運算符 三.邏輯表達式 三.視頻教程 一.什么是邏輯運算符 同時對倆個或者倆個以上的表達式進行判斷的運算符叫做邏輯運算符。 舉例&#xff1a;比如去網吧上網&#xff0c;只有年滿十八周歲并且帶身份證才可以上網。在C語言中如果…

為什么 Flink 拋棄了 Scala

曾經紅遍一時的Scala 想當初Spark橫空出世之后&#xff0c;Scala簡直就是語言界的一顆璀璨新星&#xff0c;惹得大家紛紛側目&#xff0c;連Kafka這類技術框架也選擇用Scala語言進行開發重構。 可如今&#xff0c;Flink竟然公開宣布棄用Scala 在Flink1.18的官方文檔里&#x…

國家開放大學的學子們 練習題 走起!

試卷代號&#xff1a;1356 高級英語聽說(2) 參考 試題 Section One (20 points, 2 points each) Directions: Listen to the conversation and fill in the blanks with the words you hear. Write the words on the Answer Sheet The conversation will be read TWICE. M…

windows11上安裝WSL

Windows電腦上要配置linux&#xff08;這里指ubuntu&#xff09;開發環境&#xff0c;主要有三種方式&#xff1a; 1&#xff09;在windows上裝個虛擬機&#xff08;比如vmware&#xff09;。缺點是vmware加載ubuntu后系統會變慢很多&#xff0c;而且需要通過samba來實現window…

使用Java連接Hbase

我在網上試 了很多代碼&#xff0c;但是大部分都不能實現&#xff0c;Java連接Hbase&#xff0c;一直報一個錯 java.util.concurrent.ExecutionException: org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode NoNode for /hbase/hbaseid一直也不清楚為什…

計算機組成原理。3-408

1.動態存儲和靜態存儲 2.雙端口RAM 注意&#xff1a;cpu通過地址線和數據線讀寫數據時&#xff0c;不能同時寫&#xff0c;但可以同時讀&#xff0c;也不能一邊讀一邊寫。 3.多體并行存儲器 分為高位存儲和低位存儲 小結 4.磁盤存儲器的組成 5.磁盤的性能指標 磁盤讀寫尋道…

如何對網站進行滲透測試

信息搜集 信息搜集拿到域名后獲取真實IP&#xff0c;如果存在CDN想辦法繞過端口掃描&#xff0c;針對開放的端口在獲取客戶同意的前提下進行爆破查找網站子域名&#xff0c;后臺目錄判斷網站的CMS 可以使用 Wappalyzer插件 whatcms 是一個可以用來確定特定網站正在使用的什么…

Vue中Slot的使用指南

目錄 前言 什么是slot&#xff1f; 單個slot的使用 具名slot的使用 作用域插槽 總結 前言 在Vue中&#xff0c;slot是一種非常強大和靈活的功能&#xff0c;它允許你在組件模板中預留出一個或多個"插槽"&#xff0c;然后在使用這個組件的時候動態地填充內容。這…

TSINGSEE青犀智能分析網關道路積水識別AI算法方案

在各處的街道、路口等區域&#xff0c;及時發現道路積水問題&#xff0c;可以大大減少城市管理部門壓力&#xff0c;及時處理&#xff0c;減少交通事故與人員摔倒事故。通過道路積水AI算法&#xff0c;能有效提高城市管理部門效率&#xff0c;優化城市管理方式。 那么&#xff…

【Web】PhpBypassTrick相關例題wp

目錄 ①[NSSCTF 2022 Spring Recruit]babyphp ②[鶴城杯 2021]Middle magic ③[WUSTCTF 2020]樸實無華 ④[SWPUCTF 2022 新生賽]funny_php 明天中期考&#xff0c;先整理些小知識點冷靜一下 ①[NSSCTF 2022 Spring Recruit]babyphp payload: a[]1&b1[]1&b2[]2&…