網(wǎng)站后臺(tái)如何做做搜索引擎優(yōu)化的企業(yè)
最核心的,包裝和準(zhǔn)備
個(gè)人項(xiàng)目,怎么包裝?一定要寫出代碼才可以嗎?
你可以在系統(tǒng)A中實(shí)現(xiàn)就可以,了解其中實(shí)現(xiàn)的細(xì)節(jié),怎么跟面試官對(duì)線等等,這些話術(shù)到位了之后,再把它融入到系統(tǒng)B,這樣即可。
舉個(gè)例子
一個(gè)大前提,你要想好怎么跟面試官對(duì)線?
知道怎么對(duì)線后,自然就知道,怎么去提前準(zhǔn)備這塊內(nèi)容,舉例子:
你的簡(jiǎn)歷寫了這句話,那么你要怎么準(zhǔn)備?
-
對(duì)熱點(diǎn)數(shù)據(jù)做緩存,針對(duì)可能的緩存穿透,同時(shí)使用緩存空值與布隆過(guò)濾器解決;針對(duì)熱點(diǎn)數(shù)據(jù)過(guò)期,根據(jù)不同的數(shù)據(jù)一致性要求,采用不同的緩存構(gòu)建方案,防止緩存擊穿;
-
你的簡(jiǎn)歷寫了異步秒殺業(yè)務(wù),你又該怎么介紹?
1、業(yè)務(wù)大致邏輯的介紹
業(yè)務(wù)是用戶可以搶購(gòu)大額代金券,來(lái)抵扣購(gòu)買課程所需金額,一個(gè)用戶只能搶購(gòu)一張大額優(yōu)惠券
相關(guān)的表結(jié)構(gòu)
平價(jià)券表
自增id、代金券標(biāo)題、副標(biāo)題、使用規(guī)則、支付金額、抵扣金額、類型 0普通 1秒殺、狀態(tài) 1 2 3、創(chuàng)建時(shí)間、更新時(shí)間
秒殺券表
在平價(jià)優(yōu)惠券基礎(chǔ)上,秒殺優(yōu)惠券有其他字段,獨(dú)立成一張表。
關(guān)聯(lián)平價(jià)券的自增id、庫(kù)存、秒殺開(kāi)始時(shí)間、秒殺結(jié)束時(shí)間、創(chuàng)建時(shí)間、更新時(shí)間
訂單表
Id 訂單編號(hào)(全局id)、下單用戶id、購(gòu)買的優(yōu)惠券id、支付方式 1 2 3、訂單狀態(tài) 1 2 3 4 5 6 7、搶購(gòu)時(shí)間、支付時(shí)間、核銷時(shí)間、退款時(shí)間、更新時(shí)間
有啥難點(diǎn)?
一人一單、不超賣、保證并發(fā)量 等等
2、代碼一步步實(shí)現(xiàn)的過(guò)程介紹
方案的比較
選擇哪個(gè)鎖?
整體邏輯的 初步設(shè)計(jì)是怎么樣的?
使用基于數(shù)據(jù)庫(kù)的鎖 以及 JVM的鎖實(shí)現(xiàn)功能
初步設(shè)計(jì)存在什么問(wèn)題呢?
多集群部署時(shí),JVM不能看到同一把鎖
后續(xù)又基于什么、或者通過(guò)什么方式進(jìn)行完善優(yōu)化?
業(yè)務(wù)遷移到 redis 來(lái)做 、由最初的 JVM層面的隊(duì)列,到引入redis的stream,再到引入MQ等等
那么優(yōu)化了多少?
數(shù)據(jù)呈現(xiàn)!qps等等
怎么迭代優(yōu)惠券秒殺功能?
業(yè)務(wù)場(chǎng)景是:用戶可以搶購(gòu)數(shù)量有限的大額優(yōu)惠券,并且每個(gè)用戶最多只能搶一張。
怎么解決超賣問(wèn)題?方案對(duì)比,選擇樂(lè)觀鎖
所以這個(gè)功能首先要完成的是:不要出現(xiàn)庫(kù)存超賣的情況,
有兩個(gè)解決方案:悲觀鎖syn & 樂(lè)觀鎖 cas
悲觀鎖的思想:認(rèn)為我在減庫(kù)存的時(shí)候,一定有其他用戶也在減,為了防止這種現(xiàn)象,減庫(kù)存時(shí),加了一個(gè)同步鎖synchronized,來(lái)解決并發(fā)問(wèn)題
樂(lè)觀鎖的思想:樂(lè)觀鎖是認(rèn)為我在減庫(kù)存的時(shí)候,不一定會(huì)發(fā)生并發(fā)問(wèn)題,就算有,我就放棄此次操作,再重新嘗試減一次。實(shí)現(xiàn)這一機(jī)制:
就是在減庫(kù)存的時(shí)候,判斷 庫(kù)存是否 > 0即可,只要是 > 0,就可以賣
當(dāng)出現(xiàn) <= 0時(shí),就減庫(kù)存失敗
基于樂(lè)觀鎖的性能比悲觀鎖要好,因?yàn)?/p>
悲觀鎖只允許一個(gè)線程在同步代碼塊執(zhí)行,其余線程必須等待鎖釋放,性能差
而基于庫(kù)存是否 > 0的樂(lè)觀鎖,只有在庫(kù)存真的 <= 0,才會(huì)并發(fā)失敗,性能遠(yuǎn)遠(yuǎn)比悲觀鎖好。
經(jīng)過(guò)以上方案的比較,項(xiàng)目采用樂(lè)觀鎖來(lái)解決超買問(wèn)題。
接下來(lái)是要解決每個(gè)用戶只能搶一張優(yōu)惠券的問(wèn)題
怎么保證每個(gè)用戶只能搶一張優(yōu)惠券呢?
項(xiàng)目是這樣解決的,首先確定無(wú)法使用樂(lè)觀鎖來(lái)解決
因?yàn)橛脩魮尩絻?yōu)惠券,在他沒(méi)搶到之前,數(shù)據(jù)庫(kù)并沒(méi)有記錄,無(wú)法根據(jù)字段進(jìn)行樂(lè)觀鎖。
所以采用悲觀鎖的方案,因?yàn)槟壳笆窃诮鉀Q單個(gè)用戶發(fā)起的并發(fā)請(qǐng)求,只需要針對(duì)單個(gè)用戶進(jìn)行加鎖,
確定鎖的粒度為每個(gè)用戶,鎖對(duì)象為用戶id,String 類型,為了防止加鎖的對(duì)象不是同一個(gè),采用的是toString().intern(),不同的請(qǐng)求,才會(huì)從字符串常量池中返回同一個(gè)對(duì)象,才能解決單個(gè)用戶并發(fā)問(wèn)題。
確定加鎖范圍判斷用戶是否已搶購(gòu) -> 樂(lè)觀鎖解決減庫(kù)存問(wèn)題 -> 把搶購(gòu)記錄,寫入數(shù)據(jù)庫(kù)
如果加鎖范圍只到樂(lè)觀鎖解決庫(kù)存問(wèn)題,是無(wú)法避免單個(gè)用戶的并發(fā)請(qǐng)求問(wèn)題的。
這是針對(duì)單個(gè)服務(wù)可用的方法,因?yàn)閟ynchronized鎖,基于JVM實(shí)例
如果部署多臺(tái)服務(wù),有多個(gè)JVM,synchronized無(wú)法做到分布式鎖,
所以在集群部署下,還會(huì)出現(xiàn)一人一單并發(fā)問(wèn)題
思考到集群下的JVM鎖問(wèn)題,采取分布式鎖優(yōu)化:
使用分布式鎖,解決集群下的一人一單問(wèn)題
為了解決上面說(shuō)到的問(wèn)題,決定使用跨JVM的鎖,即分布式鎖,redis就是很好的選擇。
首先自定義了一個(gè)比較簡(jiǎn)單的分布式鎖
存在的問(wèn)題是鎖超時(shí)釋放,但是業(yè)務(wù)還未執(zhí)行完畢
(想要更好的解決,可以使用redis分布式工具:redisson)
支持鎖重入:利用hash結(jié)構(gòu),通過(guò)記錄線程id、鎖的數(shù)量,來(lái)達(dá)到重入
鎖超時(shí)自動(dòng)續(xù)費(fèi):保證是業(yè)務(wù)執(zhí)行完畢,才釋放的鎖,不會(huì)被其他線程趁虛而入
每隔 1/3 的時(shí)間,會(huì)重置超時(shí)時(shí)間
支持鎖等待:即獲取不到鎖時(shí),利用發(fā)布訂閱 & 信號(hào)量的機(jī)制,等鎖釋放了 再去重試,對(duì)CPU友好。
到目前位置,業(yè)務(wù)流程為查詢優(yōu)惠券信息 ->加分布式鎖來(lái)解決同一用戶的并發(fā)請(qǐng)求-> 進(jìn)行一人一單的判斷,需要查詢數(shù)據(jù)庫(kù)->進(jìn)行樂(lè)觀鎖庫(kù)存超賣的判斷,需要更新數(shù)據(jù)庫(kù)->搶購(gòu)成功,創(chuàng)建訂單,寫入數(shù)據(jù)庫(kù)。
可以看到目前的流程存在大量的IO& 鎖,整體性能通過(guò)JMeter測(cè)試,
1000個(gè)用戶,200庫(kù)存的優(yōu)惠券,處理請(qǐng)求的平均耗時(shí)接近500ms
存在許多耗時(shí)的數(shù)據(jù)庫(kù)操作 & 鎖,還可以怎么提高性能呢?
基于redis:秒殺資格判斷異步寫入數(shù)據(jù)庫(kù)思路
通過(guò)定時(shí)任務(wù)把MySQL中參與秒殺的代金券,同步到Redis中做庫(kù)存的預(yù)扣減,基于Redis解決庫(kù)存超賣與一人一單,RocketMQ實(shí)現(xiàn)異步解耦,QPS從400提升至1200;
對(duì)業(yè)務(wù)進(jìn)行拆分,決定將耗時(shí)的數(shù)據(jù)庫(kù)操作,放到redis來(lái)做,具體為:秒殺資格的判斷
新增秒殺優(yōu)惠券的同時(shí),將優(yōu)惠券信息預(yù)熱在redis中
在redis中判斷用戶是否已經(jīng)下過(guò)單,
使用redis數(shù)據(jù)類型:Set,存放已經(jīng)下過(guò)單的用戶信息,
方便以O(shè)(1)復(fù)雜度判斷用戶是否下單sismember、sadd
key為:seckill:order:優(yōu)惠券id
如果還未下過(guò)單,使用redis判斷庫(kù)存是否充足,如果庫(kù)存充足,則需要減1
使用redis數(shù)據(jù)類型:hash,存儲(chǔ)優(yōu)惠券信息
get、incrby減庫(kù)存
key為:seckill:stock:優(yōu)惠券id
上述過(guò)程,是多條命令,無(wú)法保證這些命令執(zhí)行的原子性,會(huì)出現(xiàn)并發(fā)問(wèn)題,所以使用lua腳本
保證執(zhí)行上述命令的原子性
相當(dāng)于把之前的分布式鎖解決一人一單、樂(lè)觀鎖解決庫(kù)存超賣的問(wèn)題,通過(guò)基于內(nèi)存的redis解決了
大大提高性能
RocketMQ實(shí)現(xiàn)異步解耦,QPS從400提升至1200;
若判斷用戶有資格搶購(gòu),在這之前采用的是同步操作,同步等待信息寫入數(shù)據(jù)庫(kù),
即用戶請(qǐng)求需要等待搶購(gòu)信息寫入數(shù)據(jù)庫(kù),才可以返回
優(yōu)化的解決方案是:向消息隊(duì)列RocketMQ中添加消息(分布式id、優(yōu)惠券id、用戶id),立刻返回用戶請(qǐng)求,
開(kāi)啟異步線程,實(shí)現(xiàn)異步寫入數(shù)據(jù)庫(kù)的操作。減少響應(yīng)時(shí)間,提高用戶體驗(yàn)。
一開(kāi)始使用的是JDK自帶的阻塞隊(duì)列,耗時(shí)200ms
阻塞隊(duì)列在獲取消息時(shí),如果沒(méi)有消息,就阻塞住;等到有消息加入了,就被喚醒
使用jdk自帶的阻塞隊(duì)列缺點(diǎn):
-
使用的是JDK的阻塞隊(duì)列,用的是JVM的內(nèi)存,如果不加以限制,在高并發(fā)下,可能有無(wú)數(shù)的訂單放到阻塞隊(duì)列,可能會(huì)導(dǎo)致內(nèi)存溢出,也就是內(nèi)存受到限制。
-
消息一旦取出,就消失了,不能保證一定被消費(fèi)
-
不支持持久化,目前是基于內(nèi)存保存訂單信息,如果服務(wù)宕機(jī),內(nèi)存所有訂單信息都丟失;
選擇Stream消息隊(duì)列替代JDK自帶的阻塞隊(duì)列
耗時(shí)100ms
比較redis 不同方式實(shí)現(xiàn)消息隊(duì)列之間的優(yōu)缺點(diǎn),即為什么選擇Stream而不是List?
最重要的是記住Stream的優(yōu)點(diǎn)(持久化、全局ID、解決消息漏讀、pendin-list保證消息至少消費(fèi)一次、獨(dú)立于JVM的內(nèi)存、支持消費(fèi)者組消費(fèi),減少消息擠壓、可以阻塞讀取)
理解內(nèi)部實(shí)現(xiàn),來(lái)說(shuō)明為什么有這些優(yōu)點(diǎn)。
Stream相關(guān)的八股
具體落實(shí)到項(xiàng)目中,怎么實(shí)現(xiàn)?
創(chuàng)建一個(gè)Stream消息隊(duì)列,不指定上限
lua腳本判斷有資格后,向消息隊(duì)列添加消息
項(xiàng)目啟動(dòng)時(shí),開(kāi)啟異步線程,阻塞讀取Stream消息隊(duì)列中的消息,完成寫入數(shù)據(jù)庫(kù)操作
如果成功消費(fèi),那么發(fā)送ack確認(rèn)給消息隊(duì)列,消息才會(huì)從pending隊(duì)列中移除
如果消費(fèi)出現(xiàn)問(wèn)題,就到該消費(fèi)者的pending隊(duì)列中,再次消費(fèi)
專業(yè)消息隊(duì)列RocketMQ
RocketMQ使用并發(fā)消費(fèi)模式,并設(shè)置合理的線程數(shù)量(IO類型,寫庫(kù)存),快速處理隊(duì)列中堆積的消息,使
用Redis的分布式鎖+自旋鎖,對(duì)商品的庫(kù)存進(jìn)行并發(fā)控制,把并發(fā)壓力轉(zhuǎn)移到Redis中,緩解DB壓力;
因?yàn)椴l(fā)消費(fèi),對(duì)數(shù)據(jù)庫(kù)減庫(kù)存操作,是不安全的
除非直接利用數(shù)據(jù)庫(kù)樂(lè)觀鎖減
而不是先去讀再減 ,直接減
但是對(duì)DB壓力大
使用redis樂(lè)觀鎖 + sleep + 自旋來(lái)解決
3、未來(lái)展望 or 再次迭代 or 這個(gè)功能有什么可以完善的地方?
如果沒(méi)下單,庫(kù)存怎么還回去?
使用延時(shí)隊(duì)列? 那么又引出 - 延時(shí)隊(duì)列怎么實(shí)現(xiàn)的?
其實(shí)redis 的 stream同樣的,又引出八股文,這些都是需要準(zhǔn)備的 Stream相關(guān)的八股
.....
自定義的分布式鎖,相比官方提供的,存在缺陷,如:
最嚴(yán)重的 業(yè)務(wù)未結(jié)束,鎖先超時(shí)釋放了,其他線程趁虛而入、
不支持 鎖重入:用hash即可、
不支持 阻塞等待:用信號(hào)量、發(fā)布/訂閱機(jī)制 即可、
在多redis實(shí)例下,即主從模式下因?yàn)槭钱惒綇?fù)制的,導(dǎo)致分布式鎖不可靠性:官方提供的 紅鎖 解決
redisson 針對(duì)前面三個(gè)缺陷、RedLock 紅鎖
4、實(shí)現(xiàn)過(guò)程中遇到了什么難點(diǎn)?什么bug?
@Transational失效,因?yàn)椴皇谴韺?duì)象調(diào)用。深入了理解Spring事務(wù)原理 -- Aop。
怎么解決?
-
比較笨方法:新開(kāi)一個(gè)類
-
或者 自己注入自己,進(jìn)行調(diào)用,也是代理對(duì)象的調(diào)用
-
獲取代理對(duì)象來(lái)解決。
JVM的syn悲觀鎖解決一人一單問(wèn)題的時(shí)候:
用的是用戶的id,忘記intern放到字符串常量池,
導(dǎo)致獲取String對(duì)象的時(shí)候,每次都是新的對(duì)象,即 加 對(duì)象鎖出現(xiàn)問(wèn)題
還有syn鎖范圍設(shè)置的不夠大,釋放鎖之后,事務(wù)還未寫入,導(dǎo)致數(shù)據(jù)庫(kù)記錄還未變更,存在并發(fā)問(wèn)題
.....
5、如果你的簡(jiǎn)歷 關(guān)鍵字出現(xiàn),分布式id、分布式鎖、qps等等
心里就要思考到,哪些是會(huì)被提問(wèn)的?
怎么進(jìn)行壓力測(cè)試的?
QPS、并發(fā)量、平均花費(fèi)時(shí)間 等的關(guān)系:QPS和并發(fā)數(shù)和平均耗時(shí)的關(guān)系以及壓測(cè)思路_qps和并發(fā)數(shù)的關(guān)系-CSDN博客
分布式id相關(guān)的準(zhǔn)備
為什么不采用數(shù)據(jù)庫(kù)自增id?
單一表的存儲(chǔ)容量有上限
當(dāng)分表存儲(chǔ)時(shí),會(huì)存在重復(fù)的id
規(guī)律性明顯,容易看出訂單銷量等狀態(tài)
分布式ID是什么?
是應(yīng)用在分布式系統(tǒng)中,保證全局唯一的自增id。
它可以讓一個(gè)業(yè)務(wù),不管有多少個(gè)服務(wù)、多少?gòu)埍?/strong>,都可以擁有唯一的自增id。
全局唯一的分布式ID怎么實(shí)現(xiàn)?
使用redisString數(shù)據(jù)類型的incr自增命令,來(lái)幫助生成全局唯一id,有以下好處:
因?yàn)閞edis執(zhí)行命令是單線程的,所以在執(zhí)行自增命令生成自增id時(shí),
不存在并發(fā)問(wèn)題,自然不會(huì)導(dǎo)致id重復(fù)的問(wèn)題;
并且是自增的,符合分布式id要求;
并且redis基于內(nèi)存操作,性能極高;
為了保證生成的id安全性,具體如下操作:
采用long類型存儲(chǔ)id,long類型64位
· 第一個(gè)符號(hào)位,永遠(yuǎn)為0
· 接下來(lái)的31bit,采用精確到秒的時(shí)間戳進(jìn)行存儲(chǔ)
o 時(shí)間戳如何計(jì)算得來(lái):定義一個(gè)初始時(shí)間,用當(dāng)前下單時(shí)間減去初始時(shí)間,得到31bit
· 后面的32bit,是為了解決在一秒內(nèi)重復(fù)的下單,足夠容納一秒內(nèi)的訂單量
如何運(yùn)算?
先得到當(dāng)前時(shí)間 - 初始時(shí)間的時(shí)間戳,然后左移32位,給一天的訂單量讓出32位bit
使用自增命令,得到自增值,要保證不會(huì)超過(guò)32bit,然后直接進(jìn)行或運(yùn)算
return timestamp << COUNT_BITS | count;
時(shí)間戳的代碼
/** * 初始時(shí)間的時(shí)間戳,本質(zhì)是從1970-01-01 00:00:00 到2022-01-01 00:00:00 經(jīng)過(guò)多少秒 */ private static final long BEGIN_TIMESTAMP = 1640995200L;
//測(cè)試時(shí)間戳 public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); System.out.println( time.toEpochSecond(ZoneOffset.UTC)); }
自增命令的key怎么設(shè)置比較好?
在自增中,采用的是32bit來(lái)存儲(chǔ)自增值,也就是說(shuō)自增值超過(guò)32bit存儲(chǔ)容量,就會(huì)不符合我們的要求。
所以在設(shè)置key時(shí),采用一天一個(gè)key,一天訂單量很難超過(guò)32bit,也就是自增值不會(huì)超過(guò)
o 如:("icr:" + keyPrefix + ":"+"2022:03:20"),keyPrefix 為業(yè)務(wù)名稱
o 還帶來(lái)統(tǒng)計(jì)方便的好處
§ 比如某天的訂單數(shù),直接看對(duì)應(yīng)key的自增數(shù)字就可以。這樣做統(tǒng)計(jì)簡(jiǎn)單很多。
自增id生成器代碼
@Component
public class RedisIdWorker {/**\* 初始時(shí)間的時(shí)間戳,本質(zhì)是從1970-01-01 00:00:00 到2022-01-01 00:00:00 經(jīng)過(guò)多少秒*/private static final long BEGIN_TIMESTAMP = 1640995200L;//測(cè)試時(shí)間戳public static void main(String[] args) {LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);System.out.println( time.toEpochSecond(ZoneOffset.UTC));}/**\* 序列號(hào)的位數(shù)*/private static final int COUNT_BITS = 32;@Resourceprivate StringRedisTemplate stringRedisTemplate;/**\* @param keyPrefix key前綴,不同業(yè)務(wù)有不同的key\* @return long型,作為id,占用更少空間,有利于索引建立*/public long nextId(String keyPrefix) {
// 符號(hào)位不用管,只要保證正數(shù)就可以,怎么保證? 時(shí)間戳中,當(dāng)前時(shí)間 - 初始時(shí)間,當(dāng)前時(shí)間要 > 初始時(shí)間? // 1.生成當(dāng)前時(shí)間的 時(shí)間戳
? LocalDateTime now = LocalDateTime.now();
? long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
// 當(dāng)前時(shí)間 - 初始時(shí)間
? long timestamp = nowSecond - BEGIN_TIMESTAMP;? // 2.生成序列號(hào)
? // 2.1.獲取當(dāng)前日期,精確到天
? String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
?
? long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);? // 3.拼接并返回,如果直接拼接得到的是字符串,返回要long。所以這里采用位運(yùn)算
// 先把時(shí)間戳挪到高位,在這里 左移32位。 再跟序列號(hào)進(jìn)行 或運(yùn)算
? return timestamp << COUNT_BITS | count;}
}
你還了解哪些分布式ID生成算法?
除了基于redis生成的分布式id,還了解雪花算法、uuid、數(shù)據(jù)庫(kù)自增id
雪花算法 同樣采用64bit存儲(chǔ)
o 第一位表示符號(hào)位,為0
o 接下來(lái)的41bit,用于表示精確到毫秒的時(shí)間戳
o 接下來(lái)的10bit,(這一部分可以靈活調(diào)整)
§ 前5位表示機(jī)器id,后5位表示機(jī)房id
o 剩下的12bit,用來(lái)表示一毫秒內(nèi),能夠生成的id數(shù)量
優(yōu)點(diǎn):
生成速度快,有序遞增、易于再此基礎(chǔ)上改造
缺點(diǎn):
依賴于時(shí)間,當(dāng)機(jī)器的時(shí)間對(duì)應(yīng)不上時(shí),可能導(dǎo)致重復(fù)id
uuid 基于時(shí)間、機(jī)器id的生成方案
缺點(diǎn)是:
占用內(nèi)存大,128bit
時(shí)間問(wèn)題,導(dǎo)致id重復(fù)
可以保證唯一,但是不是自增的
若redis服務(wù)宕機(jī),分布式id如何生成?
采用redis主從復(fù)制 + 哨兵機(jī)制,來(lái)達(dá)到服務(wù)的高可用
當(dāng)主節(jié)點(diǎn)宕機(jī)時(shí),自動(dòng)故障轉(zhuǎn)移
主從復(fù)制保證數(shù)據(jù)同步。
6、分布式鎖相關(guān)的準(zhǔn)備
分布式鎖是什么?
滿足分布式或集群模式下,多線程可見(jiàn) 且 互斥的鎖。
怎么基于redis實(shí)現(xiàn)?
使用redis的 setnx命令,來(lái)實(shí)現(xiàn)分布式鎖,非阻塞,獲取失敗,直接返回
加鎖操作:setnx
因?yàn)閞edis執(zhí)行命令是單線程,不會(huì)并發(fā)安全問(wèn)題
并且為了防止死鎖,加了key的過(guò)期時(shí)間
并且將value設(shè)置唯一標(biāo)識(shí),是為了防止鎖誤刪的現(xiàn)象
解鎖操作:基于lua腳本,因?yàn)椴恢挂粭l命令
首先判斷該鎖是不是自己加的,即檢查唯一標(biāo)識(shí)get
如果是,才可以進(jìn)行解鎖del
鎖誤刪現(xiàn)象是什么?
比如目前線程A,持有鎖,當(dāng)時(shí)因?yàn)樽枞?#xff0c;導(dǎo)致業(yè)務(wù)沒(méi)執(zhí)行完,鎖超時(shí)釋放了
此時(shí)線程B重新持有鎖,進(jìn)行業(yè)務(wù)處理,
在線程B還沒(méi)處理完業(yè)務(wù)時(shí),線程A處理好了,并且二話不說(shuō),直接把鎖刪除了
這就導(dǎo)致線程B的鎖,被線程A刪掉的情況。導(dǎo)致鎖誤刪
這時(shí),其他線程又可以趁虛而入了。
唯一標(biāo)識(shí)怎么設(shè)置?
因?yàn)槟壳坝懻摰氖琼?xiàng)目在集群部署的環(huán)境下,線程id可能重復(fù)
所以基于每個(gè)線程的id + UUID來(lái)進(jìn)行唯一標(biāo)識(shí)的設(shè)置。
為什么解鎖要使用lua腳本
因?yàn)榻怄i是兩個(gè)操作get、del,必須保證解鎖的原子性,否則可能出現(xiàn)以下現(xiàn)象:鎖誤刪
判斷該鎖是我之前加的
進(jìn)行解鎖時(shí),阻塞了
知道鎖超時(shí)釋放,接著其他線程進(jìn)行加鎖
自己從阻塞狀態(tài)恢復(fù),執(zhí)行業(yè)務(wù),dek把別人的鎖又給刪除了
自定義的分布式鎖,存在什么問(wèn)題?
鎖誤刪問(wèn)題解決了,但是還存在一個(gè)比較嚴(yán)重的問(wèn)題,就是鎖超時(shí)時(shí)間的設(shè)置
如果設(shè)置的太短,可能業(yè)務(wù)還沒(méi)執(zhí)行完 或者 業(yè)務(wù)阻塞,導(dǎo)致鎖超時(shí)釋放
其他線程趁虛而入,又導(dǎo)致了一人不止下一單問(wèn)題的出現(xiàn)。
不支持鎖重入、鎖超時(shí)自動(dòng)續(xù)費(fèi)、鎖等待、
主從模式下因?yàn)槭钱惒綇?fù)制的,導(dǎo)致分布式鎖不可靠性
怎么解決自定義分布式鎖問(wèn)題?
使用redis分布式工具:redisson
· 支持鎖重入:利用hash結(jié)構(gòu),通過(guò)記錄線程id、鎖的數(shù)量,來(lái)達(dá)到重入
· 鎖超時(shí)自動(dòng)續(xù)費(fèi):保證是業(yè)務(wù)執(zhí)行完畢,才釋放的鎖,不會(huì)被其他線程趁虛而入
o 每隔 1/3 的時(shí)間,會(huì)重置超時(shí)時(shí)間
· 支持鎖等待:即獲取不到鎖時(shí),利用發(fā)布訂閱 & 信號(hào)量的機(jī)制,等鎖釋放了 再去重試,對(duì)CPU友好。
Redis 如何解決集群情況下分布式鎖的可靠性?
redis官方是實(shí)現(xiàn)了紅鎖RedLock,專門來(lái)解決集群模式下分布式鎖不可靠的問(wèn)題,
redis推薦使用5個(gè)獨(dú)立的redis主服務(wù)器
它加鎖的過(guò)程如下:
記錄開(kāi)始訪問(wèn)的時(shí)間t1,線程依次訪問(wèn)5個(gè)主服務(wù)器,進(jìn)行set nx px的操作,
會(huì)帶上唯一標(biāo)識(shí)
加上超時(shí)時(shí)間,是為了鎖一定會(huì)被釋放
并且還設(shè)定了獲取鎖的時(shí)間,一般設(shè)置為幾十毫秒,
如果在時(shí)間內(nèi)獲取不到,那么就返回,不會(huì)再某個(gè)redis服務(wù)耗費(fèi)太多的獲取鎖時(shí)間
最后統(tǒng)計(jì)線程成功獲取了幾把鎖,要獲取到一半以上,并且將獲取鎖的總時(shí)間 與 設(shè)置的鎖過(guò)期時(shí)間對(duì)比
如果 獲取鎖的總時(shí)間>設(shè)置的鎖過(guò)期時(shí)間,那么加鎖失敗
如果沒(méi)有獲取到一半以上的鎖,在這里是3把鎖,也是加鎖失敗
故加鎖成功要同時(shí)滿足兩個(gè)條件:
· 獲取到超過(guò)半數(shù)以上的鎖
· 加鎖的總耗時(shí),不大于 鎖的過(guò)期時(shí)間
并且在執(zhí)行業(yè)務(wù)時(shí),真正能夠利用的鎖時(shí)間為:設(shè)置的鎖超時(shí)時(shí)間 - 獲取鎖的總耗時(shí)
如果覺(jué)得鎖的時(shí)間已經(jīng)來(lái)不及完成業(yè)務(wù)執(zhí)行,那么可以直接釋放全部鎖,讓下一個(gè)線程來(lái)操作
避免業(yè)務(wù)還沒(méi)執(zhí)行完,就出現(xiàn)釋放鎖的現(xiàn)象
解鎖操作:
加鎖失敗后,會(huì)向所有redis主節(jié)點(diǎn)發(fā)起解鎖操作,執(zhí)行l(wèi)ua腳本保證解鎖的原子性
完整代碼,要稍微注意一下lua腳本怎么寫
// 在項(xiàng)目一啟動(dòng)類加載時(shí)就加載static代碼塊,只加載一次,性能最好。
// DefaultRedisScript是實(shí)現(xiàn)類,泛型為腳本的返回值類型
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
// 因?yàn)橐獙懖恢挂恍?#xff0c;所以放到代碼塊UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 去類路徑下找UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 設(shè)置返回值類型UNLOCK_SCRIPT.setResultType(Long.class);
}@Override
public void unlock() {
// 釋放鎖
// stringRedisTemplate.delete(KEY_PREFIX + name);/*// 獲取線程標(biāo)示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖中的標(biāo)示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判斷標(biāo)示是否一致if(threadId.equals(id)) {// 釋放鎖stringRedisTemplate.delete(KEY_PREFIX + name);}*/// 調(diào)用lua腳本stringRedisTemplate.execute(UNLOCK_SCRIPT,// 生成單元素的集合:singletonList方法Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}