• 正文
    • Java
  • 相關(guān)推薦
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

哈啰出行薪資開(kāi)了,要不要去?

02/07 13:15
2567
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

圖解學(xué)習(xí)網(wǎng)站:https://xiaolincoding.com

大家好,我是小林。

哈啰出行 25 屆開(kāi)發(fā)崗的校招薪資如下,年薪在 25-35w 之間,屬于互聯(lián)網(wǎng)中廠級(jí)別的薪資范圍:

18k x 14.5 = 26.1w,同學(xué)背景本科雙一流,辦公地點(diǎn)上海

21k x 14.5 = 30w,同學(xué)背景碩士,辦公地點(diǎn)杭州

24k x 14.5 = 34.8w,同學(xué)背景本科 985,辦公地點(diǎn)上海

哈啰出行的公積金是按 7%繳納,訓(xùn)練營(yíng)也有同學(xué)拿到哈啰 offer,開(kāi)的是 21k,手上還有一個(gè)小廠 25k 的,但是考慮到哈啰出行的平臺(tái)更大一些,選擇了去了哈啰出行。

那哈啰的面試難度如何呢?

之前有個(gè)社招同學(xué)面哈啰出行反饋說(shuō),面試難度不算難,問(wèn)的都是偏基礎(chǔ)的內(nèi)容,但是最后有算法題,平時(shí)算法練的少,最后沒(méi)做出來(lái),比較可惜。

這次帶大家來(lái)看看,哈啰出行 Java 后端開(kāi)發(fā)校招面經(jīng),面試難度跟互聯(lián)網(wǎng)大廠差距不算很大,這次的面經(jīng)面了 50 分鐘,技術(shù)方面從Java、MySQL、Redis、中間件、網(wǎng)絡(luò)進(jìn)行的拷打,最后還有一個(gè)算法手撕環(huán)節(jié)。

大家覺(jué)得哈啰出行面試難度如何?

Java

JVM內(nèi)存區(qū)域介紹一下?

根據(jù) JDK 8 規(guī)范,JVM 運(yùn)行時(shí)內(nèi)存共分為虛擬機(jī)棧、堆、元空間、程序計(jì)數(shù)器、本地方法棧五個(gè)部分。還有一部分內(nèi)存叫直接內(nèi)存,屬于操作系統(tǒng)的本地內(nèi)存,也是可以直接操作的。

JVM的內(nèi)存結(jié)構(gòu)主要分為以下幾個(gè)部分:

元空間:元空間的本質(zhì)和永久代類似,都是對(duì) JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。

Java 虛擬機(jī)棧:每個(gè)線程有一個(gè)私有的棧,隨著線程的創(chuàng)建而創(chuàng)建。棧里面存著的是一種叫 “棧幀” 的東西,每個(gè)方法會(huì)創(chuàng)建一個(gè)棧幀,棧幀中存放了局部變量表(基本數(shù)據(jù)類型和對(duì)象引用)、操作數(shù)棧、方法出口等信息。棧的大小可以固定也可以動(dòng)態(tài)擴(kuò)展。

本地方法棧:與虛擬機(jī)棧類似,區(qū)別是虛擬機(jī)棧執(zhí)行 Java 方法,本地方法棧執(zhí)行 native 方法。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言、使用方法與數(shù)據(jù)結(jié)構(gòu)沒(méi)有強(qiáng)制規(guī)定,因此虛擬機(jī)可以自由實(shí)現(xiàn)它。

程序計(jì)數(shù)器:程序計(jì)數(shù)器可以看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多內(nèi)核來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,我們稱這類內(nèi)存區(qū)域?yàn)?“線程私有” 內(nèi)存。

堆內(nèi)存:堆內(nèi)存是 JVM 所有線程共享的部分,在虛擬機(jī)啟動(dòng)的時(shí)候就已經(jīng)創(chuàng)建。所有的對(duì)象實(shí)例和數(shù)組都在堆上分配,這部分空間可通過(guò) GC 進(jìn)行回收。當(dāng)申請(qǐng)不到空間時(shí)會(huì)拋出OutOfMemoryError。堆是 JVM 內(nèi)存占用最大、管理最復(fù)雜的一個(gè)區(qū)域。JDK 1.8 后,字符串常量池和運(yùn)行時(shí)常量池從永久代中剝離出來(lái),存放在堆中。

直接內(nèi)存:直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。在 JDK 1.4 中新加入了 NIO 類,引入了一種基于通道 (Channel) 與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用 native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在 Java 堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。

垃圾回收算法和機(jī)制介紹一下

標(biāo)記-清除算法:標(biāo)記-清除算法分為“標(biāo)記”和“清除”兩個(gè)階段,首先通過(guò)可達(dá)性分析,標(biāo)記出所有需要回收的對(duì)象,然后統(tǒng)一回收所有被標(biāo)記的對(duì)象。標(biāo)記-清除算法有兩個(gè)缺陷,一個(gè)是效率問(wèn)題,標(biāo)記和清除的過(guò)程效率都不高,另外一個(gè)就是,清除結(jié)束后會(huì)造成大量的碎片空間。有可能會(huì)造成在申請(qǐng)大塊內(nèi)存的時(shí)候因?yàn)闆](méi)有足夠的連續(xù)空間導(dǎo)致再次 GC。

復(fù)制算法:為了解決碎片空間的問(wèn)題,出現(xiàn)了“復(fù)制算法”。復(fù)制算法的原理是,將內(nèi)存分成兩塊,每次申請(qǐng)內(nèi)存時(shí)都使用其中的一塊,當(dāng)內(nèi)存不夠時(shí),將這一塊內(nèi)存中所有存活的復(fù)制到另一塊上。然后將然后再把已使用的內(nèi)存整個(gè)清理掉。復(fù)制算法解決了空間碎片的問(wèn)題。但是也帶來(lái)了新的問(wèn)題。因?yàn)槊看卧谏暾?qǐng)內(nèi)存時(shí),都只能使用一半的內(nèi)存空間。內(nèi)存利用率嚴(yán)重不足。

標(biāo)記-整理算法:復(fù)制算法在 GC 之后存活對(duì)象較少的情況下效率比較高,但如果存活對(duì)象比較多時(shí),會(huì)執(zhí)行較多的復(fù)制操作,效率就會(huì)下降。而老年代的對(duì)象在 GC 之后的存活率就比較高,所以就有人提出了“標(biāo)記-整理算法”。標(biāo)記-整理算法的“標(biāo)記”過(guò)程與“標(biāo)記-清除算法”的標(biāo)記過(guò)程一致,但標(biāo)記之后不會(huì)直接清理。而是將所有存活對(duì)象都移動(dòng)到內(nèi)存的一端。移動(dòng)結(jié)束后直接清理掉剩余部分。

分代回收算法:分代收集是將內(nèi)存劃分成了新生代和老年代。分配的依據(jù)是對(duì)象的生存周期,或者說(shuō)經(jīng)歷過(guò)的 GC 次數(shù)。對(duì)象創(chuàng)建時(shí),一般在新生代申請(qǐng)內(nèi)存,當(dāng)經(jīng)歷一次 GC 之后如果對(duì)還存活,那么對(duì)象的年齡 +1。當(dāng)年齡超過(guò)一定值(默認(rèn)是 15,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold來(lái)設(shè)定)后,如果對(duì)象還存活,那么該對(duì)象會(huì)進(jìn)入老年代。

新生代和老年代的區(qū)別是什么?

在基于分代收集算法的垃圾回收機(jī)制里,Java 堆內(nèi)存被劃分為新生代和老年代,它們?cè)趯?duì)象特性、內(nèi)存大小、垃圾回收算法、回收頻率等方面存在明顯區(qū)別:

對(duì)象特性的區(qū)別

新生代:大多數(shù)新創(chuàng)建的對(duì)象會(huì)被分配到新生代。這些對(duì)象的生命周期通常較短,很多對(duì)象在創(chuàng)建后不久就不再被使用,成為垃圾對(duì)象等待回收。例如在一個(gè) Web 應(yīng)用中,每次處理請(qǐng)求時(shí)創(chuàng)建的臨時(shí)對(duì)象、方法調(diào)用時(shí)創(chuàng)建的局部變量對(duì)象等,大多都存于新生代。

老年代:存儲(chǔ)的是經(jīng)過(guò)多次垃圾回收仍然存活的對(duì)象。這些對(duì)象的生命周期較長(zhǎng),可能會(huì)在整個(gè)應(yīng)用的運(yùn)行過(guò)程中一直存在。比如數(shù)據(jù)庫(kù)連接池對(duì)象、緩存對(duì)象等,它們會(huì)在系統(tǒng)中長(zhǎng)期駐留,最終會(huì)被轉(zhuǎn)移到老年代。

垃圾回收算法的區(qū)別

新生代:主要使用復(fù)制算法進(jìn)行垃圾回收。將新生代內(nèi)存分為一個(gè)較大的 Eden 區(qū)和兩個(gè)較小的 Survivor 區(qū)(通常比例為 8:1:1 )。新對(duì)象優(yōu)先分配在 Eden 區(qū),當(dāng) Eden 區(qū)滿時(shí),會(huì)觸發(fā) Minor GC(新生代垃圾回收),將 Eden 區(qū)和一個(gè) Survivor 區(qū)中存活的對(duì)象復(fù)制到另一個(gè) Survivor 區(qū),然后清空 Eden 區(qū)和之前使用的 Survivor 區(qū)。經(jīng)過(guò)多次 Minor GC 后仍然存活的對(duì)象會(huì)被晉升到老年代。

老年代:一般采用標(biāo)記 - 清除算法或者標(biāo)記 - 整理算法。標(biāo)記 - 清除算法先標(biāo)記出存活對(duì)象,然后清除未標(biāo)記的對(duì)象,但會(huì)產(chǎn)生內(nèi)存碎片;標(biāo)記 - 整理算法在標(biāo)記存活對(duì)象后,將存活對(duì)象移動(dòng)到一端,然后清理掉邊界以外的內(nèi)存,避免了內(nèi)存碎片問(wèn)題,但移動(dòng)對(duì)象會(huì)帶來(lái)一定的性能開(kāi)銷。老年代的垃圾回收稱為 Major GC 或 Full GC,通常比 Minor GC 耗時(shí)更長(zhǎng)。

垃圾回收頻率的區(qū)別

新生代:由于新生代中的對(duì)象大多生命周期較短,很快就會(huì)成為垃圾對(duì)象,所以新生代的垃圾回收(Minor GC)頻率較高。Minor GC 的速度相對(duì)較快,因?yàn)樾枰獜?fù)制的存活對(duì)象較少。

老年代:老年代中的對(duì)象生命周期長(zhǎng),垃圾對(duì)象相對(duì)較少,因此老年代的垃圾回收(Major GC 或 Full GC)頻率較低。但一旦觸發(fā)老年代的垃圾回收,由于老年代內(nèi)存空間大、對(duì)象數(shù)量多,回收過(guò)程會(huì)比較耗時(shí),對(duì)系統(tǒng)性能的影響也更大。

常見(jiàn)的線程池類型及其好處說(shuō)一下?

ScheduledThreadPool:可以設(shè)置定期的執(zhí)行任務(wù),它支持定時(shí)或周期性執(zhí)行任務(wù),比如每隔 10 秒鐘執(zhí)行一次任務(wù),我通過(guò)這個(gè)實(shí)現(xiàn)類設(shè)置定期執(zhí)行任務(wù)的策略。

FixedThreadPool:它的核心線程數(shù)和最大線程數(shù)是一樣的,所以可以把它看作是固定線程數(shù)的線程池,它的特點(diǎn)是線程池中的線程數(shù)除了初始階段需要從 0 開(kāi)始增加外,之后的線程數(shù)量就是固定的,就算任務(wù)數(shù)超過(guò)線程數(shù),線程池也不會(huì)再創(chuàng)建更多的線程來(lái)處理任務(wù),而是會(huì)把超出線程處理能力的任務(wù)放到任務(wù)隊(duì)列中進(jìn)行等待。而且就算任務(wù)隊(duì)列滿了,到了本該繼續(xù)增加線程數(shù)的時(shí)候,由于它的最大線程數(shù)和核心線程數(shù)是一樣的,所以也無(wú)法再增加新的線程了。

CachedThreadPool:可以稱作可緩存線程池,它的特點(diǎn)在于線程數(shù)是幾乎可以無(wú)限增加的(實(shí)際最大可以達(dá)到Integer.MAX_VALUE,為 2^31-1,這個(gè)數(shù)非常大,所以基本不可能達(dá)到),而當(dāng)線程閑置時(shí)還可以對(duì)線程進(jìn)行回收。也就是說(shuō)該線程池的線程數(shù)量不是固定不變的,當(dāng)然它也有一個(gè)用于存儲(chǔ)提交任務(wù)的隊(duì)列,但這個(gè)隊(duì)列是 SynchronousQueue,隊(duì)列的容量為0,實(shí)際不存儲(chǔ)任何任務(wù),它只負(fù)責(zé)對(duì)任務(wù)進(jìn)行中轉(zhuǎn)和傳遞,所以效率比較高。

SingleThreadExecutor:它會(huì)使用唯一的線程去執(zhí)行任務(wù),原理和 FixedThreadPool 是一樣的,只不過(guò)這里線程只有一個(gè),如果線程在執(zhí)行任務(wù)的過(guò)程中發(fā)生異常,線程池也會(huì)重新創(chuàng)建一個(gè)線程來(lái)執(zhí)行后續(xù)的任務(wù)。這種線程池由于只有一個(gè)線程,所以非常適合用于所有任務(wù)都需要按被提交的順序依次執(zhí)行的場(chǎng)景,而前幾種線程池不一定能夠保障任務(wù)的執(zhí)行順序等于被提交的順序,因?yàn)樗鼈兪?a class="article-link" target="_blank" href="/baike/1545792.html">多線程并行執(zhí)行的。

SingleThreadScheduledExecutor:它實(shí)際和 ScheduledThreadPool 線程池非常相似,它只是 ScheduledThreadPool 的一個(gè)特例,內(nèi)部只有一個(gè)線程。

IOC和AOP的概念說(shuō)一下?

Spring IoC和AOP 區(qū)別:

IoC:即控制反轉(zhuǎn)的意思,它是一種創(chuàng)建和獲取對(duì)象的技術(shù)思想,依賴注入(DI)是實(shí)現(xiàn)這種技術(shù)的一種方式。傳統(tǒng)開(kāi)發(fā)過(guò)程中,我們需要通過(guò)new關(guān)鍵字來(lái)創(chuàng)建對(duì)象。使用IoC思想開(kāi)發(fā)方式的話,我們不通過(guò)new關(guān)鍵字創(chuàng)建對(duì)象,而是通過(guò)IoC容器來(lái)幫我們實(shí)例化對(duì)象。通過(guò)IoC的方式,可以大大降低對(duì)象之間的耦合度。

AOP:是面向切面編程,能夠?qū)⒛切┡c業(yè)務(wù)無(wú)關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯封裝起來(lái),以減少系統(tǒng)的重復(fù)代碼,降低模塊間的耦合度。Spring AOP 就是基于動(dòng)態(tài)代理的,如果要代理的對(duì)象,實(shí)現(xiàn)了某個(gè)接口,那么 Spring AOP 會(huì)使用 JDK Proxy,去創(chuàng)建代理對(duì)象,而對(duì)于沒(méi)有實(shí)現(xiàn)接口的對(duì)象,就無(wú)法使用 JDK Proxy 去進(jìn)行代理了,這時(shí)候 Spring AOP 會(huì)使用 Cglib 生成一個(gè)被代理對(duì)象的子類來(lái)作為代理。

在 Spring 框架中,IOC 和 AOP 結(jié)合使用,可以更好地實(shí)現(xiàn)代碼的模塊化和分層管理。例如:

    通過(guò) IOC 容器管理對(duì)象的依賴關(guān)系,然后通過(guò) AOP 將橫切關(guān)注點(diǎn)統(tǒng)一切入到需要的業(yè)務(wù)邏輯中。使用 IOC 容器管理 Service 層和 DAO 層的依賴關(guān)系,然后通過(guò) AOP 在 Service 層實(shí)現(xiàn)事務(wù)管理、日志記錄等橫切功能,使得業(yè)務(wù)邏輯更加清晰和可維護(hù)。

是否使用過(guò)AOP,具體怎么用的?

我在實(shí)際項(xiàng)目里使用過(guò) AOP,它在處理日志記錄、事務(wù)管理這類橫切關(guān)注點(diǎn)時(shí)非常實(shí)用。使用的步驟如下:

添加依賴:如果是 Maven 項(xiàng)目,要在pom.xml里添加 Spring AOP 和 AspectJ 的依賴,讓項(xiàng)目能支持 AOP 功能。

定義切面類:創(chuàng)建一個(gè)類,用@Aspect注解標(biāo)記它為切面,用@Component注解把它交給 Spring 容器管理。在類中,通過(guò)@Pointcut定義切入點(diǎn),比如execution(* com.example.service.*.*(..))可以匹配指定包下所有類的所有方法;再用@Before、@After等注解定義通知,這些通知會(huì)在切入點(diǎn)方法執(zhí)行前后執(zhí)行相應(yīng)邏輯。

@Aspect
@Component
public?class?LoggingAspect?{
????@Pointcut("execution(*?com.example.service.*.*(..))")
????public?void?serviceMethods()?{}

????@Before("serviceMethods()")
????public?void?beforeAdvice(JoinPoint?joinPoint)?{
????????System.out.println("Before?method:?"?+?joinPoint.getSignature().getName());
????}

????@After("serviceMethods()")
????public?void?afterAdvice(JoinPoint?joinPoint)?{
????????System.out.println("After?method:?"?+?joinPoint.getSignature().getName());
????}
}

啟用 AOP 自動(dòng)代理:在 Spring Boot 項(xiàng)目中默認(rèn)已啟用;傳統(tǒng) Spring 項(xiàng)目可在配置文件加<aop:aspectj-autoproxy/>,或者用 Java 配置類加@EnableAspectJAutoProxy注解。

編寫業(yè)務(wù)邏輯類:創(chuàng)建正常的業(yè)務(wù)類和方法,這些方法會(huì)被 AOP 增強(qiáng)。

@Service
public?class?UserService?{
????public?void?createUser(String?username)?{
????????System.out.println("Creating?user:?"?+?username);
????}
}

MySQL

MySQL InnoDB的數(shù)據(jù)結(jié)構(gòu)是什么?

MySQL InnoDB 引擎是用了B+樹(shù)作為了索引的數(shù)據(jù)結(jié)構(gòu)。

B+Tree 是一種多叉樹(shù),葉子節(jié)點(diǎn)才存放數(shù)據(jù),非葉子節(jié)點(diǎn)只存放索引,而且每個(gè)節(jié)點(diǎn)里的數(shù)據(jù)是按主鍵順序存放的。每一層父節(jié)點(diǎn)的索引值都會(huì)出現(xiàn)在下層子節(jié)點(diǎn)的索引值中,因此在葉子節(jié)點(diǎn)中,包括了所有的索引值信息,并且每一個(gè)葉子節(jié)點(diǎn)都有兩個(gè)指針,分別指向下一個(gè)葉子節(jié)點(diǎn)和上一個(gè)葉子節(jié)點(diǎn),形成一個(gè)雙向鏈表。

主鍵索引的 B+Tree 如圖所示:

比如,我們執(zhí)行了下面這條查詢語(yǔ)句:

select?*?from?product?where?id=?5;

這條語(yǔ)句使用了主鍵索引查詢 id 號(hào)為 5 的商品。查詢過(guò)程是這樣的,B+Tree 會(huì)自頂向下逐層進(jìn)行查找:

    將 5 與根節(jié)點(diǎn)的索引數(shù)據(jù) (1,10,20) 比較,5 在 1 和 10 之間,所以根據(jù) B+Tree的搜索邏輯,找到第二層的索引數(shù)據(jù) (1,4,7);在第二層的索引數(shù)據(jù) (1,4,7)中進(jìn)行查找,因?yàn)?5 在 4 和 7 之間,所以找到第三層的索引數(shù)據(jù)(4,5,6);在葉子節(jié)點(diǎn)的索引數(shù)據(jù)(4,5,6)中進(jìn)行查找,然后我們找到了索引值為 5 的行數(shù)據(jù)。

數(shù)據(jù)庫(kù)的索引和數(shù)據(jù)都是存儲(chǔ)在硬盤的,我們可以把讀取一個(gè)節(jié)點(diǎn)當(dāng)作一次磁盤 I/O 操作。那么上面的整個(gè)查詢過(guò)程一共經(jīng)歷了 3 個(gè)節(jié)點(diǎn),也就是進(jìn)行了 3 次 I/O 操作。

B+Tree 存儲(chǔ)千萬(wàn)級(jí)的數(shù)據(jù)只需要 3-4 層高度就可以滿足,這意味著從千萬(wàn)級(jí)的表查詢目標(biāo)數(shù)據(jù)最多需要 3-4 次磁盤 I/O,所以B+Tree 相比于 B 樹(shù)和二叉樹(shù)來(lái)說(shuō),最大的優(yōu)勢(shì)在于查詢效率很高,因?yàn)榧词乖跀?shù)據(jù)量很大的情況,查詢一個(gè)數(shù)據(jù)的磁盤 I/O 依然維持在 3-4次。

表鎖、行鎖和讀寫鎖的區(qū)別是什么?

在 MySQL 里,根據(jù)加鎖的范圍,可以分為全局鎖、表級(jí)鎖和行鎖三類。

全局鎖:通過(guò)flush tables with read lock 語(yǔ)句會(huì)將整個(gè)數(shù)據(jù)庫(kù)就處于只讀狀態(tài)了,這時(shí)其他線程執(zhí)行以下操作,增刪改或者表結(jié)構(gòu)修改都會(huì)阻塞。全局鎖主要應(yīng)用于做全庫(kù)邏輯備份,這樣在備份數(shù)據(jù)庫(kù)期間,不會(huì)因?yàn)閿?shù)據(jù)或表結(jié)構(gòu)的更新,而出現(xiàn)備份文件的數(shù)據(jù)與預(yù)期的不一樣。

表級(jí)鎖:MySQL 里面表級(jí)別的鎖有這幾種:

表鎖:通過(guò)lock tables 語(yǔ)句可以對(duì)表加表鎖,表鎖除了會(huì)限制別的線程的讀寫外,也會(huì)限制本線程接下來(lái)的讀寫操作。

元數(shù)據(jù)鎖:當(dāng)我們對(duì)數(shù)據(jù)庫(kù)表進(jìn)行操作時(shí),會(huì)自動(dòng)給這個(gè)表加上 MDL,對(duì)一張表進(jìn)行 CRUD 操作時(shí),加的是 MDL 讀鎖;對(duì)一張表做結(jié)構(gòu)變更操作的時(shí)候,加的是 MDL 寫鎖;MDL 是為了保證當(dāng)用戶對(duì)表執(zhí)行 CRUD 操作時(shí),防止其他線程對(duì)這個(gè)表結(jié)構(gòu)做了變更。

意向鎖:當(dāng)執(zhí)行插入、更新、刪除操作,需要先對(duì)表加上「意向獨(dú)占鎖」,然后對(duì)該記錄加獨(dú)占鎖。意向鎖的目的是為了快速判斷表里是否有記錄被加鎖。

行級(jí)鎖:InnoDB 引擎是支持行級(jí)鎖的,而 MyISAM 引擎并不支持行級(jí)鎖。

    • 記錄鎖,鎖住的是一條記錄。而且記錄鎖是有 S 鎖和 X 鎖之分的,滿足讀寫互斥,寫寫互斥間隙鎖,只存在于可重復(fù)讀隔離級(jí)別,目的是為了解決可重復(fù)讀隔離級(jí)別下幻讀的現(xiàn)象。Next-Key Lock 稱為臨鍵鎖,是 Record Lock + Gap Lock 的組合,鎖定一個(gè)范圍,并且鎖定記錄本身。

InnoDB和MyISAM的區(qū)別?

事務(wù):InnoDB 支持事務(wù),MyISAM 不支持事務(wù),這是 MySQL 將默認(rèn)存儲(chǔ)引擎從 MyISAM 變成 InnoDB 的重要原因之一。

索引結(jié)構(gòu):InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主鍵索引的葉子節(jié)點(diǎn)上,因此 InnoDB 必須要有主鍵,通過(guò)主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然后再通過(guò)主鍵查詢到數(shù)據(jù)。因此,主鍵不應(yīng)該過(guò)大,因?yàn)橹麈I太大,其他索引也都會(huì)很大。而 MyISAM 是非聚簇索引,數(shù)據(jù)文件是分離的,索引保存的是數(shù)據(jù)文件的指針。主鍵索引和輔助索引是獨(dú)立的。

鎖粒度:InnoDB 最小的鎖粒度是行鎖,MyISAM 最小的鎖粒度是表鎖。一個(gè)更新語(yǔ)句會(huì)鎖住整張表,導(dǎo)致其他查詢和更新都會(huì)被阻塞,因此并發(fā)訪問(wèn)受限。

count 的效率:InnoDB 不保存表的具體行數(shù),執(zhí)行 select count(*) from table 時(shí)需要全表掃描。而MyISAM 用一個(gè)變量保存了整個(gè)表的行數(shù),執(zhí)行上述語(yǔ)句時(shí)只需要讀出該變量即可,速度很快。

如何判斷一個(gè)SQL語(yǔ)句是否是慢SQL?

可以通過(guò)開(kāi)啟慢查詢?nèi)罩荆热绨崖樵兊臅r(shí)間閾值設(shè)置為 2 秒,那么執(zhí)行 sql 耗時(shí)超過(guò) 2 秒的 sql 就會(huì)被記錄到慢查詢?nèi)罩荆覀兙涂梢圆榭绰樵內(nèi)罩?,?lái)找到耗時(shí)超過(guò) 2 秒的慢 sql。

然后再通過(guò) explian 執(zhí)行計(jì)劃來(lái)分析慢 sql,重點(diǎn)關(guān)注執(zhí)行計(jì)劃中的以下信息:

img

索引使用情況:如果執(zhí)行計(jì)劃中沒(méi)有使用索引或者使用了不合適的索引,可能會(huì)導(dǎo)致 SQL 語(yǔ)句執(zhí)行緩慢。

掃描行數(shù):掃描行數(shù)過(guò)多會(huì)增加查詢的時(shí)間成本,如果執(zhí)行計(jì)劃中顯示掃描了大量的行數(shù),需要考慮優(yōu)化查詢條件或添加合適的索引。

連接類型:不同的連接類型對(duì)性能的影響不同,例如全表掃描的連接類型性能較差,應(yīng)盡量避免。

Redis

Redis常用的數(shù)據(jù)結(jié)構(gòu)有哪些?

Redis 提供了豐富的數(shù)據(jù)類型,常見(jiàn)的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

隨著 Redis 版本的更新,后面又支持了四種數(shù)據(jù)類型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五種數(shù)據(jù)類型的應(yīng)用場(chǎng)景:

    String 類型的應(yīng)用場(chǎng)景:緩存對(duì)象、常規(guī)計(jì)數(shù)、分布式鎖、共享 session 信息等。List 類型的應(yīng)用場(chǎng)景:消息隊(duì)列(但是有兩個(gè)問(wèn)題:1. 生產(chǎn)者需要自行實(shí)現(xiàn)全局唯一 ID;2. 不能以消費(fèi)組形式消費(fèi)數(shù)據(jù))等。Hash 類型:緩存對(duì)象、購(gòu)物車等。Set 類型:聚合計(jì)算(并集、交集、差集)場(chǎng)景,比如點(diǎn)贊、共同關(guān)注、抽獎(jiǎng)活動(dòng)等。Zset 類型:排序場(chǎng)景,比如排行榜、電話和姓名排序等。

Redis 后續(xù)版本又支持四種數(shù)據(jù)類型,它們的應(yīng)用場(chǎng)景如下:

    BitMap(2.2 版新增):二值狀態(tài)統(tǒng)計(jì)的場(chǎng)景,比如簽到、判斷用戶登陸狀態(tài)、連續(xù)簽到用戶總數(shù)等;HyperLogLog(2.8 版新增):海量數(shù)據(jù)基數(shù)統(tǒng)計(jì)的場(chǎng)景,比如百萬(wàn)級(jí)網(wǎng)頁(yè) UV 計(jì)數(shù)等;GEO(3.2 版新增):存儲(chǔ)地理位置信息的場(chǎng)景,比如滴滴叫車;Stream(5.0 版新增):消息隊(duì)列,相比于基于 List 類型實(shí)現(xiàn)的消息隊(duì)列,有這兩個(gè)特有的特性:自動(dòng)生成全局唯一消息ID,支持以消費(fèi)組形式消費(fèi)數(shù)據(jù)。

如何使用Redis結(jié)合token實(shí)現(xiàn)用戶登錄?

實(shí)現(xiàn)步驟:

    用戶登錄驗(yàn)證:當(dāng)用戶提交登錄請(qǐng)求時(shí),服務(wù)器會(huì)對(duì)用戶輸入的用戶名和密碼進(jìn)行驗(yàn)證,檢查其是否與數(shù)據(jù)庫(kù)中存儲(chǔ)的信息匹配。生成 Token:如果用戶驗(yàn)證通過(guò),服務(wù)器會(huì)為該用戶生成一個(gè)唯一的 Token。Token 可以是一個(gè)隨機(jī)字符串,例如使用 UUID 生成,它將作為用戶在后續(xù)請(qǐng)求中的身份標(biāo)識(shí)。將 Token 存儲(chǔ)到 Redis:把生成的 Token 與用戶信息(如用戶 ID)關(guān)聯(lián)起來(lái),并存儲(chǔ)到 Redis 中,同時(shí)為 Token 設(shè)置一個(gè)過(guò)期時(shí)間,以確保登錄狀態(tài)的時(shí)效性。返回 Token 給客戶端。服務(wù)器將生成的 Token 返回給客戶端,客戶端在后續(xù)的請(qǐng)求中需要攜帶該 Token。驗(yàn)證 Token:客戶端在發(fā)送請(qǐng)求時(shí),會(huì)將 Token 包含在請(qǐng)求頭或請(qǐng)求參數(shù)中。服務(wù)器接收到請(qǐng)求后,會(huì)從 Redis 中查找該 Token 是否存在且未過(guò)期。若存在且未過(guò)期,則認(rèn)為用戶已登錄,允許其訪問(wèn)受保護(hù)的資源;否則,要求用戶重新登錄。

如何用分布式鎖保證每人只能領(lǐng)取一張優(yōu)惠券?

加鎖:在用戶嘗試領(lǐng)取優(yōu)惠券時(shí),以用戶 ID 作為鎖的鍵,使用 Redis 的SETNX命令嘗試獲取鎖。如果返回1,表示成功獲取鎖,用戶可以繼續(xù)進(jìn)行優(yōu)惠券領(lǐng)取操作;如果返回0,表示鎖已被其他線程持有,該用戶不能領(lǐng)取優(yōu)惠券。為了避免鎖被永久持有,需要為鎖設(shè)置一個(gè)過(guò)期時(shí)間,防止出現(xiàn)死鎖,可以在SETNX命令中加上 PX 選項(xiàng)。

// lock_key 就是 key 鍵;
// unique_value 是客戶端生成的唯一的標(biāo)識(shí),區(qū)分來(lái)自不同客戶端的鎖操作;
// NX 代表只在 lock_key 不存在時(shí),才對(duì) lock_key 進(jìn)行設(shè)置操作;
// PX 10000?表示設(shè)置 lock_key 的過(guò)期時(shí)間為 10s,這是為了避免客戶端發(fā)生異常而無(wú)法釋放鎖。
SET?lock_key?unique_value?NX?PX?10000

解鎖:在用戶完成優(yōu)惠券領(lǐng)取操作后,需要釋放鎖??梢允褂?code>DEL

    命令刪除鎖的鍵,但不能亂刪,要保證執(zhí)行操作的客戶端就是加鎖的客戶端。所以,解鎖的時(shí)候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。可以看到,解鎖是有兩個(gè)操作,這時(shí)就需要 Lua 腳本來(lái)保證解鎖的原子性,因?yàn)?Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,保證了鎖釋放操作的原子性,這樣一來(lái),就通過(guò)使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上完成了分布式鎖的加鎖和解鎖。
//?釋放鎖時(shí),先比較?unique_value?是否相等,避免鎖的誤釋放
if?redis.call("get",KEYS[1])?==?ARGV[1]?then
??return?redis.call("del",KEYS[1])
else
??return?0
end

中間件

RabbitMQ的特性你知道哪些?

RabbitMQ 以 可靠性、靈活性易擴(kuò)展性 為核心優(yōu)勢(shì),適合需要穩(wěn)定消息傳遞的復(fù)雜系統(tǒng)。其豐富的插件和協(xié)議支持使其在微服務(wù)、IoT、金融等領(lǐng)域廣泛應(yīng)用,比較核心的特性有如下:

持久化機(jī)制:RabbitMQ 支持消息、隊(duì)列和交換器的持久化。當(dāng)啟用持久化時(shí),消息會(huì)被寫入磁盤,即使 RabbitMQ 服務(wù)器重啟,消息也不會(huì)丟失。例如,在聲明隊(duì)列時(shí)可以設(shè)置durable參數(shù)為true來(lái)實(shí)現(xiàn)隊(duì)列的持久化:

import?pika

connection?=?pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel?=?connection.channel()

#?聲明一個(gè)持久化隊(duì)列
channel.queue_declare(queue='durable_queue',?durable=True)

消息確認(rèn)機(jī)制:提供了生產(chǎn)者確認(rèn)和消費(fèi)者確認(rèn)機(jī)制。生產(chǎn)者可以設(shè)置confirm模式,當(dāng)消息成功到達(dá) RabbitMQ 服務(wù)器時(shí),會(huì)收到確認(rèn)消息;消費(fèi)者在處理完消息后,可以向 RabbitMQ 發(fā)送確認(rèn)信號(hào),告知服務(wù)器該消息已被成功處理,服務(wù)器才會(huì)將消息從隊(duì)列中刪除。

鏡像隊(duì)列:支持創(chuàng)建鏡像隊(duì)列,將隊(duì)列的內(nèi)容復(fù)制到多個(gè)節(jié)點(diǎn)上,提高消息的可用性和可靠性。當(dāng)一個(gè)節(jié)點(diǎn)出現(xiàn)故障時(shí),其他節(jié)點(diǎn)仍然可以提供服務(wù),確保消息不會(huì)丟失。

多種交換器類型:RabbitMQ 提供了多種類型的交換器,如直連交換器(Direct Exchange)、扇形交換器(Fanout Exchange)、主題交換器(Topic Exchange)和頭部交換器(Headers Exchange)。不同類型的交換器根據(jù)不同的規(guī)則將消息路由到隊(duì)列中。例如,扇形交換器會(huì)將接收到的消息廣播到所有綁定的隊(duì)列中;主題交換器則根據(jù)消息的路由鍵和綁定鍵的匹配規(guī)則進(jìn)行路由。

RPC的概念是什么?

RPC 即遠(yuǎn)程過(guò)程調(diào)用,允許程序調(diào)用運(yùn)行在另一臺(tái)計(jì)算機(jī)上的程序中的過(guò)程或函數(shù),就像調(diào)用本地程序中的過(guò)程或函數(shù)一樣,而無(wú)需了解底層網(wǎng)絡(luò)細(xì)節(jié)。

一個(gè)典型的 RPC 調(diào)用過(guò)程通常包含以下幾個(gè)步驟:

客戶端調(diào)用:客戶端程序調(diào)用本地的一個(gè) “偽函數(shù)”(也稱為存根,Stub),并傳入所需的參數(shù)。這個(gè) “偽函數(shù)” 看起來(lái)和普通的本地函數(shù)一樣,但實(shí)際上它會(huì)負(fù)責(zé)處理遠(yuǎn)程調(diào)用的相關(guān)事宜。

請(qǐng)求發(fā)送:客戶端存根將調(diào)用信息(包括函數(shù)名、參數(shù)等)進(jìn)行序列化,然后通過(guò)網(wǎng)絡(luò)將請(qǐng)求發(fā)送到服務(wù)器端。

服務(wù)器接收與處理:服務(wù)器端接收到請(qǐng)求后,將請(qǐng)求信息進(jìn)行反序列化,然后找到對(duì)應(yīng)的函數(shù)并執(zhí)行。

結(jié)果返回:服務(wù)器端將函數(shù)的執(zhí)行結(jié)果進(jìn)行序列化,通過(guò)網(wǎng)絡(luò)發(fā)送回客戶端。

客戶端接收結(jié)果:客戶端接收到服務(wù)器返回的結(jié)果后,將其反序列化,并將結(jié)果返回給調(diào)用者。

常見(jiàn)的 RPC 框架:

gRPC:由 Google 開(kāi)發(fā)的高性能、開(kāi)源的 RPC 框架,支持多種編程語(yǔ)言,使用 Protocol Buffers 作為序列化協(xié)議,具有高效、靈活等特點(diǎn)。

Thrift:由 Facebook 開(kāi)發(fā)的跨語(yǔ)言的 RPC 框架,支持多種數(shù)據(jù)傳輸協(xié)議和序列化格式,具有良好的可擴(kuò)展性和性能。

Dubbo阿里巴巴開(kāi)源的高性能 Java RPC 框架,提供了服務(wù)治理、集群容錯(cuò)、負(fù)載均衡等功能,廣泛應(yīng)用于國(guó)內(nèi)的互聯(lián)網(wǎng)企業(yè)。

網(wǎng)絡(luò)

HTTP狀態(tài)碼的含義你知道哪些?

HTTP 狀態(tài)碼分為 5 大類

1xx 類狀態(tài)碼屬于提示信息,是協(xié)議處理中的一種中間狀態(tài),實(shí)際用到的比較少。2xx 類狀態(tài)碼表示服務(wù)器成功處理了客戶端的請(qǐng)求,也是我們最愿意看到的狀態(tài)。3xx 類狀態(tài)碼表示客戶端請(qǐng)求的資源發(fā)生了變動(dòng),需要客戶端用新的 URL 重新發(fā)送請(qǐng)求獲取資源,也就是重定向。4xx 類狀態(tài)碼表示客戶端發(fā)送的報(bào)文有誤,服務(wù)器無(wú)法處理,也就是錯(cuò)誤碼的含義。5xx 類狀態(tài)碼表示客戶端請(qǐng)求報(bào)文正確,但是服務(wù)器處理時(shí)內(nèi)部發(fā)生了錯(cuò)誤,屬于服務(wù)器端的錯(cuò)誤碼。

其中常見(jiàn)的具體狀態(tài)碼有:

    200:請(qǐng)求成功;301:永久重定向;302:臨時(shí)重定向;404:無(wú)法找到此頁(yè)面;405:請(qǐng)求的方法類型不支持;500:服務(wù)器內(nèi)部出錯(cuò)。

RESTful API的設(shè)計(jì)原則是什么?

RESTful API 設(shè)計(jì)規(guī)范是指設(shè)計(jì)和開(kāi)發(fā) RESTful API 時(shí)應(yīng)遵循的一些規(guī)范和準(zhǔn)則。下面介紹一些常見(jiàn)的設(shè)計(jì)規(guī)范:

1、使用 HTTP 動(dòng)詞來(lái)表達(dá)操作

RESTful API 中的操作應(yīng)該使用 HTTP 動(dòng)詞來(lái)表達(dá),例如 GET、POST、PUT、DELETE 等,以確保對(duì)資源的操作被明確表示和限制。如下所示:

GET?/users/1
POST?/users
PUT?/users/1
DELETE?/users/1

2、使用名詞來(lái)表示資源

RESTful API 中應(yīng)該使用名詞來(lái)表示資源,而不是動(dòng)詞,以避免歧義和混淆。例如:

GET?/users/1
GET?/orders/1

3、使用 URI 來(lái)定位資源

RESTful API 應(yīng)該使用 URI 來(lái)定位資源,以確保每個(gè)資源都有一個(gè)唯一的標(biāo)識(shí)符。URI 應(yīng)該具有層級(jí)結(jié)構(gòu),以便表示資源之間的關(guān)系。例如:

GET?/users/1/orders/1

4、使用查詢參數(shù)來(lái)過(guò)濾和分頁(yè)

RESTful API 應(yīng)該使用查詢參數(shù)來(lái)過(guò)濾和分頁(yè)資源,例如:

GET /users?gender=male
GET /users?page=1&pageSize=10

5、使用 HTTP 狀態(tài)碼來(lái)表示請(qǐng)求結(jié)果

RESTful API 應(yīng)該使用 HTTP 狀態(tài)碼來(lái)表示請(qǐng)求結(jié)果,以便客戶端能夠根據(jù)狀態(tài)碼進(jìn)行處理。例如:

    200:請(qǐng)求成功201:資源創(chuàng)建成功400:請(qǐng)求參數(shù)錯(cuò)誤401:未授權(quán)訪問(wèn)403:表示禁止訪問(wèn)資源。404:表示未找到資源。500:表示服務(wù)器內(nèi)部錯(cuò)誤。

6、使用 JSON 或 XML 來(lái)表示數(shù)據(jù)

RESTful API 應(yīng)該使用 JSON 或 XML 來(lái)表示數(shù)據(jù),以便不同的客戶端能夠方便地進(jìn)行數(shù)據(jù)解析和處理。例如:

GET?/users/1
{
??"id":?1,
??"name":?"Tom",
??"age":?25
}

7、使用版本號(hào)來(lái)管理 API

RESTful API 應(yīng)該使用版本號(hào)來(lái)管理 API 的不同版本,以便支持舊版 API 的兼容性和平穩(wěn)升級(jí)。例如:

GET?/v1/users/1

8、使用 HATEOAS 來(lái)提高 API 的可發(fā)現(xiàn)性

HATEOAS(Hypermedia As The Engine Of Application State)是指使用超媒體作為應(yīng)用程序狀態(tài)的引擎,從而提高 RESTful API 的可發(fā)現(xiàn)性。通過(guò)使用 HATEOAS,客戶端可以通過(guò) API 返回的鏈接自主地遍歷 API,并進(jìn)行資源的操作。

例如:

GET?/users/1
{
??"id":?1,
??"name":?"Tom",
??"age":?25,
??"links":?[
????{
??????"rel":?"orders",
??????"href":?"/users/1/orders"
????},
????{
??????"rel":?"edit",
??????"href":?"/users/1/edit"
????}
??]
}

上述代碼中的 links 字段包含了與當(dāng)前資源相關(guān)的鏈接,客戶端可以通過(guò)這些鏈接來(lái)訪問(wèn)其他資源。

算法

    求數(shù)組的最大連續(xù)和。

相關(guān)推薦