外貿(mào)論壇有哪些平臺(tái)抖音seo搜索優(yōu)化
前言:
? ? ? 在Java后端業(yè)務(wù)中,?如果我們開(kāi)啟了均衡負(fù)載模式,也就是多臺(tái)服務(wù)器處理前端的請(qǐng)求,就會(huì)產(chǎn)生一個(gè)問(wèn)題:多臺(tái)服務(wù)器就會(huì)有多個(gè)JVM,多個(gè)JVM就會(huì)導(dǎo)致服務(wù)器集群下的并發(fā)問(wèn)題。我們?cè)谶@里提出的解決思路是把鎖交給Redis來(lái)實(shí)現(xiàn),因?yàn)镽edis是單線程的。而最基礎(chǔ)的Redis解決集群模式下的并發(fā)問(wèn)題的核心解決方案是使用Setnx構(gòu)造分布式鎖,下文來(lái)讓我們?cè)敿?xì)的看一下過(guò)程。
目錄
前言:
核心思路:?
具體業(yè)務(wù)邏輯:
業(yè)務(wù)問(wèn)題解決思路
1.選擇加鎖問(wèn)題:
2.Redis分布式鎖的誤刪問(wèn)題:
3,如何保證刪除鎖代碼的原子性?
業(yè)務(wù)雜項(xiàng)知識(shí)點(diǎn):
1.Spring mvc中的事務(wù)失效引起的并發(fā)問(wèn)題:
2.包裝類與基本數(shù)據(jù)類型的差異:
總結(jié):
?
核心思路:?
?其實(shí)整個(gè)爆改過(guò)程的思路都很清楚,我們先來(lái)解釋一下SETNX的作用:
SETNX key value
SETNX命令的作用是:只有當(dāng)指定的鍵名?
key
?不存在時(shí),將鍵值對(duì)存儲(chǔ)到Redis數(shù)據(jù)庫(kù)中。如果鍵名?key
?已經(jīng)存在,則不執(zhí)行任何操作。
那么整體的核心思路就是:讓當(dāng)前線程嘗試先創(chuàng)建A再執(zhí)行業(yè)務(wù)邏輯代碼,如果A不存在,就進(jìn)行創(chuàng)建,并執(zhí)行相關(guān)業(yè)務(wù)邏輯,業(yè)務(wù)邏輯執(zhí)行完畢后釋放A;如果A存在,那么說(shuō)明此時(shí)有其他的線程在執(zhí)行業(yè)務(wù)邏輯代碼,則拒絕當(dāng)前線程執(zhí)行業(yè)務(wù)邏輯(掛起線程)
其實(shí)就是通過(guò)SETNX構(gòu)造了一個(gè)唯一數(shù)據(jù),并且把這個(gè)數(shù)據(jù)作為鎖。這種思路使得我們的鎖不再局限于某一個(gè)JAVA對(duì)象,從而避開(kāi)了synchronized只能在JVM內(nèi)部生效。解決了集群架構(gòu)下多JVM上鎖困難的困境
具體業(yè)務(wù)邏輯:
本次的具體業(yè)務(wù)應(yīng)用場(chǎng)景是優(yōu)惠卷秒殺場(chǎng)景,簡(jiǎn)單的來(lái)講:就是商家發(fā)放優(yōu)惠卷,用戶進(jìn)行搶購(gòu)。而在優(yōu)惠卷秒殺業(yè)務(wù)中,我們需要注意的是一人一單問(wèn)題。一人一單就是一個(gè)用戶只允許下一單。而我們本項(xiàng)目的背景是允許多端登錄。我們可以想一想這個(gè)問(wèn)題的核心問(wèn)題:如果多端登錄,在服務(wù)器集群架構(gòu)的模式下,如果我們還是傳統(tǒng)模式加鎖,就會(huì)出現(xiàn)這個(gè)問(wèn)題:
用戶A同時(shí)登錄的電腦和手機(jī),在以前的模式下:我們是簡(jiǎn)單粗暴的給一人一單核心代碼直接解鎖。但這樣做有兩個(gè)問(wèn)題:
1.如果直接加鎖,那么也就是說(shuō)程序的并發(fā)性大大降低,我們一次只能處理一個(gè)用戶的優(yōu)惠卷訂單,效率大大降低。
2.如果是在集群模式下,傳統(tǒng)的鎖只能在一個(gè)JVM內(nèi)生效,并不能跨JVM。如果用戶的電腦購(gòu)買優(yōu)惠卷請(qǐng)求進(jìn)入到了服務(wù)器A,而用戶的手機(jī)購(gòu)買優(yōu)惠卷請(qǐng)求進(jìn)入到了服務(wù)器B,那么就有可能造成優(yōu)惠卷超賣的情況。
總結(jié)一下優(yōu)惠卷超賣場(chǎng)景的業(yè)務(wù)邏輯
- 查詢優(yōu)惠卷是否存在
- 查詢優(yōu)惠卷是否在售賣時(shí)間
- 查詢當(dāng)前優(yōu)惠卷是否還有庫(kù)存
- 查詢用戶是否已經(jīng)下過(guò)單(如果有直接返回給前端Result,封裝消息類)
- 扣減優(yōu)惠卷庫(kù)存
- 創(chuàng)建訂單ID
- 返回訂單號(hào)給前端
- 封裝訂單相關(guān)信息,更新數(shù)據(jù)庫(kù)
在這幾步中,從4-8步就是一人一單問(wèn)題,而解決優(yōu)惠卷秒殺問(wèn)題,大部分情況就是在解決這個(gè)問(wèn)題。
業(yè)務(wù)問(wèn)題解決思路
我們來(lái)一步一步看當(dāng)前有哪些問(wèn)題需要我們解決:
1.選擇加鎖問(wèn)題:
在我們最開(kāi)始的加鎖中,我們選擇的是synchronized關(guān)鍵字,但是它會(huì)導(dǎo)致程序的并發(fā)性大大降低。并且無(wú)法跨JVM容器生效。
我們?yōu)榱私鉀Qsynchronized關(guān)鍵字無(wú)法跨JVM容器生效,采用了SETNX關(guān)鍵字。通過(guò)這種方法,我們解決了鎖跨JVM容器生效。
synchronized 是基于JVM層面的同步機(jī)制,它會(huì)鎖定整個(gè)方法,而且它的作用范圍限定在單個(gè)JVM內(nèi)。在分布式系統(tǒng)或者集群環(huán)境中,synchronized 不能跨JVM工作,因此不適合作為分布式鎖使用。而分布式鎖 simpleRedisLock 是基于Redis實(shí)現(xiàn)的,可以跨多個(gè)應(yīng)用實(shí)例工作,適用于分布式系統(tǒng)。
但是它本質(zhì)上和synchronized關(guān)鍵字的作用一樣,并沒(méi)有解決程序的并發(fā)性大大降低的問(wèn)題。只不過(guò)以前我們是通過(guò)synchronized關(guān)鍵字?jǐn)r截線程,現(xiàn)在是通過(guò)SETNX攔截線程。
那么讓我們來(lái)逆推一下思路,加鎖是為了解決兩個(gè)問(wèn)題:
- 同一用戶在不同端多次購(gòu)買的相同優(yōu)惠卷的行為
- 不同用戶同時(shí)購(gòu)買同一優(yōu)惠卷的行為。
而我們可以先來(lái)優(yōu)化一下同一用戶在不同端多購(gòu)買的行為。按照我們之前的思路是不管三七二十一就上鎖。如圖所示可以理解為:
但是我們真的有這個(gè)必要嘛?我們仔細(xì)想一想:如果只是為了避免同一用戶在不同端多次購(gòu)買的相同優(yōu)惠卷,那么我們只需要針對(duì)這個(gè)用戶加鎖不就好了嘛?
?也就是說(shuō):現(xiàn)在我們?cè)O(shè)計(jì)的鎖,應(yīng)該是只會(huì)攔截同一個(gè)用戶的多次登錄,而不攔截多個(gè)用戶的并發(fā)登錄。如圖所示可以理解為:
我們從代碼層面解釋一下:我們利用SETNX創(chuàng)建key的時(shí)候,將key設(shè)置為USERID。那么此時(shí)就會(huì)出現(xiàn)兩種情況:
1.同一用戶多端登錄發(fā)送購(gòu)票請(qǐng)求,由于SETNX創(chuàng)建KEY的時(shí)候是根據(jù)UserID創(chuàng)建的,因此只能有一個(gè)端創(chuàng)建key成功,實(shí)現(xiàn)了為同一用戶加鎖,避免多端登錄購(gòu)票。
2.不同的用戶由于UserID不同,因此SETNX創(chuàng)建KEY的時(shí)候不會(huì)失敗,也就是說(shuō)不會(huì)被攔截。
也就是說(shuō):我們通過(guò)根據(jù)UserID構(gòu)造key的方式,實(shí)現(xiàn)了為每個(gè)用戶加鎖,提高了程序的并發(fā)性能。
我們?cè)賮?lái)解決一下:多個(gè)用戶同時(shí)購(gòu)買同一優(yōu)惠卷的問(wèn)題。我們?cè)賮?lái)轉(zhuǎn)變一下角度:之所以要處理多個(gè)用戶同時(shí)購(gòu)買同一優(yōu)惠卷,是因?yàn)闀?huì)存在超賣問(wèn)題。而我們?nèi)绾纬思渔i之外,還有沒(méi)有其他的方法解決超賣問(wèn)題呢?
答案是有的.我們?cè)诿恳淮慰蹨p庫(kù)存的時(shí)候,都同步判斷一下當(dāng)前數(shù)據(jù)庫(kù)中優(yōu)惠卷庫(kù)存是否大于0不就好了嘛!
當(dāng)然,這里要保證判斷庫(kù)存和扣減庫(kù)存的原子性,不可以被打斷。
其實(shí)這里的思路就是CAS算法,即Compare And Swap
那么選擇加鎖問(wèn)題我們已經(jīng)解決了,為了優(yōu)化普通模式下加鎖的無(wú)法跨JVM容器和拷打并發(fā)性的問(wèn)題,我們采用了以下兩個(gè)步驟:
- 無(wú)法跨容器:使用Redis中的SETNX來(lái)保證鎖可跨JVM容器
- 并發(fā)性差:利用userID構(gòu)造每個(gè)用戶專屬的鎖,并且通過(guò)數(shù)據(jù)庫(kù)操作維護(hù)多用戶下單超賣問(wèn)題。
此時(shí)我們用流程圖來(lái)展示一下當(dāng)前的執(zhí)行邏輯:
當(dāng)然了,為了避免死鎖的出現(xiàn),我們要為SETNX構(gòu)造出的鍵值對(duì)設(shè)置過(guò)期時(shí)間,防止死鎖的出現(xiàn)。
而接下來(lái)的問(wèn)題也就是我們要著重介紹的一個(gè)問(wèn)題:
2.Redis分布式鎖的誤刪問(wèn)題:
此處我們說(shuō)的是同一用戶多端登錄引發(fā)的并發(fā)性問(wèn)題,而不同用戶之間由于構(gòu)造的時(shí)候key就不一樣,因此不存在誤刪問(wèn)題。
在我們前面構(gòu)造的業(yè)務(wù)邏輯中,理想的狀態(tài)應(yīng)該是:
在理想狀態(tài)下,多段登錄可以正確的創(chuàng)建和釋放鎖,維護(hù)程序的并發(fā)性,而在我們的業(yè)務(wù)邏輯中,可能會(huì)出現(xiàn)如下異常情況:
這段異常簡(jiǎn)單的來(lái)講:線程1的阻塞使得線程1所創(chuàng)建的用戶鎖被超時(shí)釋放,此時(shí)Redis中并沒(méi)有針對(duì)當(dāng)前用戶的鎖,當(dāng)前用戶再發(fā)起一個(gè)線程2,線程2獲取到鎖。而線程1此時(shí)阻塞結(jié)束,開(kāi)始執(zhí)行業(yè)務(wù)和最后刪除鎖的操作,導(dǎo)致線程2創(chuàng)建的當(dāng)前用戶鎖被刪除。此時(shí)線程2在執(zhí)行自己的業(yè)務(wù),但是整個(gè)redis中已經(jīng)無(wú)針對(duì)當(dāng)前用戶的鎖了。線程3此時(shí)嘗試獲取鎖,獲取成功。那么在這種環(huán)境下,線程1,2,3都獲取到了鎖并且執(zhí)行了買票業(yè)務(wù)。
這種業(yè)務(wù)場(chǎng)景雖然少見(jiàn),但仍是我們要解決的問(wèn)題。
而解決的思路也很簡(jiǎn)單:主要的思路:設(shè)置鎖標(biāo)識(shí),讓每個(gè)線程只能刪除自己的鎖?
也就是說(shuō):以前我們利用SETNX創(chuàng)建鎖的時(shí)候,是不管鎖的value值的,現(xiàn)在為了解決鎖的誤刪問(wèn)題,我們要給value中賦值,使其成為鎖標(biāo)識(shí)。
我們看看代碼:
創(chuàng)建鎖:
刪除鎖:
但是這樣就對(duì)了嘛??
其實(shí)是不對(duì)的! 這是因?yàn)槲覀冊(cè)趗nlock里面執(zhí)行了多條語(yǔ)句,可能在獲取鎖的標(biāo)識(shí)的時(shí)候,還沒(méi)來(lái)得及執(zhí)行delete語(yǔ)句,線程就又被阻塞了,此時(shí)就又會(huì)發(fā)生我們之前說(shuō)的誤刪問(wèn)題。
3,如何保證刪除鎖代碼的原子性?
在這里我們使用的是lua腳本。Redis提供了lua腳本功能,在一個(gè)腳本中編寫多條Redis命令,確保多條命令執(zhí)行時(shí)的原子性。
關(guān)于lua腳本的書寫我們這里不做具體介紹,感興趣的同學(xué)可以自學(xué),lua是基于c語(yǔ)言實(shí)現(xiàn)的,他的語(yǔ)法結(jié)構(gòu)很簡(jiǎn)單。
Lua 教程 (w3schools.cn)
https://www.w3schools.cn/lua/index.asp
將之前的unlock中的redis操作轉(zhuǎn)化為lua腳本,然后再交給redis執(zhí)行。
我們來(lái)看看代碼:
通過(guò)這種方式,我們就確保了多條Redis命令的原子性,解決了刪除鎖代碼的原子性問(wèn)題。
業(yè)務(wù)雜項(xiàng)知識(shí)點(diǎn):
1.Spring mvc中的事務(wù)失效引起的并發(fā)問(wèn)題:
在代碼框架設(shè)計(jì)的時(shí)候,我把4-8過(guò)程單獨(dú)拉出來(lái)封裝了一個(gè)方法:
封裝部分代碼:?
為了保證扣減庫(kù)存的時(shí)候執(zhí)行的多條SQL語(yǔ)句的原子性,我們加上了@Transactional注解。然后在獲取鎖后執(zhí)行業(yè)務(wù)邏輯代碼的時(shí)候調(diào)用這個(gè)方法。
但這也就是一個(gè)坑點(diǎn):Spring mvc中的事務(wù)是會(huì)失效的。?
????????在Spring框架中,聲明式事務(wù)管理依賴于AOP(面向切面編程)。當(dāng)我們?cè)谝粋€(gè)方法上使用@Transactional注解時(shí),Spring將創(chuàng)建一個(gè)代理對(duì)象來(lái)包裝原始的Bean。這個(gè)代理對(duì)象會(huì)在方法調(diào)用前后添加事務(wù)管理的邏輯,如開(kāi)啟和關(guān)閉事務(wù),以及在發(fā)生異常時(shí)進(jìn)行回滾操作。
如果直接調(diào)用同一個(gè)類中的另一個(gè)@Transactional方法,由于是內(nèi)部調(diào)用,并不會(huì)經(jīng)過(guò)代理對(duì)象,因此事務(wù)管理相關(guān)的邏輯不會(huì)被執(zhí)行。這就是為什么通常建議將事務(wù)管理放在服務(wù)層(Service Layer),并且只通過(guò)注入的方式跨類調(diào)用事務(wù)方法,確保每次調(diào)用都能通過(guò)代理對(duì)象,從而讓AOP能夠正確地應(yīng)用事務(wù)管理的邏輯。
如果不使用Spring AOP代理機(jī)制,那么@Transactional注解將不會(huì)生效,因?yàn)闆](méi)有任何機(jī)制來(lái)攔截方法調(diào)用并應(yīng)用事務(wù)的邊界。這意味著即使定義了事務(wù),也不會(huì)有實(shí)際的事務(wù)行為發(fā)生,如開(kāi)始新事務(wù)、加入現(xiàn)有事務(wù)或在發(fā)生異常時(shí)回滾事務(wù)。
總結(jié)來(lái)說(shuō),Spring的聲明式事務(wù)管理是通過(guò)AOP代理實(shí)現(xiàn)的,不使用AOP代理將導(dǎo)致事務(wù)失效。要確保事務(wù)能夠正常工作,必須遵循Spring的配置和使用準(zhǔn)則,確保通過(guò)代理對(duì)象對(duì)事務(wù)方法進(jìn)行調(diào)用。
因此在調(diào)用這個(gè)方法時(shí)候,我們不能直接調(diào)用,這種方式是錯(cuò)誤的!?
而應(yīng)該這么調(diào)用:
?
2.包裝類與基本數(shù)據(jù)類型的差異:
當(dāng)我們使用stringRedisTemplate來(lái)操作Redis的時(shí)候,返回值會(huì)有包裝類型,例如Boolean。
但是如果我們直接這樣返回的話,會(huì)出現(xiàn)一個(gè)問(wèn)題:我們要求的返回值類型是boolean,也就是基本數(shù)據(jù)類型。雖然Boolean會(huì)有自動(dòng)拆箱功能,可以自動(dòng)轉(zhuǎn)換為boolean,但是可能會(huì)出現(xiàn)空指針異常!
這是為什么呢?原因很簡(jiǎn)單:Boolean是包裝類,可以存放空值,而在自動(dòng)拆箱的時(shí)候空值會(huì)轉(zhuǎn)變?yōu)榭罩羔?。而基本?shù)據(jù)類型不允許存儲(chǔ)空指針。因此直接拋出空指針異常。
總結(jié):
? ? ? ? 經(jīng)過(guò)本文的講解,我們了解了如何利用Redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式鎖。而其實(shí)Redis就已經(jīng)為我們提供了一套高性能,高可用的分布式鎖:Redission。在之后的文章我也會(huì)給大家介紹如何使用Redission。
如果我的內(nèi)容對(duì)你有幫助,請(qǐng)點(diǎn)贊,評(píng)論,收藏。創(chuàng)作不易,大家的支持就是我堅(jiān)持下去的動(dòng)力!