中廠Java后端15連問!
前言
大家好,我是田螺。最近一位星球粉絲去面試一個(gè)中廠,Java后端。他說,好幾道題答不上來,于是我?guī)兔φ砹艘徊ù鸢?/p>
- G1收集器
- JVM內(nèi)存劃分
- 對象進(jìn)入老年代標(biāo)志
- 你在項(xiàng)目中用到的是哪種收集器,怎么調(diào)優(yōu)的
- new對象的內(nèi)存分布
- 局部變量的內(nèi)存分布
- Synchronized和Lock的區(qū)別
- Synchronized原理
- 可重入是如何知道當(dāng)前鎖的擁有著的
- Spring用到的設(shè)計(jì)模式
- 說說SPI
- 排行榜怎么設(shè)計(jì)
- SpringBoot 中注解實(shí)現(xiàn)緩存用過沒?實(shí)現(xiàn)原理是什么。
- 深分頁優(yōu)化
- Redis分布式鎖如何進(jìn)一步提升性能
1. 說說G1收集器
G1(Garbage-First)收集器是 Java 虛擬機(jī)中的一種垃圾收集器。它于 JDK 7 中首次引入,并在后續(xù)版本中不斷改進(jìn)優(yōu)化。
- 可以使用 -XX:+UseG1GC 開啟它。這個(gè)選項(xiàng)告訴 JVM 在運(yùn)行時(shí)使用 G1 垃圾收集器來管理堆內(nèi)存。
- G1(Garbage-First)收集器的核心原則之一就是“首先收集盡可能多的垃圾”,即優(yōu)先回收那些包含最多垃圾的區(qū)域。這個(gè)原則是與CMS收集器有所不同的,CMS主要關(guān)注于盡可能減少應(yīng)用程序的停頓時(shí)間。
- G1 收集器會將 Java 堆劃分為多個(gè)大小相等的區(qū)域(Region),然后根據(jù)當(dāng)前堆內(nèi)存的使用情況,選擇性地進(jìn)行垃圾回收。
- 當(dāng) G1 收集器決定進(jìn)行Full GC時(shí),它會執(zhí)行一次 Full GC(Full GC),這時(shí)整個(gè)應(yīng)用程序?qū)和#⊿TW)。但在Mixed GC中,G1 收集器會在某些情況下選擇部分年輕代分區(qū)和部分老年代分區(qū)進(jìn)行回收,這樣一來,即使不是整個(gè)堆內(nèi)存的垃圾回收,仍然可能會導(dǎo)致一些暫停,但相對于 Full GC,這種暫停時(shí)間會更短。
- 適用于大內(nèi)存、對停頓時(shí)間敏感、需要高吞吐量的應(yīng)用場景。
2. JVM內(nèi)存劃分
Java 虛擬機(jī)(JVM)的內(nèi)存分為多個(gè)區(qū)域,每個(gè)區(qū)域都有不同的作用和管理方式。JVM 內(nèi)存劃分的主要區(qū)域:
圖片
- 堆(Heap):堆是 JVM 中最大的一塊內(nèi)存區(qū)域,用于存儲對象實(shí)例和數(shù)組。堆內(nèi)存由所有線程共享,是垃圾收集器主要管理的區(qū)域。
- 程序計(jì)數(shù)器(Program Counter):程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它是線程私有的,用于存儲當(dāng)前線程正在執(zhí)行的字節(jié)碼指令的地址。在多線程環(huán)境下,每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,以保證線程切換后能夠恢復(fù)到正確的執(zhí)行位置。
- 虛擬機(jī)棧(Java Virtual Machine Stacks):虛擬機(jī)棧也是線程私有的,用于存儲方法執(zhí)行過程中的局部變量、方法參數(shù)、中間結(jié)果等數(shù)據(jù)。每個(gè)方法在執(zhí)行時(shí)都會創(chuàng)建一個(gè)棧幀,棧幀包含了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址等信息。
- 本地方法棧(Native Method Stacks):本地方法棧與虛擬機(jī)棧類似,用于執(zhí)行本地方法(Native Method)時(shí)的數(shù)據(jù)存儲。與虛擬機(jī)棧一樣,本地方法棧也是線程私有的。
- 方法區(qū)(Method Area):方法區(qū)用于存儲類信息、常量、靜態(tài)變量和即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。在jdk7及以前,習(xí)慣上把方法區(qū),稱為永久代。jdk8開始,使用元空間取代了永久代。本質(zhì)上,方法區(qū)和永久代并不等價(jià)。通過-XX:Permsize來設(shè)置永久代初始分配空間??梢允褂脜?shù) -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定默認(rèn)值依賴于平臺。
- 運(yùn)行時(shí)常量池(Runtime Constant Pool):運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存儲編譯時(shí)生成的字面量常量和符號引用。與類文件中的常量池(Constant Pool)相對應(yīng),運(yùn)行時(shí)常量池具有動(dòng)態(tài)性,可以在運(yùn)行時(shí)動(dòng)態(tài)地添加、修改常量。
- 直接內(nèi)存(Direct Memory):直接內(nèi)存不是 JVM 內(nèi)存的一部分,但是在一些情況下會被 JVM 使用。直接內(nèi)存是通過使用 java.nio 包中的類來申請和釋放的,它允許 JVM 在堆外分配內(nèi)存,可以提高 I/O 操作的效率。
3. 對象進(jìn)入老年代標(biāo)志
圖片
- 對象年齡達(dá)到閾值:對象在 Java 堆中的存活時(shí)間通常會通過對象年齡(Age)來衡量。在某些垃圾收集器中,當(dāng)對象經(jīng)過多次垃圾收集后仍然存活,并且達(dá)到了一定的年齡閾值,就會被晉升到老年代。年齡閾值可以通過 JVM 參數(shù)進(jìn)行調(diào)整。
- 大對象:大對象通常指的是占用大量內(nèi)存空間的對象,例如數(shù)組或者很大的字符串。在一些垃圾收集器中,為了避免在新生代中頻繁復(fù)制大對象,會直接將大對象分配在老年代中。
- 長期存活的對象:一些長期存活的對象,例如長期存在的線程、靜態(tài)變量等,有可能直接被分配到老年代中。
- 晉升失?。寒?dāng)新生代中的對象經(jīng)過多次垃圾回收仍然存活,并且新生代內(nèi)存不足以容納這些存活對象時(shí),會發(fā)生一次晉升失敗,這時(shí)一部分存活對象可能會被直接晉升到老年代。
通常情況下,老年代用于存放長期存活的對象,以減少垃圾收集的頻率和提高垃圾回收的效率。
4. 你在項(xiàng)目中用到的是哪種收集器,怎么調(diào)優(yōu)的
我列舉了常見的垃圾收集器以及調(diào)優(yōu)策略:
串行收集器(Serial Garbage Collector):
- 適用于單核 CPU 或小型應(yīng)用場景。
- 調(diào)優(yōu)參數(shù):-XX:+UseSerialGC
并行收集器(Parallel Garbage Collector):
- 適用于多核 CPU,通過并行收集來提高垃圾回收效率。
- 調(diào)優(yōu)參數(shù):-XX:+UseParallelGC
并發(fā)標(biāo)記-清除收集器(Concurrent Mark-Sweep Garbage Collector,CMS GC):
- 適用于對系統(tǒng)停頓時(shí)間敏感的應(yīng)用場景。
- 調(diào)優(yōu)參數(shù):-XX:+UseConcMarkSweepGC
G1 收集器(Garbage-First Garbage Collector):
- 適用于大內(nèi)存、對停頓時(shí)間敏感、需要高吞吐量的應(yīng)用場景。
- 調(diào)優(yōu)參數(shù):-XX:+UseG1GC
ZGC 和 Shenandoah(JDK 11+):
- 適用于超大內(nèi)存、對停頓時(shí)間極為敏感的應(yīng)用場景。
- 調(diào)優(yōu)參數(shù):-XX:+UseZGC(ZGC)、-XX:+UseShenandoahGC(Shenandoah)
當(dāng)然,調(diào)優(yōu)的具體策略會根據(jù)應(yīng)用的特點(diǎn)和需求來定,可以通過以下一些常見的調(diào)優(yōu)手段來改進(jìn)垃圾收集器的性能:
- 調(diào)整堆大小(-Xms、-Xmx)以及新生代與老年代的比例(-XX:NewRatio、-XX:SurvivorRatio)。
- 設(shè)置垃圾收集器的參數(shù),如并行收集器的線程數(shù)(-XX:ParallelGCThreads)、并發(fā)標(biāo)記-清除收集器的初始標(biāo)記階段并發(fā)線程數(shù)(-XX:ConcGCThreads)等。
- 設(shè)置垃圾收集器的觸發(fā)條件,如新生代垃圾收集的觸發(fā)條件(-XX:MaxNewSize、-XX:NewThreshold)等。
- 監(jiān)控和分析 GC 日志,根據(jù)應(yīng)用的實(shí)際情況進(jìn)行優(yōu)化。選擇合適的 GC 日志分析工具,如GCViewer、GCEasy等,進(jìn)行進(jìn)一步的性能分析和優(yōu)化。
5. new對象的內(nèi)存分布
在 Java 中,當(dāng)使用 new 關(guān)鍵字創(chuàng)建一個(gè)對象時(shí),對象在內(nèi)存中的基本結(jié)構(gòu)通常由對象頭、實(shí)例數(shù)據(jù)和填充組成。
- 對象頭(Object Header):對象頭存儲了對象的元數(shù)據(jù)信息,如哈希碼、對象鎖狀態(tài)、GC 相關(guān)信息等。對象頭的結(jié)構(gòu)和大小在不同的 JVM 實(shí)現(xiàn)中可能會有所不同,但通常包括一些固定的字段。
- 實(shí)例數(shù)據(jù)(Instance Data):實(shí)例數(shù)據(jù)包含了對象的實(shí)際數(shù)據(jù),即對象中的成員變量的值。這些數(shù)據(jù)根據(jù)對象的類定義來確定,包括各種類型的字段和對象引用等。
- 填充(Padding):填充是為了滿足內(nèi)存對齊的需要而添加的額外字節(jié)。內(nèi)存對齊可以提高內(nèi)存訪問的效率,一些 JVM 可能會在對象的實(shí)例數(shù)據(jù)后添加填充字節(jié),使得對象的起始地址能夠?qū)R到某個(gè)特定的邊界。
6. 局部變量的內(nèi)存分布
棧幀結(jié)構(gòu):
- 棧幀由三部分組成:局部變量表(Local Variable Table)、操作數(shù)棧(Operand Stack)和幀數(shù)據(jù)(Frame Data)。
- 局部變量表用于存儲方法參數(shù)和方法內(nèi)部定義的局部變量。
- 操作數(shù)棧用于存儲方法執(zhí)行過程中的操作數(shù)。
- 幀數(shù)據(jù)包含了方法的異常處理信息、方法返回地址等。
局部變量表:
- 局部變量表是一個(gè)數(shù)組,用于存儲方法參數(shù)和方法內(nèi)部定義的局部變量。
- 局部變量表中的每個(gè)元素都可以存儲一個(gè)數(shù)據(jù)值,數(shù)據(jù)類型可以是基本數(shù)據(jù)類型或者引用類型。
- 局部變量表中的變量只在方法執(zhí)行期間有效,當(dāng)方法執(zhí)行結(jié)束后,局部變量表所占用的內(nèi)存空間會被釋放。
局部變量的存儲位置:
- 對于基本數(shù)據(jù)類型的局部變量,它們的值直接存儲在局部變量表中。
- 對于引用類型的局部變量,局部變量表中存儲的是對象的引用,而對象的實(shí)際數(shù)據(jù)存儲在堆內(nèi)存中。
局部變量的生命周期:
- 局部變量的生命周期與方法的執(zhí)行周期相同,當(dāng)方法執(zhí)行結(jié)束后,局部變量表所占用的內(nèi)存空間會被釋放。
- 局部變量的生命周期也可能會被延長,比如局部變量被捕獲到一個(gè)匿名內(nèi)部類中,那么該局部變量的生命周期會與匿名內(nèi)部類的生命周期保持一致。
7.Synchronized和ReenTrantLock的區(qū)別
- Synchronized是依賴于JVM實(shí)現(xiàn)的,而ReenTrantLock是API實(shí)現(xiàn)的。
- 在Synchronized優(yōu)化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)后,兩者性能就差不多了。
- Synchronized的使用比較方便簡潔,它由編譯器去保證鎖的加鎖和釋放。而ReenTrantLock需要手工聲明來加鎖和釋放鎖,最好在finally中聲明釋放鎖。
- ReentrantLock可以指定是公平鎖還是?公平鎖。?synchronized只能是?公平鎖。
- ReentrantLock可響應(yīng)中斷、可輪回,而Synchronized是不可以響應(yīng)中斷的
8.Synchronized原理
synchronized是Java中的關(guān)鍵字,是一種同步鎖。synchronized關(guān)鍵字可以作用于方法或者代碼塊。
一般面試時(shí)??梢赃@么回答:
- 反編譯后,monitorenter、monitorexit、ACC_SYNCHRONIZED
- monitor監(jiān)視器
- Java Monitor 的工作機(jī)理
- 對象與monitor關(guān)聯(lián)
8.1 monitorenter、monitorexit、ACC_SYNCHRONIZED
- 如果synchronized作用于代碼塊,反編譯可以看到兩個(gè)指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)同步;
- 如果作用synchronized作用于方法,反編譯可以看到ACCSYNCHRONIZED標(biāo)記,JVM通過在方法訪問標(biāo)識符(flags)中加入ACCSYNCHRONIZED來實(shí)現(xiàn)同步功能。
- 同步代碼塊是通過monitorenter和monitorexit來實(shí)現(xiàn),當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得monitor鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)行到monitorexit的時(shí)候則要釋放鎖。
- 同步方法是通過中設(shè)置ACCSYNCHRONIZED標(biāo)志來實(shí)現(xiàn),當(dāng)線程執(zhí)行有ACCSYNCHRONIZED標(biāo)志的方法,需要獲得monitor鎖。每個(gè)對象都與一個(gè)monitor相關(guān)聯(lián),線程可以占有或者釋放monitor。
8.2 monitor監(jiān)視器
monitor是什么呢?操作系統(tǒng)的管程(monitors)是概念原理,ObjectMonitor是它的原理實(shí)現(xiàn)。
在Java虛擬機(jī)(HotSpot)中,Monitor(管程)是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor() {
_header = NULL;
_count = 0; // 記錄個(gè)數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 處于wait狀態(tài)的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 處于等待鎖block狀態(tài)的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中幾個(gè)關(guān)鍵字段的含義如圖所示:
圖片
8.3 Java Monitor 的工作機(jī)理
圖片
- 想要獲取monitor的線程,首先會進(jìn)入_EntryList隊(duì)列。
- 當(dāng)某個(gè)線程獲取到對象的monitor后,進(jìn)入Owner區(qū)域,設(shè)置為當(dāng)前線程,同時(shí)計(jì)數(shù)器count加1。
- 如果線程調(diào)用了wait()方法,則會進(jìn)入WaitSet隊(duì)列。它會釋放monitor鎖,即將owner賦值為null,count自減1,進(jìn)入WaitSet隊(duì)列阻塞等待。
- 如果其他線程調(diào)用 notify() / notifyAll() ,會喚醒WaitSet中的某個(gè)線程,該線程再次嘗試獲取monitor鎖,成功即進(jìn)入Owner區(qū)域。
- 同步方法執(zhí)行完畢了,線程退出臨界區(qū),會將monitor的owner設(shè)為null,并釋放監(jiān)視鎖。
8.4 對象與monitor關(guān)聯(lián)
圖片
- 在HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header),實(shí)例數(shù)據(jù)(Instance Data)和對象填充(Padding)。
- 對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Class Pointer(類型指針)。
- Mark Word 是用于存儲對象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等。
- 重量級鎖,指向互斥量的指針。其實(shí)synchronized是重量級鎖,也就是說Synchronized的對象鎖,Mark Word鎖標(biāo)識位為10,其中指針指向的是Monitor對象的起始地址。
9.可重入是如何知道當(dāng)前鎖的擁有著的
比如,ReentrantLock 是可重入的鎖。ReentrantLock 類實(shí)現(xiàn)了可重入鎖的概念,允許同一個(gè)線程在持有鎖的情況下多次獲取同一個(gè)鎖,而不會被阻塞。
在 ReentrantLock 中,每個(gè)鎖都關(guān)聯(lián)著一個(gè)持有計(jì)數(shù)器和一個(gè)擁有者線程。當(dāng)一個(gè)線程首次獲取鎖時(shí),持有計(jì)數(shù)器會增加;當(dāng)該線程再次獲取鎖時(shí),持有計(jì)數(shù)器會繼續(xù)增加。每次釋放鎖時(shí),計(jì)數(shù)器會相應(yīng)減少。只有當(dāng)持有計(jì)數(shù)器減為零時(shí),鎖才會完全釋放,其他線程才有機(jī)會獲取鎖。
這樣一來,當(dāng)線程嘗試獲取鎖時(shí),它會檢查當(dāng)前鎖的持有計(jì)數(shù)器以及擁有者線程。如果鎖未被任何線程持有,或者當(dāng)前線程是鎖的擁有者,那么鎖將立即分配給當(dāng)前線程。否則,當(dāng)前線程會被阻塞,直到鎖被釋放。
10.Spring用到的設(shè)計(jì)模式
- 單例模式(Singleton Pattern):Spring 中的 Bean 默認(rèn)是單例的,即每個(gè) Bean 只有一個(gè)實(shí)例。這種方式可以提高性能并減少資源消耗。
- 工廠模式(Factory Pattern):Spring 使用工廠模式來創(chuàng)建和管理 Bean。它提供了幾種不同類型的工廠,比如 BeanFactory 和 ApplicationContext,用于創(chuàng)建和管理 Bean 對象。
- 代理模式(Proxy Pattern):Spring AOP(面向切面編程)功能基于代理模式實(shí)現(xiàn)。它允許通過在運(yùn)行時(shí)為目標(biāo)對象創(chuàng)建代理對象來添加橫切關(guān)注點(diǎn)(如日志記錄、性能監(jiān)控等)。
- 裝飾者模式(Decorator Pattern):Spring 中的 BeanPostProcessor 接口就是一個(gè)裝飾器模式的例子。它允許在 Bean 初始化過程中動(dòng)態(tài)地添加新的功能。
- 觀察者模式(Observer Pattern):Spring 的事件機(jī)制就是基于觀察者模式實(shí)現(xiàn)的。它允許 Bean 發(fā)布事件,其他 Bean 可以注冊監(jiān)聽器來響應(yīng)這些事件。
- 策略模式(Strategy Pattern):Spring 的 IOC(控制反轉(zhuǎn))和 DI(依賴注入)功能基于策略模式實(shí)現(xiàn)。它允許將不同的實(shí)現(xiàn)注入到一個(gè)接口或抽象類中,以便在運(yùn)行時(shí)選擇不同的行為。
- 模板模式(Template Pattern):Spring 的 JdbcTemplate 和 HibernateTemplate 等模板類就是模板模式的應(yīng)用。它們封裝了一些常見的操作,使開發(fā)者可以通過簡單的方法調(diào)用來執(zhí)行數(shù)據(jù)庫操作。
- 適配器模式(Adapter Pattern):Spring 的 AOP 功能和 Spring MVC 框架都使用了適配器模式。它們允許將現(xiàn)有的類與新的接口進(jìn)行適配,以便實(shí)現(xiàn)新的功能。
- 建造者模式(Builder Pattern):Spring 中的 BeanDefinitionBuilder 和 BeanFactoryBuilder 等構(gòu)建器模式的應(yīng)用。它們用于構(gòu)建復(fù)雜的對象,并且允許逐步設(shè)置對象的屬性。
11.聊聊SPI
SPI,其實(shí)就是Service Provider Interface,是Java提供的一種機(jī)制,用于在運(yùn)行時(shí)動(dòng)態(tài)裝載實(shí)現(xiàn)模塊,使得應(yīng)用程序能夠擴(kuò)展、替換特定的服務(wù)或?qū)崿F(xiàn)。
SPI的工作原理主要包括以下幾個(gè)關(guān)鍵點(diǎn):
- 接口定義:首先,需要定義一個(gè)接口,該接口定義了一組操作或服務(wù)。這個(gè)接口通常由Java核心庫或者第三方庫提供。
- 服務(wù)提供者:其次,可以有多個(gè)服務(wù)提供者來實(shí)現(xiàn)這個(gè)接口。這些服務(wù)提供者通常是獨(dú)立的模塊或者庫,它們在自己的jar包中提供了實(shí)現(xiàn)。
- 配置文件:在Java中,SPI通過在META-INF/services目錄下的特定配置文件中指定實(shí)現(xiàn)類的方式來實(shí)現(xiàn)動(dòng)態(tài)裝載。具體來說,SPI機(jī)制要求服務(wù)提供者在這個(gè)目錄下創(chuàng)建一個(gè)以接口全限定名為文件名的文件,文件中列出了具體的實(shí)現(xiàn)類名。
- 動(dòng)態(tài)加載:當(dāng)應(yīng)用程序需要某個(gè)服務(wù)時(shí),Java運(yùn)行時(shí)會動(dòng)態(tài)加載配置文件中指定的實(shí)現(xiàn)類,并實(shí)例化它們。這種機(jī)制使得應(yīng)用程序能夠在不修改源代碼的情況下靈活地替換或擴(kuò)展特定的服務(wù)實(shí)現(xiàn)。
在實(shí)際應(yīng)用中,我們可以利用SPI來實(shí)現(xiàn)插件化架構(gòu)、模塊化開發(fā)等,從而提高代碼的可維護(hù)性和可擴(kuò)展性。
12.排行榜怎么設(shè)計(jì)
可以考慮:數(shù)據(jù)庫的order by、或者Redis 的zset
大家可以看下我的這篇文章哈:
如何設(shè)計(jì)一個(gè)排行榜
對于游戲排行榜,如果數(shù)據(jù)量達(dá)到億萬級別,需要考慮的確實(shí)是分桶策略,而桶排序則是分桶策略的一種可能實(shí)現(xiàn)方式之一。
桶排序是一種基于這種分桶策略的排序算法,它將數(shù)據(jù)劃分到若干個(gè)有序的桶中,然后分別對每個(gè)桶中的數(shù)據(jù)進(jìn)行排序,最后將所有桶中的數(shù)據(jù)按照順序合并起來,得到排好序的結(jié)果。
在游戲排行榜系統(tǒng)中,可以根據(jù)玩家的分?jǐn)?shù)范圍、等級、地區(qū)等因素來設(shè)計(jì)分桶策略,將玩家數(shù)據(jù)劃分到不同的桶中。這樣可以使得每個(gè)桶內(nèi)的數(shù)據(jù)量相對較小,提高了排序的效率。
13.SpringBoot 中注解實(shí)現(xiàn)緩存用過沒?實(shí)現(xiàn)原理是什么。
常見的緩存注解包括 @Cacheable、@CachePut、@CacheEvict 等。這些注解的實(shí)現(xiàn)原理基于Spring提供的緩存抽象。
是的,Spring Boot 中的緩存注解常用于提升系統(tǒng)性能,減少重復(fù)計(jì)算,常見的緩存注解包括 @Cacheable、@CachePut、@CacheEvict 等。這些注解的實(shí)現(xiàn)原理基于 Spring Framework 提供的緩存抽象。
實(shí)現(xiàn)原理:
- 代理機(jī)制:
- Spring Boot 使用 AOP(面向切面編程)的方式,在運(yùn)行時(shí)動(dòng)態(tài)地為帶有緩存注解的方法生成代理對象。
- 當(dāng)調(diào)用帶有緩存注解的方法時(shí),實(shí)際上是調(diào)用代理對象的方法。
- 緩存管理器:
- Spring Boot 提供了多種緩存管理器的實(shí)現(xiàn),比如基于 ConcurrentHashMap 的 SimpleCacheManager、基于 Ehcache的緩存管理器。
- 緩存管理器負(fù)責(zé)真正地操作緩存,將數(shù)據(jù)存儲到緩存中或者從緩存中獲取數(shù)據(jù)。
- 緩存注解:
- @Cacheable:標(biāo)記在方法上,表示方法的返回值將會被緩存,當(dāng)方法被調(diào)用時(shí),首先從緩存中查找數(shù)據(jù),如果緩存中存在數(shù)據(jù),則直接返回,否則執(zhí)行方法并將結(jié)果存儲到緩存中。
- @CachePut:標(biāo)記在方法上,表示方法的返回值將會被存儲到緩存中,即使緩存中已經(jīng)存在相同 key 的數(shù)據(jù),也會重新存儲。
- @CacheEvict:標(biāo)記在方法上,表示清除緩存中的數(shù)據(jù),可以根據(jù)條件來清除指定的緩存數(shù)據(jù)。
- 緩存 key 的生成:
- 默認(rèn)情況下,緩存 key 是由方法的參數(shù)組成的,默認(rèn)的 key 生成器是 SimpleKeyGenerator。
- 可以通過自定義 key 生成器來生成復(fù)雜的緩存 key。
- 緩存注解的執(zhí)行流程:
- 當(dāng)方法被調(diào)用時(shí),首先會根據(jù)方法參數(shù)生成緩存 key。
- 然后從緩存中根據(jù)緩存 key 查找數(shù)據(jù),如果找到則返回,否則執(zhí)行方法并將結(jié)果存儲到緩存中。
- 在 @CachePut 和 @CacheEvict 注解中,會根據(jù)條件清除緩存中的數(shù)據(jù)或者將數(shù)據(jù)存儲到緩存中。
14.深分頁優(yōu)化
我們可以通過減少回表次數(shù)來優(yōu)化。一般有標(biāo)簽記錄法和延遲關(guān)聯(lián)法。
14.1 標(biāo)簽記錄法
就是標(biāo)記一下上次查詢到哪一條了,下次再來查的時(shí)候,從該條開始往下掃描。就好像看書一樣,上次看到哪里了,你就折疊一下或者夾個(gè)書簽,下次來看的時(shí)候,直接就翻到啦。
假設(shè)上一次記錄到100000,則SQL可以修改為:
select id,name,balance FROM account where id > 100000 limit 10;
這樣的話,后面無論翻多少頁,性能都會不錯(cuò)的,因?yàn)槊辛薸d索引。但是這種方式有局限性:需要一種類似連續(xù)自增的字段。
14.2 延遲關(guān)聯(lián)法
延遲關(guān)聯(lián)法,就是把條件轉(zhuǎn)移到主鍵索引樹,然后減少回表。假設(shè)原生SQL是這樣的的,其中id是主鍵,create_time是普通索引
select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
使用延遲關(guān)聯(lián)法優(yōu)化,如下:
select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN
(SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10)
AS acct2 on acct1.id= acct2.id;
優(yōu)化思路就是,先通過idx_create_time二級索引樹查詢到滿足條件的主鍵ID,再與原表通過主鍵ID內(nèi)連接,這樣后面直接走了主鍵索引了,同時(shí)也減少了回表。
15.分布式鎖如何進(jìn)一步提升性能,答了Redis的實(shí)現(xiàn)思路好像不是面試官想聽的
redis分布式鎖,我覺得可以從一下這幾個(gè)方向來回答:
- Pipeline 批量操作:使用 Redis 的 Pipeline 功能可以將多個(gè) Redis 命令打包發(fā)送給服務(wù)器,減少網(wǎng)絡(luò)延遲,提高性能。在獲取鎖、釋放鎖等操作時(shí),可以考慮使用 Pipeline 來批量執(zhí)行多個(gè)命令。
- Lua 腳本:Redis 支持 Lua 腳本,可以將多個(gè)操作封裝在一個(gè)腳本中,在服務(wù)器端原子性地執(zhí)行,減少了網(wǎng)絡(luò)通信的開銷??梢詫@取鎖和釋放鎖的邏輯封裝在 Lua 腳本中,以提高性能。
- 降級策略:在高并發(fā)情況下,可以考慮引入降級策略,當(dāng)獲取鎖失敗時(shí),可以使用備用方案或者默認(rèn)值來處理,而不是一直等待鎖的釋放。
- 監(jiān)控和優(yōu)化:通過監(jiān)控 Redis 的性能指標(biāo),如連接數(shù)、命令執(zhí)行時(shí)間等,可以及時(shí)發(fā)現(xiàn)性能瓶頸,并進(jìn)行優(yōu)化??梢酝ㄟ^ Redis 的監(jiān)控工具或者第三方監(jiān)控工具來實(shí)現(xiàn)監(jiān)控。
- 合理設(shè)置鎖的過期時(shí)間:根據(jù)業(yè)務(wù)場景的特點(diǎn)和需求,合理設(shè)置鎖的過期時(shí)間,避免鎖被長時(shí)間占用而影響系統(tǒng)性能。可以根據(jù)操作的耗時(shí)和鎖的競爭情況來動(dòng)態(tài)調(diào)整鎖的過期時(shí)間。
- 使用 Redlock 算法:Redlock 是一種基于多個(gè) Redis 節(jié)點(diǎn)的分布式鎖算法,可以提高分布式鎖的可靠性和性能??梢钥紤]使用 Redlock 算法來實(shí)現(xiàn)分布式鎖。