b站黑马springCloud-常见面试题,多多三连
常見面試題
1.微服務(wù)篇
1.1.SpringCloud常見組件有哪些?
問題說明:這個(gè)題目主要考察對(duì)SpringCloud的組件基本了解
難易程度:簡(jiǎn)單
參考話術(shù):
SpringCloud包含的組件很多,有很多功能是重復(fù)的。其中最常用組件包括:
?注冊(cè)中心組件:Eureka、Nacos等
?負(fù)載均衡組件:Ribbon
?遠(yuǎn)程調(diào)用組件:OpenFeign
?網(wǎng)關(guān)組件:Zuul、Gateway
?服務(wù)保護(hù)組件:Hystrix、Sentinel
?服務(wù)配置管理組件:SpringCloudConfig、Nacos
1.2.Nacos的服務(wù)注冊(cè)表結(jié)構(gòu)是怎樣的?
問題說明:考察對(duì)Nacos數(shù)據(jù)分級(jí)結(jié)構(gòu)的了解,以及Nacos源碼的掌握情況
難易程度:一般
參考話術(shù):
Nacos采用了數(shù)據(jù)的分級(jí)存儲(chǔ)模型,最外層是Namespace,用來隔離環(huán)境。然后是Group,用來對(duì)服務(wù)分組。接下來就是服務(wù)(Service)了,一個(gè)服務(wù)包含多個(gè)實(shí)例,但是可能處于不同機(jī)房,因此Service下有多個(gè)集群(Cluster),Cluster下是不同的實(shí)例(Instance)。
對(duì)應(yīng)到Java代碼中,Nacos采用了一個(gè)多層的Map來表示。結(jié)構(gòu)為Map<String, Map<String, Service>>,其中最外層Map的key就是namespaceId,值是一個(gè)Map。內(nèi)層Map的key是group拼接serviceName,值是Service對(duì)象。Service對(duì)象內(nèi)部又是一個(gè)Map,key是集群名稱,值是Cluster對(duì)象。而Cluster對(duì)象內(nèi)部維護(hù)了Instance的集合。
如圖:
?
1.3.Nacos如何支撐阿里內(nèi)部數(shù)十萬服務(wù)注冊(cè)壓力?
問題說明:考察對(duì)Nacos源碼的掌握情況 (異步實(shí)現(xiàn))
難易程度:難
參考話術(shù):
Nacos內(nèi)部接收到注冊(cè)的請(qǐng)求時(shí),不會(huì)立即寫數(shù)據(jù),而是將服務(wù)注冊(cè)的任務(wù)放入一個(gè)阻塞隊(duì)列就立即響應(yīng)給客戶端(由于是阻塞隊(duì)列,所以只有隊(duì)列中有任務(wù)時(shí),task才會(huì)被執(zhí)行,沒有時(shí)task會(huì)等待,線程釋放CPU執(zhí)行權(quán))。然后利用線程池讀取阻塞隊(duì)列中的任務(wù),異步來完成實(shí)例更新,從而提高并發(fā)寫能力。
1.4.Nacos如何避免并發(fā)讀寫沖突問題?
問題說明:考察對(duì)Nacos源碼的掌握情況
難易程度:難
參考話術(shù):
Nacos在更新實(shí)例列表時(shí),會(huì)采用CopyOnWrite技術(shù),首先將舊的實(shí)例列表拷貝一份,然后更新拷貝的實(shí)例列表,再用更新后的實(shí)例列表來覆蓋舊的實(shí)例列表。
這樣在更新的過程中,就不會(huì)對(duì)讀實(shí)例列表的請(qǐng)求產(chǎn)生影響,也不會(huì)出現(xiàn)臟讀問題了。
1.5.Nacos與Eureka的區(qū)別有哪些?
問題說明:考察對(duì)Nacos、Eureka的底層實(shí)現(xiàn)的掌握情況
難易程度:難
參考話術(shù):
Nacos與Eureka有相同點(diǎn),也有不同之處,可以從以下幾點(diǎn)來描述:
-
接口方式:Nacos與Eureka都對(duì)外暴露了Rest風(fēng)格的API接口,用來實(shí)現(xiàn)服務(wù)注冊(cè)、發(fā)現(xiàn)等功能
-
實(shí)例類型:Nacos的實(shí)例有永久和臨時(shí)實(shí)例之分;而Eureka只支持臨時(shí)實(shí)例
-
健康檢測(cè):Nacos對(duì)臨時(shí)實(shí)例采用心跳模式檢測(cè),對(duì)永久實(shí)例采用主動(dòng)請(qǐng)求來檢測(cè);Eureka只支持心跳模式
-
服務(wù)發(fā)現(xiàn):Nacos支持定時(shí)拉取和訂閱推送兩種模式;Eureka只支持定時(shí)拉取模式
1.6.Sentinel的限流與Gateway的限流有什么差別?
問題說明:考察對(duì)限流算法的掌握情況
難易程度:難
參考話術(shù):
限流算法常見的有三種實(shí)現(xiàn):滑動(dòng)時(shí)間窗口、令牌桶算法、漏桶算法。Gateway則采用了基于Redis實(shí)現(xiàn)的令牌桶算法。
而Sentinel內(nèi)部卻比較復(fù)雜:
-
默認(rèn)限流模式是基于滑動(dòng)時(shí)間窗口算法
-
排隊(duì)等待的限流模式則基于漏桶算法
-
而熱點(diǎn)參數(shù)限流則是基于令牌桶算法
1.7.Sentinel的線程隔離與Hystix的線程隔離有什么差別?
問題說明:考察對(duì)線程隔離方案的掌握情況
難易程度:一般
參考話術(shù):
Hystix默認(rèn)是基于線程池實(shí)現(xiàn)的線程隔離,每一個(gè)被隔離的業(yè)務(wù)都要?jiǎng)?chuàng)建一個(gè)獨(dú)立的線程池,線程過多會(huì)帶來額外的CPU開銷,性能一般,但是隔離性更強(qiáng)。
Sentinel是基于信號(hào)量(計(jì)數(shù)器)實(shí)現(xiàn)的線程隔離,不用創(chuàng)建線程池,性能較好,但是隔離性一般。
2.MQ篇
2.1.你們?yōu)槭裁催x擇了RabbitMQ而不是其它的MQ?
如圖:
?
話術(shù):
kafka是以吞吐量高而聞名,不過其數(shù)據(jù)穩(wěn)定性一般,而且無法保證消息有序性。我們公司的日志收集也有使用,業(yè)務(wù)模塊中則使用的RabbitMQ。
阿里巴巴的RocketMQ基于Kafka的原理,彌補(bǔ)了Kafka的缺點(diǎn),繼承了其高吞吐的優(yōu)勢(shì),其客戶端目前以Java為主。但是我們擔(dān)心阿里巴巴開源產(chǎn)品的穩(wěn)定性,所以就沒有使用。
RabbitMQ基于面向并發(fā)的語(yǔ)言Erlang開發(fā),吞吐量不如Kafka,但是對(duì)我們公司來講夠用了。而且消息可靠性較好,并且消息延遲極低,集群搭建比較方便。支持多種協(xié)議,并且有各種語(yǔ)言的客戶端,比較靈活。Spring對(duì)RabbitMQ的支持也比較好,使用起來比較方便,比較符合我們公司的需求。
綜合考慮我們公司的并發(fā)需求以及穩(wěn)定性需求,我們選擇了RabbitMQ。
2.2.RabbitMQ如何確保消息的不丟失?
話術(shù):
RabbitMQ針對(duì)消息傳遞過程中可能發(fā)生問題的各個(gè)地方,給出了針對(duì)性的解決方案:
-
生產(chǎn)者發(fā)送消息時(shí)可能因?yàn)榫W(wǎng)絡(luò)問題導(dǎo)致消息沒有到達(dá)交換機(jī):
-
RabbitMQ提供了publisher confirm機(jī)制
-
生產(chǎn)者發(fā)送消息后,可以編寫ConfirmCallback函數(shù)
-
消息成功到達(dá)交換機(jī)后,RabbitMQ會(huì)調(diào)用ConfirmCallback通知消息的發(fā)送者,返回ACK
-
消息如果未到達(dá)交換機(jī),RabbitMQ也會(huì)調(diào)用ConfirmCallback通知消息的發(fā)送者,返回NACK
-
消息超時(shí)未發(fā)送成功也會(huì)拋出異常
-
-
-
消息到達(dá)交換機(jī)后,如果未能到達(dá)隊(duì)列,也會(huì)導(dǎo)致消息丟失:
-
RabbitMQ提供了publisher return機(jī)制
-
生產(chǎn)者可以定義ReturnCallback函數(shù)
-
消息到達(dá)交換機(jī),未到達(dá)隊(duì)列,RabbitMQ會(huì)調(diào)用ReturnCallback通知發(fā)送者,告知失敗原因
-
-
-
消息到達(dá)隊(duì)列后,MQ宕機(jī)也可能導(dǎo)致丟失消息:
-
RabbitMQ提供了持久化功能,集群的主從備份功能
-
消息持久化,RabbitMQ會(huì)將交換機(jī)、隊(duì)列、消息持久化到磁盤,宕機(jī)重啟可以恢復(fù)消息
-
鏡像集群,仲裁隊(duì)列,都可以提供主從備份功能,主節(jié)點(diǎn)宕機(jī),從節(jié)點(diǎn)會(huì)自動(dòng)切換為主,數(shù)據(jù)依然在
-
-
-
消息投遞給消費(fèi)者后,如果消費(fèi)者處理不當(dāng),也可能導(dǎo)致消息丟失
-
SpringAMQP基于RabbitMQ提供了消費(fèi)者確認(rèn)機(jī)制、消費(fèi)者重試機(jī)制,消費(fèi)者失敗處理策略:
-
消費(fèi)者的確認(rèn)機(jī)制:
-
消費(fèi)者處理消息成功,未出現(xiàn)異常時(shí),Spring返回ACK給RabbitMQ,消息才被移除
-
消費(fèi)者處理消息失敗,拋出異常,宕機(jī),Spring返回NACK或者不返回結(jié)果,消息不被異常
-
-
消費(fèi)者重試機(jī)制:
-
默認(rèn)情況下,消費(fèi)者處理失敗時(shí),消息會(huì)再次回到MQ隊(duì)列,然后投遞給其它消費(fèi)者。Spring提供的消費(fèi)者重試機(jī)制,則是在處理失敗后不返回NACK,而是直接在消費(fèi)者本地重試。多次重試都失敗后,則按照消費(fèi)者失敗處理策略來處理消息。避免了消息頻繁入隊(duì)帶來的額外壓力。
-
-
消費(fèi)者失敗策略:
-
當(dāng)消費(fèi)者多次本地重試失敗時(shí),消息默認(rèn)會(huì)丟棄。
-
Spring提供了Republish策略,在多次重試都失敗,耗盡重試次數(shù)后,將消息重新投遞給指定的異常交換機(jī),并且會(huì)攜帶上異常棧信息,幫助定位問題。
-
-
-
2.3.RabbitMQ如何避免消息堆積?
話術(shù):
消息堆積問題產(chǎn)生的原因往往是因?yàn)橄l(fā)送的速度超過了消費(fèi)者消息處理的速度。因此解決方案無外乎以下三點(diǎn):
-
提高消費(fèi)者處理速度
-
增加更多消費(fèi)者
-
增加隊(duì)列消息存儲(chǔ)上限
1)提高消費(fèi)者處理速度
消費(fèi)者處理速度是由業(yè)務(wù)代碼決定的,所以我們能做的事情包括:
-
盡可能優(yōu)化業(yè)務(wù)代碼,提高業(yè)務(wù)性能
-
接收到消息后,開啟線程池,并發(fā)處理多個(gè)消息
優(yōu)點(diǎn):成本低,改改代碼即可
缺點(diǎn):開啟線程池會(huì)帶來額外的性能開銷,對(duì)于高頻、低時(shí)延的任務(wù)不合適。推薦任務(wù)執(zhí)行周期較長(zhǎng)的業(yè)務(wù)。
2)增加更多消費(fèi)者
一個(gè)隊(duì)列綁定多個(gè)消費(fèi)者,共同爭(zhēng)搶任務(wù),自然可以提供消息處理的速度。
優(yōu)點(diǎn):能用錢解決的問題都不是問題。實(shí)現(xiàn)簡(jiǎn)單粗暴
缺點(diǎn):問題是沒有錢。成本太高
3)增加隊(duì)列消息存儲(chǔ)上限
在RabbitMQ的1.8版本后,加入了新的隊(duì)列模式:Lazy Queue
這種隊(duì)列不會(huì)將消息保存在內(nèi)存中,而是在收到消息后直接寫入磁盤中,理論上沒有存儲(chǔ)上限。可以解決消息堆積問題。
優(yōu)點(diǎn):磁盤存儲(chǔ)更安全;存儲(chǔ)無上限;避免內(nèi)存存儲(chǔ)帶來的Page Out問題,性能更穩(wěn)定;
缺點(diǎn):磁盤存儲(chǔ)受到IO性能的限制,消息時(shí)效性不如內(nèi)存模式,但影響不大。
2.4.RabbitMQ如何保證消息的有序性?
話術(shù):
其實(shí)RabbitMQ是隊(duì)列存儲(chǔ),天然具備先進(jìn)先出的特點(diǎn),只要消息的發(fā)送是有序的,那么理論上接收也是有序的。不過當(dāng)一個(gè)隊(duì)列綁定了多個(gè)消費(fèi)者時(shí),可能出現(xiàn)消息輪詢投遞給消費(fèi)者的情況,而消費(fèi)者的處理順序就無法保證了。
因此,要保證消息的有序性,需要做的下面幾點(diǎn):
-
保證消息發(fā)送的有序性
-
保證一組有序的消息都發(fā)送到同一個(gè)隊(duì)列
-
保證一個(gè)隊(duì)列只包含一個(gè)消費(fèi)者
2.5.如何防止MQ消息被重復(fù)消費(fèi)?
話術(shù):
消息重復(fù)消費(fèi)的原因多種多樣,不可避免。所以只能從消費(fèi)者端入手,只要能保證消息處理的冪等性就可以確保消息不被重復(fù)消費(fèi)。
而冪等性的保證又有很多方案:
-
給每一條消息都添加一個(gè)唯一id,在本地記錄消息表及消息狀態(tài),處理消息時(shí)基于數(shù)據(jù)庫(kù)表的id唯一性做判斷
-
同樣是記錄消息表,利用消息狀態(tài)字段實(shí)現(xiàn)基于樂觀鎖的判斷,保證冪等
-
基于業(yè)務(wù)本身的冪等性。比如根據(jù)id的刪除、查詢業(yè)務(wù)天生冪等;新增、修改等業(yè)務(wù)可以考慮基于數(shù)據(jù)庫(kù)id唯一性、或者樂觀鎖機(jī)制確保冪等。本質(zhì)與消息表方案類似。
2.6.如何保證RabbitMQ的高可用?
話術(shù):
要實(shí)現(xiàn)RabbitMQ的高可用無外乎下面兩點(diǎn):
-
做好交換機(jī)、隊(duì)列、消息的持久化
-
搭建RabbitMQ的鏡像集群,做好主從備份。當(dāng)然也可以使用仲裁隊(duì)列代替鏡像集群。
2.7.使用MQ可以解決那些問題?
話術(shù):
RabbitMQ能解決的問題很多,例如:
-
解耦合:將幾個(gè)業(yè)務(wù)關(guān)聯(lián)的微服務(wù)調(diào)用修改為基于MQ的異步通知,可以解除微服務(wù)之間的業(yè)務(wù)耦合。同時(shí)還提高了業(yè)務(wù)性能。
-
流量削峰:將突發(fā)的業(yè)務(wù)請(qǐng)求放入MQ中,作為緩沖區(qū)。后端的業(yè)務(wù)根據(jù)自己的處理能力從MQ中獲取消息,逐個(gè)處理任務(wù)。流量曲線變的平滑很多
-
延遲隊(duì)列:基于RabbitMQ的死信隊(duì)列或者DelayExchange插件,可以實(shí)現(xiàn)消息發(fā)送后,延遲接收的效果。
3.Redis篇
3.1.Redis與Memcache的區(qū)別?
-
redis支持更豐富的數(shù)據(jù)類型(支持更復(fù)雜的應(yīng)用場(chǎng)景):Redis不僅僅支持簡(jiǎn)單的k/v類型的數(shù)據(jù),同時(shí)還提供list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)。memcache支持簡(jiǎn)單的數(shù)據(jù)類型,String。
-
Redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時(shí)候可以再次加載進(jìn)行使用,而Memecache把數(shù)據(jù)全部存在內(nèi)存之中。
-
集群模式:memcached沒有原生的集群模式,需要依靠客戶端來實(shí)現(xiàn)往集群中分片寫入數(shù)據(jù);但是 redis 目前是原生支持 cluster 模式的.
-
Redis使用單線程:Memcached是多線程,非阻塞IO復(fù)用的網(wǎng)絡(luò)模型;Redis使用單線程的多路 IO 復(fù)用模型。
?
3.2.Redis的單線程問題
面試官:Redis采用單線程,如何保證高并發(fā)?
面試話術(shù):
Redis快的主要原因是:
完全基于內(nèi)存
數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)單,對(duì)數(shù)據(jù)操作也簡(jiǎn)單
使用多路 I/O 復(fù)用模型,充分利用CPU資源
面試官:這樣做的好處是什么?
面試話術(shù):
單線程優(yōu)勢(shì)有下面幾點(diǎn):
-
代碼更清晰,處理邏輯更簡(jiǎn)單
-
不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因?yàn)殒i而導(dǎo)致的性能消耗
-
不存在多進(jìn)程或者多線程導(dǎo)致的CPU切換,充分利用CPU資源
3.2.Redis的持久化方案由哪些?
相關(guān)資料:
1)RDB 持久化
RDB持久化可以使用save或bgsave,為了不阻塞主進(jìn)程業(yè)務(wù),一般都使用bgsave,流程:
-
Redis 進(jìn)程會(huì) fork 出一個(gè)子進(jìn)程(與父進(jìn)程內(nèi)存數(shù)據(jù)一致)。
-
父進(jìn)程繼續(xù)處理客戶端請(qǐng)求命令
-
由子進(jìn)程將內(nèi)存中的所有數(shù)據(jù)寫入到一個(gè)臨時(shí)的 RDB 文件中。
-
完成寫入操作之后,舊的 RDB 文件會(huì)被新的 RDB 文件替換掉。
下面是一些和 RDB 持久化相關(guān)的配置:
-
save 60 10000:如果在 60 秒內(nèi)有 10000 個(gè) key 發(fā)生改變,那就執(zhí)行 RDB 持久化。
-
stop-writes-on-bgsave-error yes:如果 Redis 執(zhí)行 RDB 持久化失敗(常見于操作系統(tǒng)內(nèi)存不足),那么 Redis 將不再接受 client 寫入數(shù)據(jù)的請(qǐng)求。
-
rdbcompression yes:當(dāng)生成 RDB 文件時(shí),同時(shí)進(jìn)行壓縮。
-
dbfilename dump.rdb:將 RDB 文件命名為 dump.rdb。
-
dir /var/lib/redis:將 RDB 文件保存在/var/lib/redis目錄下。
當(dāng)然在實(shí)踐中,我們通常會(huì)將stop-writes-on-bgsave-error設(shè)置為false,同時(shí)讓監(jiān)控系統(tǒng)在 Redis 執(zhí)行 RDB 持久化失敗時(shí)發(fā)送告警,以便人工介入解決,而不是粗暴地拒絕 client 的寫入請(qǐng)求。
RDB持久化的優(yōu)點(diǎn):
-
RDB持久化文件小,Redis數(shù)據(jù)恢復(fù)時(shí)速度快
-
子進(jìn)程不影響父進(jìn)程,父進(jìn)程可以持續(xù)處理客戶端命令
-
子進(jìn)程fork時(shí)采用copy-on-write方式,大多數(shù)情況下,沒有太多的內(nèi)存消耗,效率比較好。
RDB 持久化的缺點(diǎn):
-
子進(jìn)程fork時(shí)采用copy-on-write方式,如果Redis此時(shí)寫操作較多,可能導(dǎo)致額外的內(nèi)存占用,甚至內(nèi)存溢出
-
RDB文件壓縮會(huì)減小文件體積,但通過時(shí)會(huì)對(duì)CPU有額外的消耗
-
如果業(yè)務(wù)場(chǎng)景很看重?cái)?shù)據(jù)的持久性 (durability),那么不應(yīng)該采用 RDB 持久化。譬如說,如果 Redis 每 5 分鐘執(zhí)行一次 RDB 持久化,要是 Redis 意外奔潰了,那么最多會(huì)丟失 5 分鐘的數(shù)據(jù)。
2)AOF 持久化
可以使用appendonly yes配置項(xiàng)來開啟 AOF 持久化。Redis 執(zhí)行 AOF 持久化時(shí),會(huì)將接收到的寫命令追加到 AOF 文件的末尾,因此 Redis 只要對(duì) AOF 文件中的命令進(jìn)行回放,就可以將數(shù)據(jù)庫(kù)還原到原先的狀態(tài)。 與 RDB 持久化相比,AOF 持久化的一個(gè)明顯優(yōu)勢(shì)就是,它可以提高數(shù)據(jù)的持久性 (durability)。因?yàn)樵?AOF 模式下,Redis 每次接收到 client 的寫命令,就會(huì)將命令write()到 AOF 文件末尾。 然而,在 Linux 中,將數(shù)據(jù)write()到文件后,數(shù)據(jù)并不會(huì)立即刷新到磁盤,而會(huì)先暫存在 OS 的文件系統(tǒng)緩沖區(qū)。在合適的時(shí)機(jī),OS 才會(huì)將緩沖區(qū)的數(shù)據(jù)刷新到磁盤(如果需要將文件內(nèi)容刷新到磁盤,可以調(diào)用fsync()或fdatasync())。 通過appendfsync配置項(xiàng),可以控制 Redis 將命令同步到磁盤的頻率:
-
always:每次 Redis 將命令write()到 AOF 文件時(shí),都會(huì)調(diào)用fsync(),將命令刷新到磁盤。這可以保證最好的數(shù)據(jù)持久性,但卻會(huì)給系統(tǒng)帶來極大的開銷。
-
no:Redis 只將命令write()到 AOF 文件。這會(huì)讓 OS 決定何時(shí)將命令刷新到磁盤。
-
everysec:除了將命令write()到 AOF 文件,Redis 還會(huì)每秒執(zhí)行一次fsync()。在實(shí)踐中,推薦使用這種設(shè)置,一定程度上可以保證數(shù)據(jù)持久性,又不會(huì)明顯降低 Redis 性能。
然而,AOF 持久化并不是沒有缺點(diǎn)的:Redis 會(huì)不斷將接收到的寫命令追加到 AOF 文件中,導(dǎo)致 AOF 文件越來越大。過大的 AOF 文件會(huì)消耗磁盤空間,并且導(dǎo)致 Redis 重啟時(shí)更加緩慢。為了解決這個(gè)問題,在適當(dāng)情況下,Redis 會(huì)對(duì) AOF 文件進(jìn)行重寫,去除文件中冗余的命令,以減小 AOF 文件的體積。在重寫 AOF 文件期間, Redis 會(huì)啟動(dòng)一個(gè)子進(jìn)程,由子進(jìn)程負(fù)責(zé)對(duì) AOF 文件進(jìn)行重寫。 可以通過下面兩個(gè)配置項(xiàng),控制 Redis 重寫 AOF 文件的頻率:
-
auto-aof-rewrite-min-size 64mb
-
auto-aof-rewrite-percentage 100
上面兩個(gè)配置的作用:當(dāng) AOF 文件的體積大于 64MB,并且 AOF 文件的體積比上一次重寫之后的體積大了至少一倍,那么 Redis 就會(huì)執(zhí)行 AOF 重寫。
優(yōu)點(diǎn):
-
持久化頻率高,數(shù)據(jù)可靠性高
-
沒有額外的內(nèi)存或CPU消耗
缺點(diǎn):
-
文件體積大
-
文件大導(dǎo)致服務(wù)數(shù)據(jù)恢復(fù)時(shí)效率較低
面試話術(shù):
Redis 提供了兩種數(shù)據(jù)持久化的方式,一種是 RDB,另一種是 AOF。默認(rèn)情況下,Redis 使用的是 RDB 持久化。
RDB持久化文件體積較小,但是保存數(shù)據(jù)的頻率一般較低,可靠性差,容易丟失數(shù)據(jù)。另外RDB寫數(shù)據(jù)時(shí)會(huì)采用Fork函數(shù)拷貝主進(jìn)程,可能有額外的內(nèi)存消耗,文件壓縮也會(huì)有額外的CPU消耗。
ROF持久化可以做到每秒鐘持久化一次,可靠性高。但是持久化文件體積較大,導(dǎo)致數(shù)據(jù)恢復(fù)時(shí)讀取文件時(shí)間較長(zhǎng),效率略低
3.3.Redis的集群方式有哪些?
面試話術(shù):
Redis集群可以分為主從集群和分片集群兩類。
主從集群一般一主多從,主庫(kù)用來寫數(shù)據(jù),從庫(kù)用來讀數(shù)據(jù)。結(jié)合哨兵,可以再主庫(kù)宕機(jī)時(shí)從新選主,目的是保證Redis的高可用。
分片集群是數(shù)據(jù)分片,我們會(huì)讓多個(gè)Redis節(jié)點(diǎn)組成集群,并將16383個(gè)插槽分到不同的節(jié)點(diǎn)上。存儲(chǔ)數(shù)據(jù)時(shí)利用對(duì)key做hash運(yùn)算,得到插槽值后存儲(chǔ)到對(duì)應(yīng)的節(jié)點(diǎn)即可。因?yàn)榇鎯?chǔ)數(shù)據(jù)面向的是插槽而非節(jié)點(diǎn)本身,因此可以做到集群動(dòng)態(tài)伸縮。目的是讓Redis能存儲(chǔ)更多數(shù)據(jù)。
1)主從集群
主從集群,也是讀寫分離集群。一般都是一主多從方式。
Redis 的復(fù)制(replication)功能允許用戶根據(jù)一個(gè) Redis 服務(wù)器來創(chuàng)建任意多個(gè)該服務(wù)器的復(fù)制品,其中被復(fù)制的服務(wù)器為主服務(wù)器(master),而通過復(fù)制創(chuàng)建出來的服務(wù)器復(fù)制品則為從服務(wù)器(slave)。
只要主從服務(wù)器之間的網(wǎng)絡(luò)連接正常,主從服務(wù)器兩者會(huì)具有相同的數(shù)據(jù),主服務(wù)器就會(huì)一直將發(fā)生在自己身上的數(shù)據(jù)更新同步 給從服務(wù)器,從而一直保證主從服務(wù)器的數(shù)據(jù)相同。
-
寫數(shù)據(jù)時(shí)只能通過主節(jié)點(diǎn)完成
-
讀數(shù)據(jù)可以從任何節(jié)點(diǎn)完成
-
如果配置了哨兵節(jié)點(diǎn),當(dāng)master宕機(jī)時(shí),哨兵會(huì)從salve節(jié)點(diǎn)選出一個(gè)新的主。
主從集群分兩種:
?
?帶有哨兵的集群:
2)分片集群
主從集群中,每個(gè)節(jié)點(diǎn)都要保存所有信息,容易形成木桶效應(yīng)。并且當(dāng)數(shù)據(jù)量較大時(shí),單個(gè)機(jī)器無法滿足需求。此時(shí)我們就要使用分片集群了。
?
集群特征:
-
每個(gè)節(jié)點(diǎn)都保存不同數(shù)據(jù)
-
所有的redis節(jié)點(diǎn)彼此互聯(lián)(PING-PONG機(jī)制),內(nèi)部使用二進(jìn)制協(xié)議優(yōu)化傳輸速度和帶寬.
-
節(jié)點(diǎn)的fail是通過集群中超過半數(shù)的節(jié)點(diǎn)檢測(cè)失效時(shí)才生效.
-
客戶端與redis節(jié)點(diǎn)直連,不需要中間proxy層連接集群中任何一個(gè)可用節(jié)點(diǎn)都可以訪問到數(shù)據(jù)
-
redis-cluster把所有的物理節(jié)點(diǎn)映射到[0-16383]slot(插槽)上,實(shí)現(xiàn)動(dòng)態(tài)伸縮
為了保證Redis中每個(gè)節(jié)點(diǎn)的高可用,我們還可以給每個(gè)節(jié)點(diǎn)創(chuàng)建replication(slave節(jié)點(diǎn)),如圖:
?出現(xiàn)故障時(shí),主從可以及時(shí)切換:
?
3.4.Redis的常用數(shù)據(jù)類型有哪些?
支持多種類型的數(shù)據(jù)結(jié)構(gòu),主要區(qū)別是value存儲(chǔ)的數(shù)據(jù)格式不同:
-
string:最基本的數(shù)據(jù)類型,二進(jìn)制安全的字符串,最大512M。
-
list:按照添加順序保持順序的字符串列表。
-
set:無序的字符串集合,不存在重復(fù)的元素。
-
sorted set:已排序的字符串集合。
-
hash:key-value對(duì)格式
3.5.聊一下Redis事務(wù)機(jī)制
相關(guān)資料:
參考:事務(wù)(transaction) — Redis 命令參考
Redis事務(wù)功能是通過MULTI、EXEC、DISCARD和WATCH 四個(gè)原語(yǔ)實(shí)現(xiàn)的。Redis會(huì)將一個(gè)事務(wù)中的所有命令序列化,然后按順序執(zhí)行。但是Redis事務(wù)不支持回滾操作,命令運(yùn)行出錯(cuò)后,正確的命令會(huì)繼續(xù)執(zhí)行。
-
MULTI: 用于開啟一個(gè)事務(wù),它總是返回OK。 MULTI執(zhí)行之后,客戶端可以繼續(xù)向服務(wù)器發(fā)送任意多條命令,這些命令不會(huì)立即被執(zhí)行,而是被放到一個(gè)待執(zhí)行命令隊(duì)列中
-
EXEC:按順序執(zhí)行命令隊(duì)列內(nèi)的所有命令。返回所有命令的返回值。事務(wù)執(zhí)行過程中,Redis不會(huì)執(zhí)行其它事務(wù)的命令。
-
DISCARD:清空命令隊(duì)列,并放棄執(zhí)行事務(wù), 并且客戶端會(huì)從事務(wù)狀態(tài)中退出
-
WATCH:Redis的樂觀鎖機(jī)制,利用compare-and-set(CAS)原理,可以監(jiān)控一個(gè)或多個(gè)鍵,一旦其中有一個(gè)鍵被修改,之后的事務(wù)就不會(huì)執(zhí)行
使用事務(wù)時(shí)可能會(huì)遇上以下兩種錯(cuò)誤:
-
執(zhí)行 EXEC 之前,入隊(duì)的命令可能會(huì)出錯(cuò)。比如說,命令可能會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤(參數(shù)數(shù)量錯(cuò)誤,參數(shù)名錯(cuò)誤,等等),或者其他更嚴(yán)重的錯(cuò)誤,比如內(nèi)存不足(如果服務(wù)器使用 maxmemory 設(shè)置了最大內(nèi)存限制的話)。
-
Redis 2.6.5 開始,服務(wù)器會(huì)對(duì)命令入隊(duì)失敗的情況進(jìn)行記錄,并在客戶端調(diào)用 EXEC 命令時(shí),拒絕執(zhí)行并自動(dòng)放棄這個(gè)事務(wù)。
-
-
命令可能在 EXEC 調(diào)用之后失敗。舉個(gè)例子,事務(wù)中的命令可能處理了錯(cuò)誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。
-
即使事務(wù)中有某個(gè)/某些命令在執(zhí)行時(shí)產(chǎn)生了錯(cuò)誤, 事務(wù)中的其他命令仍然會(huì)繼續(xù)執(zhí)行,不會(huì)回滾。
-
為什么 Redis 不支持回滾(roll back)?
以下是這種做法的優(yōu)點(diǎn):
-
Redis 命令只會(huì)因?yàn)殄e(cuò)誤的語(yǔ)法而失敗(并且這些問題不能在入隊(duì)時(shí)發(fā)現(xiàn)),或是命令用在了錯(cuò)誤類型的鍵上面:這也就是說,從實(shí)用性的角度來說,失敗的命令是由編程錯(cuò)誤造成的,而這些錯(cuò)誤應(yīng)該在開發(fā)的過程中被發(fā)現(xiàn),而不應(yīng)該出現(xiàn)在生產(chǎn)環(huán)境中。
-
因?yàn)椴恍枰獙?duì)回滾進(jìn)行支持,所以 Redis 的內(nèi)部可以保持簡(jiǎn)單且快速。
鑒于沒有任何機(jī)制能避免程序員自己造成的錯(cuò)誤, 并且這類錯(cuò)誤通常不會(huì)在生產(chǎn)環(huán)境中出現(xiàn), 所以 Redis 選擇了更簡(jiǎn)單、更快速的無回滾方式來處理事務(wù)。
面試話術(shù):
Redis事務(wù)其實(shí)是把一系列Redis命令放入隊(duì)列,然后批量執(zhí)行,執(zhí)行過程中不會(huì)有其它事務(wù)來打斷。不過與關(guān)系型數(shù)據(jù)庫(kù)的事務(wù)不同,Redis事務(wù)不支持回滾操作,事務(wù)中某個(gè)命令執(zhí)行失敗,其它命令依然會(huì)執(zhí)行。
為了彌補(bǔ)不能回滾的問題,Redis會(huì)在事務(wù)入隊(duì)時(shí)就檢查命令,如果命令異常則會(huì)放棄整個(gè)事務(wù)。
因此,只要程序員編程是正確的,理論上說Redis會(huì)正確執(zhí)行所有事務(wù),無需回滾。
面試官:如果事務(wù)執(zhí)行一半的時(shí)候Redis宕機(jī)怎么辦?
Redis有持久化機(jī)制,因?yàn)榭煽啃詥栴},我們一般使用AOF持久化。事務(wù)的所有命令也會(huì)寫入AOF文件,但是如果在執(zhí)行EXEC命令之前,Redis已經(jīng)宕機(jī),則AOF文件中事務(wù)不完整。使用 redis-check-aof 程序可以移除 AOF 文件中不完整事務(wù)的信息,確保服務(wù)器可以順利啟動(dòng)。
3.6.Redis的Key過期策略
參考資料:
為什么需要內(nèi)存回收?
-
1、在Redis中,set指令可以指定key的過期時(shí)間,當(dāng)過期時(shí)間到達(dá)以后,key就失效了;
-
2、Redis是基于內(nèi)存操作的,所有的數(shù)據(jù)都是保存在內(nèi)存中,一臺(tái)機(jī)器的內(nèi)存是有限且很寶貴的。
基于以上兩點(diǎn),為了保證Redis能繼續(xù)提供可靠的服務(wù),Redis需要一種機(jī)制清理掉不常用的、無效的、多余的數(shù)據(jù),失效后的數(shù)據(jù)需要及時(shí)清理,這就需要內(nèi)存回收了。
Redis的內(nèi)存回收主要分為過期刪除策略和內(nèi)存淘汰策略兩部分。
過期刪除策略
刪除達(dá)到過期時(shí)間的key。
-
1)定時(shí)刪除
對(duì)于每一個(gè)設(shè)置了過期時(shí)間的key都會(huì)創(chuàng)建一個(gè)定時(shí)器,一旦到達(dá)過期時(shí)間就立即刪除。該策略可以立即清除過期的數(shù)據(jù),對(duì)內(nèi)存較友好,但是缺點(diǎn)是占用了大量的CPU資源去處理過期的數(shù)據(jù),會(huì)影響Redis的吞吐量和響應(yīng)時(shí)間。
-
2)惰性刪除
當(dāng)訪問一個(gè)key時(shí),才判斷該key是否過期,過期則刪除。該策略能最大限度地節(jié)省CPU資源,但是對(duì)內(nèi)存卻十分不友好。有一種極端的情況是可能出現(xiàn)大量的過期key沒有被再次訪問,因此不會(huì)被清除,導(dǎo)致占用了大量的內(nèi)存。
在計(jì)算機(jī)科學(xué)中,懶惰刪除(英文:lazy deletion)指的是從一個(gè)散列表(也稱哈希表)中刪除元素的一種方法。在這個(gè)方法中,刪除僅僅是指標(biāo)記一個(gè)元素被刪除,而不是整個(gè)清除它。被刪除的位點(diǎn)在插入時(shí)被當(dāng)作空元素,在搜索之時(shí)被當(dāng)作已占據(jù)。
-
3)定期刪除
每隔一段時(shí)間,掃描Redis中過期key字典,并清除部分過期的key。該策略是前兩者的一個(gè)折中方案,還可以通過調(diào)整定時(shí)掃描的時(shí)間間隔和每次掃描的限定耗時(shí),在不同情況下使得CPU和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。
在Redis中,同時(shí)使用了定期刪除和惰性刪除。不過Redis定期刪除采用的是隨機(jī)抽取的方式刪除部分Key,因此不能保證過期key 100%的刪除。
Redis結(jié)合了定期刪除和惰性刪除,基本上能很好的處理過期數(shù)據(jù)的清理,但是實(shí)際上還是有點(diǎn)問題的,如果過期key較多,定期刪除漏掉了一部分,而且也沒有及時(shí)去查,即沒有走惰性刪除,那么就會(huì)有大量的過期key堆積在內(nèi)存中,導(dǎo)致redis內(nèi)存耗盡,當(dāng)內(nèi)存耗盡之后,有新的key到來會(huì)發(fā)生什么事呢?是直接拋棄還是其他措施呢?有什么辦法可以接受更多的key?
內(nèi)存淘汰策略
Redis的內(nèi)存淘汰策略,是指內(nèi)存達(dá)到maxmemory極限時(shí),使用某種算法來決定清理掉哪些數(shù)據(jù),以保證新數(shù)據(jù)的存入。
Redis的內(nèi)存淘汰機(jī)制包括:
-
noeviction: 當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),新寫入操作會(huì)報(bào)錯(cuò)。
-
allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間(server.db[i].dict)中,移除最近最少使用的 key(這個(gè)是最常用的)。
-
allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間(server.db[i].dict)中,隨機(jī)移除某個(gè) key。
-
volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,移除最近最少使用的 key。
-
volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,隨機(jī)移除某個(gè) key。
-
volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,有更早過期時(shí)間的 key 優(yōu)先移除。
在配置文件中,通過maxmemory-policy可以配置要使用哪一個(gè)淘汰機(jī)制。
什么時(shí)候會(huì)進(jìn)行淘汰?
Redis會(huì)在每一次處理命令的時(shí)候(processCommand函數(shù)調(diào)用freeMemoryIfNeeded)判斷當(dāng)前redis是否達(dá)到了內(nèi)存的最大限制,如果達(dá)到限制,則使用對(duì)應(yīng)的算法去處理需要?jiǎng)h除的key。
在淘汰key時(shí),Redis默認(rèn)最常用的是LRU算法(Latest Recently Used)。Redis通過在每一個(gè)redisObject保存lru屬性來保存key最近的訪問時(shí)間,在實(shí)現(xiàn)LRU算法時(shí)直接讀取key的lru屬性。
具體實(shí)現(xiàn)時(shí),Redis遍歷每一個(gè)db,從每一個(gè)db中隨機(jī)抽取一批樣本key,默認(rèn)是3個(gè)key,再?gòu)倪@3個(gè)key中,刪除最近最少使用的key。
面試話術(shù):
Redis過期策略包含定期刪除和惰性刪除兩部分。定期刪除是在Redis內(nèi)部有一個(gè)定時(shí)任務(wù),會(huì)定期刪除一些過期的key。惰性刪除是當(dāng)用戶查詢某個(gè)Key時(shí),會(huì)檢查這個(gè)Key是否已經(jīng)過期,如果沒過期則返回用戶,如果過期則刪除。
但是這兩個(gè)策略都無法保證過期key一定刪除,漏網(wǎng)之魚越來越多,還可能導(dǎo)致內(nèi)存溢出。當(dāng)發(fā)生內(nèi)存不足問題時(shí),Redis還會(huì)做內(nèi)存回收。內(nèi)存回收采用LRU策略,就是最近最少使用。其原理就是記錄每個(gè)Key的最近使用時(shí)間,內(nèi)存回收時(shí),隨機(jī)抽取一些Key,比較其使用時(shí)間,把最老的幾個(gè)刪除。
Redis的邏輯是:最近使用過的,很可能再次被使用
3.7.Redis在項(xiàng)目中的哪些地方有用到?
(1)共享session
在分布式系統(tǒng)下,服務(wù)會(huì)部署在不同的tomcat,因此多個(gè)tomcat的session無法共享,以前存儲(chǔ)在session中的數(shù)據(jù)無法實(shí)現(xiàn)共享,可以用redis代替session,解決分布式系統(tǒng)間數(shù)據(jù)共享問題。
(2)數(shù)據(jù)緩存
Redis采用內(nèi)存存儲(chǔ),讀寫效率較高。我們可以把數(shù)據(jù)庫(kù)的訪問頻率高的熱點(diǎn)數(shù)據(jù)存儲(chǔ)到redis中,這樣用戶請(qǐng)求時(shí)優(yōu)先從redis中讀取,減少數(shù)據(jù)庫(kù)壓力,提高并發(fā)能力。
(3)異步隊(duì)列
Reids在內(nèi)存存儲(chǔ)引擎領(lǐng)域的一大優(yōu)點(diǎn)是提供 list 和 set 操作,這使得Redis能作為一個(gè)很好的消息隊(duì)列平臺(tái)來使用。而且Redis中還有pub/sub這樣的專用結(jié)構(gòu),用于1對(duì)N的消息通信模式。
(4)分布式鎖
Redis中的樂觀鎖機(jī)制,可以幫助我們實(shí)現(xiàn)分布式鎖的效果,用于解決分布式系統(tǒng)下的多線程安全問題
3.8.Redis的緩存擊穿、緩存雪崩、緩存穿透
1)緩存穿透
參考資料:
-
什么是緩存穿透
-
正常情況下,我們?nèi)ゲ樵償?shù)據(jù)都是存在。那么請(qǐng)求去查詢一條壓根兒數(shù)據(jù)庫(kù)中根本就不存在的數(shù)據(jù),也就是緩存和數(shù)據(jù)庫(kù)都查詢不到這條數(shù)據(jù),但是請(qǐng)求每次都會(huì)打到數(shù)據(jù)庫(kù)上面去。這種查詢不存在數(shù)據(jù)的現(xiàn)象我們稱為緩存穿透。
-
-
穿透帶來的問題
-
試想一下,如果有黑客會(huì)對(duì)你的系統(tǒng)進(jìn)行攻擊,拿一個(gè)不存在的id 去查詢數(shù)據(jù),會(huì)產(chǎn)生大量的請(qǐng)求到數(shù)據(jù)庫(kù)去查詢。可能會(huì)導(dǎo)致你的數(shù)據(jù)庫(kù)由于壓力過大而宕掉。
-
-
解決辦法
-
緩存空值:之所以會(huì)發(fā)生穿透,就是因?yàn)榫彺嬷袥]有存儲(chǔ)這些空數(shù)據(jù)的key。從而導(dǎo)致每次查詢都到數(shù)據(jù)庫(kù)去了。那么我們就可以為這些key對(duì)應(yīng)的值設(shè)置為null 丟到緩存里面去。后面再出現(xiàn)查詢這個(gè)key 的請(qǐng)求的時(shí)候,直接返回null 。這樣,就不用在到數(shù)據(jù)庫(kù)中去走一圈了,但是別忘了設(shè)置過期時(shí)間。
-
BloomFilter(布隆過濾):將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。在緩存之前在加一層 BloomFilter ,在查詢的時(shí)候先去 BloomFilter 去查詢 key 是否存在,如果不存在就直接返回,存在再走查緩存 -> 查 DB。
-
話術(shù):
緩存穿透有兩種解決方案:其一是把不存在的key設(shè)置null值到緩存中。其二是使用布隆過濾器,在查詢緩存前先通過布隆過濾器判斷key是否存在,存在再去查詢緩存。
設(shè)置null值可能被惡意針對(duì),攻擊者使用大量不存在的不重復(fù)key ,那么方案一就會(huì)緩存大量不存在key數(shù)據(jù)。此時(shí)我們還可以對(duì)Key規(guī)定格式模板,然后對(duì)不存在的key做正則規(guī)范匹配,如果完全不符合就不用存null值到redis,而是直接返回錯(cuò)誤。
2)緩存擊穿
相關(guān)資料:
-
什么是緩存擊穿?
key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問,是一種非常“熱點(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候,需要考慮一個(gè)問題:緩存被“擊穿”的問題。
當(dāng)這個(gè)key在失效的瞬間,redis查詢失敗,持續(xù)的大并發(fā)就穿破緩存,直接請(qǐng)求數(shù)據(jù)庫(kù),就像在一個(gè)屏障上鑿開了一個(gè)洞。
-
解決方案:
-
使用互斥鎖(mutex key):mutex,就是互斥。簡(jiǎn)單地來說,就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db,而是先使用Redis的SETNX去set一個(gè)互斥key,當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個(gè)get緩存的方法。SETNX,是「SET if Not eXists」的縮寫,也就是只有不存在的時(shí)候才設(shè)置,可以利用它來實(shí)現(xiàn)互斥的效果。
-
軟過期:也就是邏輯過期,不使用redis提供的過期時(shí)間,而是業(yè)務(wù)層在數(shù)據(jù)中存儲(chǔ)過期時(shí)間信息。查詢時(shí)由業(yè)務(wù)程序判斷是否過期,如果數(shù)據(jù)即將過期時(shí),將緩存的時(shí)效延長(zhǎng),程序可以派遣一個(gè)線程去數(shù)據(jù)庫(kù)中獲取最新的數(shù)據(jù),其他線程這時(shí)看到延長(zhǎng)了的過期時(shí)間,就會(huì)繼續(xù)使用舊數(shù)據(jù),等派遣的線程獲取最新數(shù)據(jù)后再更新緩存。
-
推薦使用互斥鎖,因?yàn)檐涍^期會(huì)有業(yè)務(wù)邏輯侵入和額外的判斷。
面試話術(shù):
緩存擊穿主要擔(dān)心的是某個(gè)Key過期,更新緩存時(shí)引起對(duì)數(shù)據(jù)庫(kù)的突發(fā)高并發(fā)訪問。因此我們可以在更新緩存時(shí)采用互斥鎖控制,只允許一個(gè)線程去更新緩存,其它線程等待并重新讀取緩存。例如Redis的setnx命令就能實(shí)現(xiàn)互斥效果。
3)緩存雪崩
相關(guān)資料:
緩存雪崩,是指在某一個(gè)時(shí)間段,緩存集中過期失效。對(duì)這批數(shù)據(jù)的訪問查詢,都落到了數(shù)據(jù)庫(kù)上,對(duì)于數(shù)據(jù)庫(kù)而言,就會(huì)產(chǎn)生周期性的壓力波峰。
解決方案:
-
數(shù)據(jù)分類分批處理:采取不同分類數(shù)據(jù),緩存不同周期
-
相同分類數(shù)據(jù):采用固定時(shí)長(zhǎng)加隨機(jī)數(shù)方式設(shè)置緩存
-
熱點(diǎn)數(shù)據(jù)緩存時(shí)間長(zhǎng)一些,冷門數(shù)據(jù)緩存時(shí)間短一些
-
避免redis節(jié)點(diǎn)宕機(jī)引起雪崩,搭建主從集群,保證高可用
面試話術(shù):
解決緩存雪崩問題的關(guān)鍵是讓緩存Key的過期時(shí)間分散。因此我們可以把數(shù)據(jù)按照業(yè)務(wù)分類,然后設(shè)置不同過期時(shí)間。相同業(yè)務(wù)類型的key,設(shè)置固定時(shí)長(zhǎng)加隨機(jī)數(shù)。盡可能保證每個(gè)Key的過期時(shí)間都不相同。
另外,Redis宕機(jī)也可能導(dǎo)致緩存雪崩,因此我們還要搭建Redis主從集群及哨兵監(jiān)控,保證Redis的高可用。
3.9.緩存冷熱數(shù)據(jù)分離
背景資料:
Redis使用的是內(nèi)存存儲(chǔ),當(dāng)需要海量數(shù)據(jù)存儲(chǔ)時(shí),成本非常高。
經(jīng)過調(diào)研發(fā)現(xiàn),當(dāng)前主流DDR3內(nèi)存和主流SATA SSD的單位成本價(jià)格差距大概在20倍左右,為了優(yōu)化redis機(jī)器綜合成本,我們考慮實(shí)現(xiàn)基于熱度統(tǒng)計(jì) 的數(shù)據(jù)分級(jí)存儲(chǔ)及數(shù)據(jù)在RAM/FLASH之間的動(dòng)態(tài)交換,從而大幅度降低成本,達(dá)到性能與成本的高平衡。
基本思路:基于key訪問次數(shù)(LFU)的熱度統(tǒng)計(jì)算法識(shí)別出熱點(diǎn)數(shù)據(jù),并將熱點(diǎn)數(shù)據(jù)保留在redis中,對(duì)于無訪問/訪問次數(shù)少的數(shù)據(jù)則轉(zhuǎn)存到SSD上,如果SSD上的key再次變熱,則重新將其加載到redis內(nèi)存中。
目前流行的高性能磁盤存儲(chǔ),并且遵循Redis協(xié)議的方案包括:
-
SSDB:SSDB - 高性能的支持豐富數(shù)據(jù)結(jié)構(gòu)的 NoSQL 數(shù)據(jù)庫(kù), 替代 Redis
-
RocksDB:RocksDB中文網(wǎng) | 一個(gè)持久型的key-value存儲(chǔ)
因此,我們就需要在應(yīng)用程序與緩存服務(wù)之間引入代理,實(shí)現(xiàn)Redis和SSD之間的切換,如圖:
?
3.10.Redis實(shí)現(xiàn)分布式鎖
分布式鎖要滿足的條件:
-
多進(jìn)程互斥:同一時(shí)刻,只有一個(gè)進(jìn)程可以獲取鎖
-
保證鎖可以釋放:任務(wù)結(jié)束或出現(xiàn)異常,鎖一定要釋放,避免死鎖
-
阻塞鎖(可選):獲取鎖失敗時(shí)可否重試
-
重入鎖(可選):獲取鎖的代碼遞歸調(diào)用時(shí),依然可以獲取鎖
1)最基本的分布式鎖:
利用Redis的setnx命令,這個(gè)命令的特征時(shí)如果多次執(zhí)行,只有第一次執(zhí)行會(huì)成功,可以實(shí)現(xiàn)互斥的效果。但是為了保證服務(wù)宕機(jī)時(shí)也可以釋放鎖,需要利用expire命令給鎖設(shè)置一個(gè)有效期
setnx lock thread-01 # 嘗試獲取鎖 expire lock 10 # 設(shè)置有效期面試官問題1:如果expire之前服務(wù)宕機(jī)怎么辦?
要保證setnx和expire命令的原子性。redis的set命令可以滿足:
set key value [NX] [EX time]需要添加nx和ex的選項(xiàng):
-
NX:與setnx一致,第一次執(zhí)行成功
-
EX:設(shè)置過期時(shí)間
面試官問題2:釋放鎖的時(shí)候,如果自己的鎖已經(jīng)過期了,此時(shí)會(huì)出現(xiàn)安全漏洞,如何解決?
在鎖中存儲(chǔ)當(dāng)前進(jìn)程和線程標(biāo)識(shí),釋放鎖時(shí)對(duì)鎖的標(biāo)識(shí)判斷,如果是自己的則刪除,不是則放棄操作。
但是這兩步操作要保證原子性,需要通過Lua腳本來實(shí)現(xiàn)。
if redis.call("get",KEYS[1]) == ARGV[1] thenredis.call("del",KEYS[1]) end2)可重入分布式鎖
如果有重入的需求,則除了在鎖中記錄進(jìn)程標(biāo)識(shí),還要記錄重試次數(shù),流程如下:
?
下面我們假設(shè)鎖的key為“l(fā)ock”,hashKey是當(dāng)前線程的id:“threadId”,鎖自動(dòng)釋放時(shí)間假設(shè)為20
獲取鎖的步驟:
-
1、判斷l(xiāng)ock是否存在 EXISTS lock
-
存在,說明有人獲取鎖了,下面判斷是不是自己的鎖
-
判斷當(dāng)前線程id作為hashKey是否存在:HEXISTS lock threadId
-
不存在,說明鎖已經(jīng)有了,且不是自己獲取的,鎖獲取失敗,end
-
存在,說明是自己獲取的鎖,重入次數(shù)+1:HINCRBY lock threadId 1,去到步驟3
-
-
-
2、不存在,說明可以獲取鎖,HSET key threadId 1
-
3、設(shè)置鎖自動(dòng)釋放時(shí)間,EXPIRE lock 20
-
釋放鎖的步驟:
-
1、判斷當(dāng)前線程id作為hashKey是否存在:HEXISTS lock threadId
-
不存在,說明鎖已經(jīng)失效,不用管了
-
存在,說明鎖還在,重入次數(shù)減1:HINCRBY lock threadId -1,獲取新的重入次數(shù)
-
-
2、判斷重入次數(shù)是否為0:
-
為0,說明鎖全部釋放,刪除key:DEL lock
-
大于0,說明鎖還在使用,重置有效時(shí)間:EXPIRE lock 20
-
對(duì)應(yīng)的Lua腳本如下:
首先是獲取鎖:
local key = KEYS[1]; -- 鎖的key local threadId = ARGV[1]; -- 線程唯一標(biāo)識(shí) local releaseTime = ARGV[2]; -- 鎖的自動(dòng)釋放時(shí)間if(redis.call('exists', key) == 0) then -- 判斷是否存在redis.call('hset', key, threadId, '1'); -- 不存在, 獲取鎖redis.call('expire', key, releaseTime); -- 設(shè)置有效期return 1; -- 返回結(jié)果 end;if(redis.call('hexists', key, threadId) == 1) then -- 鎖已經(jīng)存在,判斷threadId是否是自己 redis.call('hincrby', key, threadId, '1'); -- 不存在, 獲取鎖,重入次數(shù)+1redis.call('expire', key, releaseTime); -- 設(shè)置有效期return 1; -- 返回結(jié)果 end; return 0; -- 代碼走到這里,說明獲取鎖的不是自己,獲取鎖失敗然后是釋放鎖:
local key = KEYS[1]; -- 鎖的key local threadId = ARGV[1]; -- 線程唯一標(biāo)識(shí) local releaseTime = ARGV[2]; -- 鎖的自動(dòng)釋放時(shí)間if (redis.call('HEXISTS', key, threadId) == 0) then -- 判斷當(dāng)前鎖是否還是被自己持有return nil; -- 如果已經(jīng)不是自己,則直接返回 end; local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的鎖,則重入次數(shù)-1if (count > 0) then -- 判斷是否重入次數(shù)是否已經(jīng)為0redis.call('EXPIRE', key, releaseTime); -- 大于0說明不能釋放鎖,重置有效期然后返回return nil; elseredis.call('DEL', key); -- 等于0說明可以釋放鎖,直接刪除return nil; end;3)高可用的鎖
面試官問題:redis分布式鎖依賴與redis,如果redis宕機(jī)則鎖失效。如何解決?
此時(shí)大多數(shù)同學(xué)會(huì)回答說:搭建主從集群,做數(shù)據(jù)備份。
這樣就進(jìn)入了陷阱,因?yàn)槊嬖嚬俚南乱粋€(gè)問題就來了:
面試官問題:如果搭建主從集群做數(shù)據(jù)備份時(shí),進(jìn)程A獲取鎖,master還沒有把數(shù)據(jù)備份到slave,master宕機(jī),slave升級(jí)為master,此時(shí)原來鎖失效,其它進(jìn)程也可以獲取鎖,出現(xiàn)安全問題。如何解決?
關(guān)于這個(gè)問題,Redis官網(wǎng)給出了解決方案,使用RedLock思路可以解決:
在Redis的分布式環(huán)境中,我們假設(shè)有N個(gè)Redis master。這些節(jié)點(diǎn)完全互相獨(dú)立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制。之前我們已經(jīng)描述了在Redis單實(shí)例下怎么安全地獲取和釋放鎖。我們確保將在每(N)個(gè)實(shí)例上使用此方法獲取和釋放鎖。在這個(gè)樣例中,我們假設(shè)有5個(gè)Redis master節(jié)點(diǎn),這是一個(gè)比較合理的設(shè)置,所以我們需要在5臺(tái)機(jī)器上面或者5臺(tái)虛擬機(jī)上面運(yùn)行這些實(shí)例,這樣保證他們不會(huì)同時(shí)都宕掉。
為了取到鎖,客戶端應(yīng)該執(zhí)行以下操作:
獲取當(dāng)前Unix時(shí)間,以毫秒為單位。
依次嘗試從N個(gè)實(shí)例,使用相同的key和隨機(jī)值獲取鎖。在步驟2,當(dāng)向Redis設(shè)置鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下,客戶端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試另外一個(gè)Redis實(shí)例。
客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
如果取到了鎖,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。
如果因?yàn)槟承┰?#xff0c;獲取鎖失敗(沒有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒有加鎖成功)。
3.11.如何實(shí)現(xiàn)數(shù)據(jù)庫(kù)與緩存數(shù)據(jù)一致?
面試話術(shù):
實(shí)現(xiàn)方案有下面幾種:
-
本地緩存同步:當(dāng)前微服務(wù)的數(shù)據(jù)庫(kù)數(shù)據(jù)與緩存數(shù)據(jù)同步,可以直接在數(shù)據(jù)庫(kù)修改時(shí)加入對(duì)Redis的修改邏輯,保證一致。
-
跨服務(wù)緩存同步:服務(wù)A調(diào)用了服務(wù)B,并對(duì)查詢結(jié)果緩存。服務(wù)B數(shù)據(jù)庫(kù)修改,可以通過MQ通知服務(wù)A,服務(wù)A修改Redis緩存數(shù)據(jù)
-
通用方案:使用Canal框架,偽裝成MySQL的salve節(jié)點(diǎn),監(jiān)聽MySQL的binLog變化,然后修改Redis緩存數(shù)據(jù)
總結(jié)
以上是生活随笔為你收集整理的b站黑马springCloud-常见面试题,多多三连的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 京东线报接口 全网一手线报全网(京东,淘
- 下一篇: 能打开QQ,但打开不了网页-网络热门故障