還得是美團(tuán)啊!依舊是校招大戶,給力!
共 30352字,需瀏覽 61分鐘
·
2024-08-02 17:00
大家好,我是小林。
早上看到美團(tuán)要準(zhǔn)備開始秋招了,計(jì)劃招聘的人數(shù)也寫出來了:6000 人,規(guī)模還是非常龐大的,不愧是秋招大戶!
去年美團(tuán)就是大廠里校招大戶,很多進(jìn)了大廠的同學(xué),都是拿了美團(tuán)的 offer,而且發(fā) offer 發(fā)的快,也發(fā)的很早,10-11 月份就基本發(fā)了 多 offer,很多同學(xué)拿到的第一個(gè)大廠 offer,就是美團(tuán)給的。
去年美團(tuán)是大廠里第一個(gè)開獎校招薪資的,今年美團(tuán)校招招聘也開啟比較早,大概率 9-10 月份就能看到很多同學(xué)都收到了美團(tuán)的開獎通知,那么先給大家看看去年美團(tuán)校招薪資,等今年校招薪資出來,在做一下對比。
美團(tuán)年總包構(gòu)成 = 月薪 x 15.5
-
普通 offer:21k*15.5,年包:32w -
sp offer:24k~26k*15.5,年包:37w~40w -
ssp offer:27k~29k * 15.5 + 股票 + 簽字費(fèi),年包:42w~50w
既然美團(tuán)秋招準(zhǔn)備開始了,那么給大家分享一位去年秋招拿到美團(tuán)offer 同學(xué)的 Java后端面經(jīng),給準(zhǔn)備參加秋招的同學(xué)做一個(gè)參考。
面經(jīng)比較有代表性,也對問題做了總結(jié),希望能幫助最近在準(zhǔn)備面試的同學(xué)。根據(jù)面試熱點(diǎn)題目去準(zhǔn)備知識的,目的性會比較強(qiáng),方向也比較清晰一點(diǎn)。
-
Java:線程池、鎖、反射、ThreadLocal、四種引用、spring、JVM -
MySQL:事務(wù)特性、解決慢查詢、explain、B+樹、索引、MVCC -
Redis: 數(shù)據(jù)結(jié)構(gòu)、跳表 -
框架:請求到SpringBoot的處理流程 -
算法:層序遍歷
Java
Java線程池工作原理是什么?
線程池是為了減少頻繁的創(chuàng)建線程和銷毀線程帶來的性能損耗。
線程池分為核心線程池,線程池的最大容量,還有等待任務(wù)的隊(duì)列,提交一個(gè)任務(wù),如果核心線程沒有滿,就創(chuàng)建一個(gè)線程,如果滿了,就是會加入等待隊(duì)列,如果等待隊(duì)列滿了,就會增加線程,如果達(dá)到最大線程數(shù)量,如果都達(dá)到最大線程數(shù)量,就會按照一些丟棄的策略進(jìn)行處理。
那線程池的參數(shù)有哪些?
線程池的構(gòu)造函數(shù)有7個(gè)參數(shù):
-
corePoolSize:線程池核心線程數(shù)量。默認(rèn)情況下,線程池中線程的數(shù)量如果 <= corePoolSize,那么即使這些線程處于空閑狀態(tài),那也不會被銷毀。 -
maximumPoolSize:線程池中最多可容納的線程數(shù)量。當(dāng)一個(gè)新任務(wù)交給線程池,如果此時(shí)線程池中有空閑的線程,就會直接執(zhí)行,如果沒有空閑的線程且當(dāng)前線程池的線程數(shù)量小于corePoolSize,就會創(chuàng)建新的線程來執(zhí)行任務(wù),否則就會將該任務(wù)加入到阻塞隊(duì)列中,如果阻塞隊(duì)列滿了,就會創(chuàng)建一個(gè)新線程,從阻塞隊(duì)列頭部取出一個(gè)任務(wù)來執(zhí)行,并將新任務(wù)加入到阻塞隊(duì)列末尾。如果當(dāng)前線程池中線程的數(shù)量等于maximumPoolSize,就不會創(chuàng)建新線程,就會去執(zhí)行拒絕策略。 -
keepAliveTime:當(dāng)線程池中線程的數(shù)量大于corePoolSize,并且某個(gè)線程的空閑時(shí)間超過了keepAliveTime,那么這個(gè)線程就會被銷毀。 -
unit:就是keepAliveTime時(shí)間的單位。 -
workQueue:工作隊(duì)列。當(dāng)沒有空閑的線程執(zhí)行新任務(wù)時(shí),該任務(wù)就會被放入工作隊(duì)列中,等待執(zhí)行。 -
threadFactory:線程工廠。可以用來給線程取名字等等 -
handler:拒絕策略。當(dāng)一個(gè)新任務(wù)交給線程池,如果此時(shí)線程池中有空閑的線程,就會直接執(zhí)行,如果沒有空閑的線程,就會將該任務(wù)加入到阻塞隊(duì)列中,如果阻塞隊(duì)列滿了,就會創(chuàng)建一個(gè)新線程,從阻塞隊(duì)列頭部取出一個(gè)任務(wù)來執(zhí)行,并將新任務(wù)加入到阻塞隊(duì)列末尾。如果當(dāng)前線程池中線程的數(shù)量等于maximumPoolSize,就不會創(chuàng)建新線程,就會去執(zhí)行拒絕策略
線程池里面的核心線程數(shù)設(shè)置多少合適?
-
CPU 密集型任務(wù)配置盡可能小的線程,cpu核數(shù)+1。 -
IO 密集型任務(wù)則由于線程并不是一直在執(zhí)行任務(wù),則配置盡可能多的線程,如2*cpu核數(shù)。
介紹一下Java里面鎖的分類和特點(diǎn)?
Java中的鎖是用于管理多線程并發(fā)訪問共享資源的關(guān)鍵機(jī)制。鎖可以確保在任意給定時(shí)間內(nèi)只有一個(gè)線程可以訪問特定的資源,從而避免數(shù)據(jù)競爭和不一致性。Java提供了多種鎖機(jī)制,可以分為以下幾類:
-
內(nèi)置鎖(synchronized):Java中的 synchronized關(guān)鍵字是內(nèi)置鎖機(jī)制的基礎(chǔ),可以用于方法或代碼塊。當(dāng)一個(gè)線程進(jìn)入synchronized代碼塊或方法時(shí),它會獲取關(guān)聯(lián)對象的鎖;當(dāng)線程離開該代碼塊或方法時(shí),鎖會被釋放。如果其他線程嘗試獲取同一個(gè)對象的鎖,它們將被阻塞,直到鎖被釋放。其中,syncronized加鎖時(shí)有無鎖、偏向鎖、輕量級鎖和重量級鎖幾個(gè)級別。偏向鎖用于當(dāng)一個(gè)線程進(jìn)入同步塊時(shí),如果沒有任何其他線程競爭,就會使用偏向鎖,以減少鎖的開銷。輕量級鎖使用線程棧上的數(shù)據(jù)結(jié)構(gòu),避免了操作系統(tǒng)級別的鎖。重量級鎖則涉及操作系統(tǒng)級的互斥鎖。 -
ReentrantLock: java.util.concurrent.locks.ReentrantLock是一個(gè)顯式的鎖類,提供了比synchronized更高級的功能,如可中斷的鎖等待、定時(shí)鎖等待、公平鎖選項(xiàng)等。ReentrantLock使用lock()和unlock()方法來獲取和釋放鎖。其中,公平鎖按照線程請求鎖的順序來分配鎖,保證了鎖分配的公平性,但可能增加鎖的等待時(shí)間。非公平鎖不保證鎖分配的順序,可以減少鎖的競爭,提高性能,但可能造成某些線程的饑餓。 -
讀寫鎖(ReadWriteLock): java.util.concurrent.locks.ReadWriteLock接口定義了一種鎖,允許多個(gè)讀取者同時(shí)訪問共享資源,但只允許一個(gè)寫入者。讀寫鎖通常用于讀取遠(yuǎn)多于寫入的情況,以提高并發(fā)性。 -
樂觀鎖和悲觀鎖:悲觀鎖(Pessimistic Locking)通常指在訪問數(shù)據(jù)前就鎖定資源,假設(shè)最壞的情況,即數(shù)據(jù)很可能被其他線程修改。 synchronized和ReentrantLock都是悲觀鎖的例子。樂觀鎖(Optimistic Locking)通常不鎖定資源,而是在更新數(shù)據(jù)時(shí)檢查數(shù)據(jù)是否已被其他線程修改。樂觀鎖常使用版本號或時(shí)間戳來實(shí)現(xiàn)。 -
自旋鎖:自旋鎖是一種鎖機(jī)制,線程在等待鎖時(shí)會持續(xù)循環(huán)檢查鎖是否可用,而不是放棄CPU并阻塞。通常可以使用CAS來實(shí)現(xiàn)。這在鎖等待時(shí)間很短的情況下可以提高性能,但過度自旋會浪費(fèi)CPU資源。
Java的反射機(jī)制是什么?
Java 反射機(jī)制是在運(yùn)行狀態(tài)中,對于任意一個(gè)類,都能夠知道這個(gè)類中的所有屬性和方法,對于任意一個(gè)對象,都能夠調(diào)用它的任意一個(gè)方法和屬性;這種動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為 Java 語言的反射機(jī)制。
反射具有以下特性:
-
運(yùn)行時(shí)類信息訪問:反射機(jī)制允許程序在運(yùn)行時(shí)獲取類的完整結(jié)構(gòu)信息,包括類名、包名、父類、實(shí)現(xiàn)的接口、構(gòu)造函數(shù)、方法和字段等。 -
動態(tài)對象創(chuàng)建:可以使用反射API動態(tài)地創(chuàng)建對象實(shí)例,即使在編譯時(shí)不知道具體的類名。這是通過Class類的newInstance()方法或Constructor對象的newInstance()方法實(shí)現(xiàn)的。 -
動態(tài)方法調(diào)用:可以在運(yùn)行時(shí)動態(tài)地調(diào)用對象的方法,包括私有方法。這通過Method類的invoke()方法實(shí)現(xiàn),允許你傳入對象實(shí)例和參數(shù)值來執(zhí)行方法。 -
訪問和修改字段值:反射還允許程序在運(yùn)行時(shí)訪問和修改對象的字段值,即使是私有的。這是通過Field類的get()和set()方法完成的。
Spring框架的依賴注入(DI)和控制反轉(zhuǎn)(IoC)
Spring框架是Java生態(tài)系統(tǒng)中最流行的框架之一,它大量使用反射來實(shí)現(xiàn)其核心特性——依賴注入。在Spring中,開發(fā)者可以通過XML配置文件或者基于注解的方式聲明組件之間的依賴關(guān)系。當(dāng)應(yīng)用程序啟動時(shí),Spring容器會掃描這些配置或注解,然后利用反射來實(shí)例化Bean(即Java對象),并根據(jù)配置自動裝配它們的依賴。
例如,當(dāng)一個(gè)Service類需要依賴另一個(gè)DAO類時(shí),開發(fā)者可以在Service類中使用@Autowired注解,而無需自己編寫創(chuàng)建DAO實(shí)例的代碼。Spring容器會在運(yùn)行時(shí)解析這個(gè)注解,通過反射找到對應(yīng)的DAO類,實(shí)例化它,并將其注入到Service類中。這樣不僅降低了組件之間的耦合度,也極大地增強(qiáng)了代碼的可維護(hù)性和可測試性。
動態(tài)代理的實(shí)現(xiàn)
在需要對現(xiàn)有類的方法調(diào)用進(jìn)行攔截、記錄日志、權(quán)限控制或是事務(wù)管理等場景中,反射結(jié)合動態(tài)代理技術(shù)被廣泛應(yīng)用。一個(gè)典型的例子是Spring AOP(面向切面編程)的實(shí)現(xiàn)。Spring AOP允許開發(fā)者定義切面(Aspect),這些切面可以橫切關(guān)注點(diǎn)(如日志記錄、事務(wù)管理),并將其插入到業(yè)務(wù)邏輯中,而不需要修改業(yè)務(wù)邏輯代碼。
例如,為了給所有的服務(wù)層方法添加日志記錄功能,可以定義一個(gè)切面,在這個(gè)切面中,Spring會使用JDK動態(tài)代理或CGLIB(如果目標(biāo)類沒有實(shí)現(xiàn)接口)來創(chuàng)建目標(biāo)類的代理對象。這個(gè)代理對象在調(diào)用任何方法前或后,都會執(zhí)行切面中定義的代碼邏輯(如記錄日志),而這一切都是在運(yùn)行時(shí)通過反射來動態(tài)構(gòu)建和執(zhí)行的,無需硬編碼到每個(gè)方法調(diào)用中。
這兩個(gè)例子展示了反射機(jī)制如何在實(shí)際工程中促進(jìn)松耦合、高內(nèi)聚的設(shè)計(jì),以及如何提供動態(tài)、靈活的編程能力,特別是在框架層面和解決跨切面問題時(shí)。
ThreadLocal原理,怎么使用?
ThreadLocal是Java中用于解決線程安全問題的一種機(jī)制,它允許創(chuàng)建線程局部變量,即每個(gè)線程都有自己獨(dú)立的變量副本,從而避免了線程間的資源共享和同步問題。
ThreadLocal的作用:
-
線程隔離: ThreadLocal為每個(gè)線程提供了獨(dú)立的變量副本,這意味著線程之間不會相互影響,可以安全地在多線程環(huán)境中使用這些變量而不必?fù)?dān)心數(shù)據(jù)競爭或同步問題。 -
降低耦合度:在同一個(gè)線程內(nèi)的多個(gè)函數(shù)或組件之間,使用 ThreadLocal可以減少參數(shù)的傳遞,降低代碼之間的耦合度,使代碼更加清晰和模塊化。 -
性能優(yōu)勢:由于 ThreadLocal避免了線程間的同步開銷,所以在大量線程并發(fā)執(zhí)行時(shí),相比傳統(tǒng)的鎖機(jī)制,它可以提供更好的性能。
ThreadLocal的原理
ThreadLocal的實(shí)現(xiàn)依賴于Thread類中的一個(gè)ThreadLocalMap字段,這是一個(gè)存儲ThreadLocal變量本身和對應(yīng)值的映射。
每個(gè)線程都有自己的ThreadLocalMap實(shí)例,用于存儲該線程所持有的所有ThreadLocal變量的值。當(dāng)你創(chuàng)建一個(gè)ThreadLocal變量時(shí),它實(shí)際上就是一個(gè)ThreadLocal對象的實(shí)例。
每個(gè)ThreadLocal對象都可以存儲任意類型的值,這個(gè)值對每個(gè)線程來說是獨(dú)立的。當(dāng)調(diào)用ThreadLocal的get()方法時(shí),ThreadLocal會檢查當(dāng)前線程的ThreadLocalMap中是否有與之關(guān)聯(lián)的值。如果有,返回該值;如果沒有,會調(diào)用initialValue()方法(如果重寫了的話)來初始化該值,然后將其放入ThreadLocalMap中并返回。
當(dāng)調(diào)用set()方法時(shí),ThreadLocal會將給定的值與當(dāng)前線程關(guān)聯(lián)起來,即在當(dāng)前線程的ThreadLocalMap中存儲一個(gè)鍵值對,鍵是ThreadLocal對象自身,值是傳入的值。
調(diào)用remove()方法時(shí),會從當(dāng)前線程的ThreadLocalMap中移除與該ThreadLocal對象關(guān)聯(lián)的條目。
可能存在的問題
當(dāng)一個(gè)線程結(jié)束時(shí),其ThreadLocalMap也會隨之銷毀,但是ThreadLocal對象本身不會立即被垃圾回收,直到?jīng)]有其他引用指向它為止。
因此,在使用ThreadLocal時(shí)需要注意,如果不顯式調(diào)用remove()方法,或者線程結(jié)束時(shí)未正確清理ThreadLocal變量,可能會導(dǎo)致內(nèi)存泄漏,因?yàn)?code style="font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(100, 149, 237);">ThreadLocalMap會持續(xù)持有ThreadLocal變量的引用,即使這些變量不再被其他地方引用。
因此,實(shí)際應(yīng)用中需要在使用完ThreadLocal變量后調(diào)用remove()方法釋放資源。ThreadLocal類在Java中提供了一種線程綁定的變量版本,這樣每個(gè)線程都有自己的獨(dú)立變量副本,從而避免了多線程之間的數(shù)據(jù)沖突和競態(tài)條件。
下面是如何使用ThreadLocal的基本步驟和示例:創(chuàng)建ThreadLocal實(shí)例你可以直接創(chuàng)建一個(gè)ThreadLocal對象,或者繼承ThreadLocal類并覆蓋其initialValue()方法來提供一個(gè)初始值。示例1:不帶初始值
ThreadLocal<String> threadLocal = new ThreadLocal<>();
示例2:帶有初始值
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "Default Value";
}
};
在Java 8及以上版本,你也可以使用ThreadLocal.withInitial(Supplier<T> supplier)方法來提供一個(gè)初始值:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Default Value");
設(shè)置值使用set()方法將值設(shè)置到當(dāng)前線程的ThreadLocal副本中。
threadLocal.set("Hello World");
獲取值使用get()方法從當(dāng)前線程的ThreadLocal副本中獲取值。
String value = threadLocal.get();
System.out.println(value); // 輸出: Hello World
刪除值使用remove()方法來刪除當(dāng)前線程的ThreadLocal副本中的值。這有助于垃圾回收,避免內(nèi)存泄漏。
threadLocal.remove();
ThreadLocal內(nèi)存泄露問題是什么?
當(dāng)一個(gè)線程結(jié)束時(shí),其ThreadLocalMap也會隨之銷毀,但是ThreadLocal對象本身不會立即被垃圾回收,直到?jīng)]有其他引用指向它為止。
因此,在使用ThreadLocal時(shí)需要注意,如果不顯式調(diào)用remove()方法,或者線程結(jié)束時(shí)未正確清理ThreadLocal變量,可能會導(dǎo)致內(nèi)存泄漏,因?yàn)?/strong>ThreadLocalMap會持續(xù)持有ThreadLocal變量的引用,即使這些變量不再被其他地方引用。
因此,實(shí)際應(yīng)用中需要在使用完ThreadLocal變量后調(diào)用remove()方法釋放資源。
強(qiáng)引用,軟引用,弱引用,虛引用,舉例子說明分別怎么使用
強(qiáng)引用 (Strong Reference)
這是最常見的引用類型,只要一個(gè)對象被強(qiáng)引用關(guān)聯(lián),垃圾收集器永遠(yuǎn)不會回收這個(gè)對象,即使是在內(nèi)存不足的情況下。示例代碼:
String str = new String("Hello");
// str 是一個(gè)強(qiáng)引用,只要 str 存在,"Hello" 對象就不會被垃圾回收。
軟引用 (Soft Reference)
軟引用用于描述一些“有用但不是必須”的對象。如果內(nèi)存空間足夠,軟引用的對象不會被回收,但如果內(nèi)存不足,垃圾收集器會回收軟引用的對象。示例代碼:
import java.lang.ref.SoftReference;
SoftReference<String> softRef = new SoftReference<>(new String("Hello"));
String data = softRef.get(); // 如果軟引用的對象還沒有被回收,get() 將返回它
if (data == null) {
System.out.println("Object has been garbage collected.");
} else {
System.out.println("Object is still alive: " + data);
}
弱引用 (Weak Reference)
弱引用的對象擁有更短的生命周期。只要垃圾收集器進(jìn)行清理,無論內(nèi)存是否充足,弱引用的對象都會被回收。示例代碼:
import java.lang.ref.WeakReference;
WeakReference<String> weakRef = new WeakReference<>(new String("Hello"));
String data = weakRef.get(); // 如果弱引用的對象還沒有被回收,get() 將返回它
if (data == null) {
System.out.println("Object has been garbage collected.");
} else {
System.out.println("Object is still alive: " + data);
}
虛引用 (Phantom Reference)
虛引用是最弱的引用類型。一個(gè)對象如果有虛引用存在,幾乎跟沒有引用一樣,無法通過虛引用來獲取對象實(shí)例。虛引用的主要用途是跟蹤對象的垃圾回收過程。示例代碼:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Hello"), queue);
// 無法通過 PhantomReference 獲取對象實(shí)例
String data = phantomRef.get(); // 返回 null
if (queue.poll() != null) {
System.out.println("Object has been garbage collected and reference enqueued.");
}
在使用虛引用時(shí),通常需要和ReferenceQueue配合使用。當(dāng)垃圾收集器準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它有虛引用,就會把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。通過檢查這個(gè)隊(duì)列,可以了解對象何時(shí)被回收。
spring循環(huán)依賴是什么?怎么解決?
循環(huán)依賴指的是兩個(gè)類中的屬性相互依賴對方:例如 A 類中有 B 屬性,B 類中有 A屬性,從而形成了一個(gè)依賴閉環(huán),如下圖。循環(huán)依賴問題在Spring中主要有三種情況:
-
第一種:通過構(gòu)造方法進(jìn)行依賴注入時(shí)產(chǎn)生的循環(huán)依賴問題。 -
第二種:通過setter方法進(jìn)行依賴注入且是在多例(原型)模式下產(chǎn)生的循環(huán)依賴問題。 -
第三種:通過setter方法進(jìn)行依賴注入且是在單例模式下產(chǎn)生的循環(huán)依賴問題。
只有【第三種方式】的循環(huán)依賴問題被 Spring 解決了,其他兩種方式在遇到循環(huán)依賴問題時(shí),Spring都會產(chǎn)生異常。Spring 解決單例模式下的setter循環(huán)依賴問題的主要方式是通過三級緩存解決循環(huán)依賴。三級緩存指的是 Spring 在創(chuàng)建 Bean 的過程中,通過三級緩存來緩存正在創(chuàng)建的 Bean,以及已經(jīng)創(chuàng)建完成的 Bean 實(shí)例。具體步驟如下:
-
實(shí)例化 Bean:Spring 在實(shí)例化 Bean 時(shí),會先創(chuàng)建一個(gè)空的 Bean 對象,并將其放入一級緩存中。 -
屬性賦值:Spring 開始對 Bean 進(jìn)行屬性賦值,如果發(fā)現(xiàn)循環(huán)依賴,會將當(dāng)前 Bean 對象提前暴露給后續(xù)需要依賴的 Bean(通過提前暴露的方式解決循環(huán)依賴)。 -
初始化 Bean:完成屬性賦值后,Spring 將 Bean 進(jìn)行初始化,并將其放入二級緩存中。 -
注入依賴:Spring 繼續(xù)對 Bean 進(jìn)行依賴注入,如果發(fā)現(xiàn)循環(huán)依賴,會從二級緩存中獲取已經(jīng)完成初始化的 Bean 實(shí)例。
通過三級緩存的機(jī)制,Spring 能夠在處理循環(huán)依賴時(shí),確保及時(shí)暴露正在創(chuàng)建的 Bean 對象,并能夠正確地注入已經(jīng)初始化的 Bean 實(shí)例,從而解決循環(huán)依賴問題,保證應(yīng)用程序的正常運(yùn)行。
spring有哪些常用注解?
Spring框架提供了許多注解來簡化Java開發(fā)中的依賴注入(DI)和面向切面編程(AOP)。下面列舉了一些Spring中常用的注解及其用途:
-
依賴注入相關(guān)注解: -
@Autowired: 自動裝配Bean,Spring會自動尋找類型匹配的Bean并注入。 -
@Qualifier: 與@Autowired配合使用,當(dāng)有多個(gè)相同類型的Bean時(shí),可以指定具體要注入哪一個(gè)。 -
@Primary: 當(dāng)存在多個(gè)相同類型的Bean時(shí),指定首選的Bean。 -
@Resource: 用于注入資源,可以指定名稱,優(yōu)先級低于@Autowired。 -
組件掃描和Bean定義注解: -
@Component: 泛指組件,用于定義Spring Bean。 -
@Repository: 用于數(shù)據(jù)訪問層(DAO層)的Bean定義。 -
@Service: 用于業(yè)務(wù)邏輯層的Bean定義。 -
@Controller: 用于Web層的Bean定義。 -
@RestController: 結(jié)合了@Controller和@ResponseBody,用于RESTful風(fēng)格的Web層Bean定義。 -
@Configuration: 標(biāo)記類為配置類,可以替代XML配置文件。 -
@Bean: 在配置類中定義Bean,可以替代XML配置中的<bean>標(biāo)簽。 -
作用域相關(guān)注解: -
@Scope: 定義Bean的作用域,如單例(singleton)、原型(prototype)等。 -
切面編程相關(guān)注解: -
@Aspect: 標(biāo)記類為切面。 -
@Pointcut: 定義切入點(diǎn)表達(dá)式。 -
@Before: 在切入點(diǎn)方法之前執(zhí)行。 -
@After: 在切入點(diǎn)方法之后執(zhí)行,無論方法是否成功。 -
@AfterReturning: 在切入點(diǎn)方法成功返回后執(zhí)行。 -
@AfterThrowing: 在切入點(diǎn)方法拋出異常后執(zhí)行。 -
Spring MVC和Web相關(guān)注解: -
@RequestMapping: 用于映射HTTP請求到處理方法。 -
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping: 分別用于映射GET、POST、PUT、DELETE請求。 -
@PathVariable: 用于獲取URL中的變量值。 -
@RequestParam: 用于獲取URL查詢字符串中的參數(shù)值。 -
@RequestBody: 用于將HTTP請求體中的數(shù)據(jù)綁定到方法參數(shù)上。 -
@ResponseBody: 表明方法的返回值應(yīng)該直接寫入HTTP響應(yīng)體中。 -
Spring Boot相關(guān)注解: -
@SpringBootApplication: 包含了@Configuration、@EnableAutoConfiguration和@ComponentScan的功能,用于標(biāo)記Spring Boot的主配置類。 -
@EnableAutoConfiguration: 開啟自動配置功能,讓Spring Boot能夠自動配置應(yīng)用。
JVM中堆分為哪些區(qū)域?
根據(jù) JVM8 規(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ì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。 -
Java 虛擬機(jī)棧:每個(gè)線程有一個(gè)私有的棧,隨著線程的創(chuàng)建而創(chuàng)建。棧里面存著的是一種叫“棧幀”的東西,每個(gè)方法會創(chuàng)建一個(gè)棧幀,棧幀中存放了局部變量表(基本數(shù)據(jù)類型和對象引用)、操作數(shù)棧、方法出口等信息。棧的大小可以固定也可以動態(tài)擴(kuò)展。 -
本地方法棧:與虛擬機(jī)棧類似,區(qū)別是虛擬機(jī)棧執(zhí)行java方法,本地方法站執(zhí)行native方法。在虛擬機(jī)規(guī)范中對本地方法棧中方法使用的語言、使用方法與數(shù)據(jù)結(jié)構(gòu)沒有強(qiáng)制規(guī)定,因此虛擬機(jī)可以自由實(shí)現(xiàn)它。 -
程序計(jì)數(shù)器:程序計(jì)數(shù)器可以看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對于多內(nèi)核來說是一個(gè)內(nèi)核)都只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”內(nèi)存。 -
堆內(nèi)存:堆內(nèi)存是 JVM 所有線程共享的部分,在虛擬機(jī)啟動的時(shí)候就已經(jīng)創(chuàng)建。所有的對象和數(shù)組都在堆上進(jìn)行分配。這部分空間可通過 GC 進(jìn)行回收。當(dāng)申請不到空間時(shí)會拋出 OutOfMemoryError。堆是JVM內(nèi)存占用最大,管理最復(fù)雜的一個(gè)區(qū)域。其唯一的用途就是存放對象實(shí)例:所有的對象實(shí)例及數(shù)組都在對上進(jìn)行分配。jdk1.8后,字符串常量池從永久代中剝離出來,存放在隊(duì)中。 -
直接內(nèi)存:直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java 虛擬機(jī)規(guī)范中農(nóng)定義的內(nèi)存區(qū)域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O 方式,它可以使用native 函數(shù)庫直接分配堆外內(nèi)存,然后通脫一個(gè)存儲在Java堆中的DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
Java中堆和棧的區(qū)別?
-
用途:棧主要用于存儲局部變量、方法調(diào)用的參數(shù)、方法返回地址以及一些臨時(shí)數(shù)據(jù)。每當(dāng)一個(gè)方法被調(diào)用,一個(gè)棧幀(stack frame)就會在棧中創(chuàng)建,用于存儲該方法的信息,當(dāng)方法執(zhí)行完畢,棧幀也會被移除。堆用于存儲對象的實(shí)例(包括類的實(shí)例和數(shù)組)。當(dāng)你使用 new關(guān)鍵字創(chuàng)建一個(gè)對象時(shí),對象的實(shí)例就會在堆上分配空間。 -
生命周期:棧中的數(shù)據(jù)具有確定的生命周期,當(dāng)一個(gè)方法調(diào)用結(jié)束時(shí),其對應(yīng)的棧幀就會被銷毀,棧中存儲的局部變量也會隨之消失。堆中的對象生命周期不確定,對象會在垃圾回收機(jī)制(Garbage Collection, GC)檢測到對象不再被引用時(shí)才被回收。 -
存取速度:棧的存取速度通常比堆快,因?yàn)闂W裱冗M(jìn)后出(LIFO, Last In First Out)的原則,操作簡單快速。堆的存取速度相對較慢,因?yàn)閷ο笤诙焉系姆峙浜突厥招枰嗟臅r(shí)間,而且垃圾回收機(jī)制的運(yùn)行也會影響性能。 -
存儲空間:棧的空間相對較小,且固定,由操作系統(tǒng)管理。當(dāng)棧溢出時(shí),通常是因?yàn)檫f歸過深或局部變量過大。堆的空間較大,動態(tài)擴(kuò)展,由JVM管理。堆溢出通常是由于創(chuàng)建了太多的大對象或未能及時(shí)回收不再使用的對象。 -
可見性:棧中的數(shù)據(jù)對線程是私有的,每個(gè)線程有自己的棧空間。堆中的數(shù)據(jù)對線程是共享的,所有線程都可以訪問堆上的對象。
Redis
Redis常用的數(shù)據(jù)結(jié)構(gòu)及底層
Redis 提供了豐富的數(shù)據(jù)類型,常見的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
-
String 類型的應(yīng)用場景:緩存對象、常規(guī)計(jì)數(shù)、分布式鎖、共享 session 信息等。 -
List 類型的應(yīng)用場景:消息隊(duì)列(但是有兩個(gè)問題:1. 生產(chǎn)者需要自行實(shí)現(xiàn)全局唯一 ID;2. 不能以消費(fèi)組形式消費(fèi)數(shù)據(jù))等。 -
Hash 類型:緩存對象、購物車等。 -
Set 類型:聚合計(jì)算(并集、交集、差集)場景,比如點(diǎn)贊、共同關(guān)注、抽獎活動等。 -
Zset 類型:排序場景,比如排行榜、電話和姓名排序等。
String 類型內(nèi)部實(shí)現(xiàn)
String 類型的底層的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)主要是 SDS(簡單動態(tài)字符串)。SDS 和我們認(rèn)識的 C 字符串不太一樣,之所以沒有使用 C 語言的字符串表示,因?yàn)?SDS 相比于 C 的原生字符串:
-
SDS 不僅可以保存文本數(shù)據(jù),還可以保存二進(jìn)制數(shù)據(jù)。因?yàn)?SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結(jié)束,并且 SDS 的所有 API 都會以處理二進(jìn)制的方式來處理 SDS 存放在 buf[] 數(shù)組里的數(shù)據(jù)。所以 SDS 不光能存放文本數(shù)據(jù),而且能保存圖片、音頻、視頻、壓縮文件這樣的二進(jìn)制數(shù)據(jù)。 -
**SDS 獲取字符串長度的時(shí)間復(fù)雜度是 O(1)**。因?yàn)?C 語言的字符串并不記錄自身長度,所以獲取長度的復(fù)雜度為 O(n);而 SDS 結(jié)構(gòu)里用 len 屬性記錄了字符串長度,所以復(fù)雜度為 O(1)。 -
Redis 的 SDS API 是安全的,拼接字符串不會造成緩沖區(qū)溢出。因?yàn)?SDS 在拼接字符串之前會檢查 SDS 空間是否滿足要求,如果空間不夠會自動擴(kuò)容,所以不會導(dǎo)致緩沖區(qū)溢出的問題。
List 類型內(nèi)部實(shí)現(xiàn)
List 類型的底層數(shù)據(jù)結(jié)構(gòu)是由雙向鏈表或壓縮列表實(shí)現(xiàn)的:
-
如果列表的元素個(gè)數(shù)小于 512 個(gè)(默認(rèn)值,可由 list-max-ziplist-entries 配置),列表每個(gè)元素的值都小于 64 字節(jié)(默認(rèn)值,可由 list-max-ziplist-value 配置),Redis 會使用壓縮列表作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu); -
如果列表的元素不滿足上面的條件,Redis 會使用雙向鏈表作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu);
但是在 Redis 3.2 版本之后,List 數(shù)據(jù)類型底層數(shù)據(jù)結(jié)構(gòu)就只由 quicklist 實(shí)現(xiàn)了,替代了雙向鏈表和壓縮列表。
Hash 類型內(nèi)部實(shí)現(xiàn)
Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或哈希表實(shí)現(xiàn)的:
-
如果哈希類型元素個(gè)數(shù)小于 512 個(gè)(默認(rèn)值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字節(jié)(默認(rèn)值,可由 hash-max-ziplist-value 配置)的話,Redis 會使用壓縮列表作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu); -
如果哈希類型元素不滿足上面條件,Redis 會使用哈希表作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)。
在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)了。
Set 類型內(nèi)部實(shí)現(xiàn)
Set 類型的底層數(shù)據(jù)結(jié)構(gòu)是由哈希表或整數(shù)集合實(shí)現(xiàn)的:
-
如果集合中的元素都是整數(shù)且元素個(gè)數(shù)小于 512 (默認(rèn)值,set-maxintset-entries配置)個(gè),Redis 會使用整數(shù)集合作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu); -
如果集合中的元素不滿足上面條件,則 Redis 使用哈希表作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu)。
ZSet 類型內(nèi)部實(shí)現(xiàn)
Zset 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或跳表實(shí)現(xiàn)的:
-
如果有序集合的元素個(gè)數(shù)小于 128 個(gè),并且每個(gè)元素的值小于 64 字節(jié)時(shí),Redis 會使用壓縮列表作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu); -
如果有序集合的元素不滿足上面的條件,Redis 會使用跳表作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu);
在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)了。
Redis跳表的原理及應(yīng)用場景
Redis 只有 Zset 對象的底層實(shí)現(xiàn)用到了跳表,跳表的優(yōu)勢是能支持平均 O(logN) 復(fù)雜度的節(jié)點(diǎn)查找。
跳表結(jié)構(gòu)設(shè)計(jì)
鏈表在查找元素的時(shí)候,因?yàn)樾枰鹨徊檎遥圆樵冃史浅5停瑫r(shí)間復(fù)雜度是O(N),于是就出現(xiàn)了跳表。跳表是在鏈表基礎(chǔ)上改進(jìn)過來的,實(shí)現(xiàn)了一種「多層」的有序鏈表,這樣的好處是能快讀定位數(shù)據(jù)。那跳表長什么樣呢?我這里舉個(gè)例子,下圖展示了一個(gè)層級為 3 的跳表。圖中頭節(jié)點(diǎn)有 L0~L2 三個(gè)頭指針,分別指向了不同層級的節(jié)點(diǎn),然后每個(gè)層級的節(jié)點(diǎn)都通過指針連接起來:
-
L0 層級共有 5 個(gè)節(jié)點(diǎn),分別是節(jié)點(diǎn)1、2、3、4、5; -
L1 層級共有 3 個(gè)節(jié)點(diǎn),分別是節(jié)點(diǎn) 2、3、5; -
L2 層級只有 1 個(gè)節(jié)點(diǎn),也就是節(jié)點(diǎn) 3 。
如果我們要在鏈表中查找節(jié)點(diǎn) 4 這個(gè)元素,只能從頭開始遍歷鏈表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到節(jié)點(diǎn) 4,因?yàn)榭梢栽陬^節(jié)點(diǎn)直接從 L2 層級跳到節(jié)點(diǎn) 3,然后再往前遍歷找到節(jié)點(diǎn) 4。
可以看到,這個(gè)查找過程就是在多個(gè)層級上跳來跳去,最后定位到元素。當(dāng)數(shù)據(jù)量很大時(shí),跳表的查找復(fù)雜度就是 O(logN)。
那跳表節(jié)點(diǎn)是怎么實(shí)現(xiàn)多層級的呢?這就需要看「跳表節(jié)點(diǎn)」的數(shù)據(jù)結(jié)構(gòu)了,如下:
typedef struct zskiplistNode {
//Zset 對象的元素值
sds ele;
//元素權(quán)重值
double score;
//后向指針
struct zskiplistNode *backward;
//節(jié)點(diǎn)的level數(shù)組,保存每層上的前向指針和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
Zset 對象要同時(shí)保存「元素」和「元素的權(quán)重」,對應(yīng)到跳表節(jié)點(diǎn)結(jié)構(gòu)里就是 sds 類型的 ele 變量和 double 類型的 score 變量。每個(gè)跳表節(jié)點(diǎn)都有一個(gè)后向指針(struct zskiplistNode *backward),指向前一個(gè)節(jié)點(diǎn),目的是為了方便從跳表的尾節(jié)點(diǎn)開始訪問節(jié)點(diǎn),這樣倒序查找時(shí)很方便。
跳表是一個(gè)帶有層級關(guān)系的鏈表,而且每一層級可以包含多個(gè)節(jié)點(diǎn),每一個(gè)節(jié)點(diǎn)通過指針連接起來,實(shí)現(xiàn)這一特性就是靠跳表節(jié)點(diǎn)結(jié)構(gòu)體中的zskiplistLevel 結(jié)構(gòu)體類型的 level 數(shù)組。
level 數(shù)組中的每一個(gè)元素代表跳表的一層,也就是由 zskiplistLevel 結(jié)構(gòu)體表示,比如 leve[0] 就表示第一層,leve[1] 就表示第二層。zskiplistLevel 結(jié)構(gòu)體里定義了「指向下一個(gè)跳表節(jié)點(diǎn)的指針」和「跨度」,跨度時(shí)用來記錄兩個(gè)節(jié)點(diǎn)之間的距離。
比如,下面這張圖,展示了各個(gè)節(jié)點(diǎn)的跨度。第一眼看到跨度的時(shí)候,以為是遍歷操作有關(guān),實(shí)際上并沒有任何關(guān)系,遍歷操作只需要用前向指針(struct zskiplistNode *forward)就可以完成了。
跨度實(shí)際上是為了計(jì)算這個(gè)節(jié)點(diǎn)在跳表中的排位。具體怎么做的呢?因?yàn)樘碇械墓?jié)點(diǎn)都是按序排列的,那么計(jì)算某個(gè)節(jié)點(diǎn)排位的時(shí)候,從頭節(jié)點(diǎn)點(diǎn)到該結(jié)點(diǎn)的查詢路徑上,將沿途訪問過的所有層的跨度累加起來,得到的結(jié)果就是目標(biāo)節(jié)點(diǎn)在跳表中的排位。
跳表節(jié)點(diǎn)查詢過程
查找一個(gè)跳表節(jié)點(diǎn)的過程時(shí),跳表會從頭節(jié)點(diǎn)的最高層開始,逐一遍歷每一層。在遍歷某一層的跳表節(jié)點(diǎn)時(shí),會用跳表節(jié)點(diǎn)中的 SDS 類型的元素和元素的權(quán)重來進(jìn)行判斷,共有兩個(gè)判斷條件:
-
如果當(dāng)前節(jié)點(diǎn)的權(quán)重「小于」要查找的權(quán)重時(shí),跳表就會訪問該層上的下一個(gè)節(jié)點(diǎn)。 -
如果當(dāng)前節(jié)點(diǎn)的權(quán)重「等于」要查找的權(quán)重時(shí),并且當(dāng)前節(jié)點(diǎn)的 SDS 類型數(shù)據(jù)「小于」要查找的數(shù)據(jù)時(shí),跳表就會訪問該層上的下一個(gè)節(jié)點(diǎn)。
如果上面兩個(gè)條件都不滿足,或者下一個(gè)節(jié)點(diǎn)為空時(shí),跳表就會使用目前遍歷到的節(jié)點(diǎn)的 level 數(shù)組里的下一層指針,然后沿著下一層指針繼續(xù)查找,這就相當(dāng)于跳到了下一層接著查找。
跳表節(jié)點(diǎn)層數(shù)設(shè)置
跳表的相鄰兩層的節(jié)點(diǎn)數(shù)量的比例會影響跳表的查詢性能。
舉個(gè)例子,下圖的跳表,第二層的節(jié)點(diǎn)數(shù)量只有 1 個(gè),而第一層的節(jié)點(diǎn)數(shù)量有 6 個(gè)。
這時(shí),如果想要查詢節(jié)點(diǎn) 6,那基本就跟鏈表的查詢復(fù)雜度一樣,就需要在第一層的節(jié)點(diǎn)中依次順序查找,復(fù)雜度就是 O(N) 了。所以,為了降低查詢復(fù)雜度,我們就需要維持相鄰層結(jié)點(diǎn)數(shù)間的關(guān)系。
**跳表的相鄰兩層的節(jié)點(diǎn)數(shù)量最理想的比例是 2:1,查找復(fù)雜度可以降低到 O(logN)**。
下圖的跳表就是,相鄰兩層的節(jié)點(diǎn)數(shù)量的比例是 2 : 1。那怎樣才能維持相鄰兩層的節(jié)點(diǎn)數(shù)量的比例為 2 : 1 呢?
如果采用新增節(jié)點(diǎn)或者刪除節(jié)點(diǎn)時(shí),來調(diào)整跳表節(jié)點(diǎn)以維持比例的方法的話,會帶來額外的開銷。
Redis 則采用一種巧妙的方法是,跳表在創(chuàng)建節(jié)點(diǎn)的時(shí)候,隨機(jī)生成每個(gè)節(jié)點(diǎn)的層數(shù),并沒有嚴(yán)格維持相鄰兩層的節(jié)點(diǎn)數(shù)量比例為 2 : 1 的情況。
具體的做法是,跳表在創(chuàng)建節(jié)點(diǎn)時(shí)候,會生成范圍為[0-1]的一個(gè)隨機(jī)數(shù),如果這個(gè)隨機(jī)數(shù)小于 0.25(相當(dāng)于概率 25%),那么層數(shù)就增加 1 層,然后繼續(xù)生成下一個(gè)隨機(jī)數(shù),直到隨機(jī)數(shù)的結(jié)果大于 0.25 結(jié)束,最終確定該節(jié)點(diǎn)的層數(shù)。
這樣的做法,相當(dāng)于每增加一層的概率不超過 25%,層數(shù)越高,概率越低,層高最大限制是 64。雖然我前面講解跳表的時(shí)候,圖中的跳表的「頭節(jié)點(diǎn)」都是 3 層高,但是其實(shí)如果層高最大限制是 64,那么在創(chuàng)建跳表「頭節(jié)點(diǎn)」的時(shí)候,就會直接創(chuàng)建 64 層高的頭節(jié)點(diǎn)。
MySQL
B+樹的優(yōu)缺點(diǎn)是什么?
-
B+樹有一個(gè)最大的好處,方便掃庫,B樹必須用中序遍歷的方法按序掃庫,而B+樹直接從葉子結(jié)點(diǎn)挨個(gè)掃一遍就完了。B+樹支持range-query(區(qū)間查詢)非常方便,而B樹不支持。這是數(shù)據(jù)庫選用B+樹的最主要原因。 -
B+樹最大的性能問題是會產(chǎn)生大量的隨機(jī)IO,隨著新數(shù)據(jù)的插入,葉子節(jié)點(diǎn)會慢慢分裂,邏輯上連續(xù)的葉子節(jié)點(diǎn)在物理上往往不連續(xù),甚至分離的很遠(yuǎn),但做范圍查詢時(shí),會產(chǎn)生大量讀隨機(jī)IO。對于大量的隨機(jī)寫也一樣,舉一個(gè)插入key跨度很大的例子,如7->1000->3->2000 ... 新插入的數(shù)據(jù)存儲在磁盤上相隔很遠(yuǎn),會產(chǎn)生大量的隨機(jī)寫IO。
索引的實(shí)踐優(yōu)化知道哪些?
常見優(yōu)化索引的方法:
-
前綴索引優(yōu)化:使用前綴索引是為了減小索引字段大小,可以增加一個(gè)索引頁中存儲的索引值,有效提高索引的查詢速度。在一些大字符串的字段作為索引時(shí),使用前綴索引可以幫助我們減小索引項(xiàng)的大小。 -
覆蓋索引優(yōu)化:覆蓋索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的葉子節(jié)點(diǎn)上都能找得到的那些索引,從二級索引中查詢得到記錄,而不需要通過聚簇索引查詢獲得,可以避免回表的操作。 -
主鍵索引最好是自增的: -
如果我們使用自增主鍵,那么每次插入的新數(shù)據(jù)就會按順序添加到當(dāng)前索引節(jié)點(diǎn)的位置,不需要移動已有的數(shù)據(jù),當(dāng)頁面寫滿,就會自動開辟一個(gè)新頁面。因?yàn)槊看?strong style="color: rgb(48, 79, 254);background-attachment: scroll;background-clip: border-box;background-image: none;background-origin: padding-box;background-position: 0% 0%;background-repeat: no-repeat;background-size: auto;width: auto;height: auto;border-style: none;border-width: 3px;border-color: rgba(0, 0, 0, 0.4);border-radius: 0px;line-height: 1.75em;">插入一條新記錄,都是追加操作,不需要重新移動數(shù)據(jù),因此這種插入數(shù)據(jù)的方法效率非常高。 -
如果我們使用非自增主鍵,由于每次插入主鍵的索引值都是隨機(jī)的,因此每次插入新的數(shù)據(jù)時(shí),就可能會插入到現(xiàn)有數(shù)據(jù)頁中間的某個(gè)位置,這將不得不移動其它數(shù)據(jù)來滿足新數(shù)據(jù)的插入,甚至需要從一個(gè)頁面復(fù)制數(shù)據(jù)到另外一個(gè)頁面,我們通常將這種情況稱為頁分裂。頁分裂還有可能會造成大量的內(nèi)存碎片,導(dǎo)致索引結(jié)構(gòu)不緊湊,從而影響查詢效率。 -
防止索引失效: -
當(dāng)我們使用左或者左右模糊匹配的時(shí)候,也就是 like %xx或者like %xx%這兩種方式都會造成索引失效; -
當(dāng)我們在查詢條件中對索引列做了計(jì)算、函數(shù)、類型轉(zhuǎn)換操作,這些情況下都會造成索引失效; -
聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配,否則就會導(dǎo)致索引失效。 -
在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會失效。
查詢的實(shí)踐優(yōu)化你知道哪些?
-
使用EXPLAIN分析查詢計(jì)劃:EXPLAIN命令可以幫助你理解數(shù)據(jù)庫如何執(zhí)行查詢,包括使用的索引、表的掃描方式等,從而找出性能瓶頸。 -
避免SELECT :明確列出查詢中需要的列,而不是使用SELECT *。這可以減少數(shù)據(jù)傳輸量,提高查詢速度。 -
創(chuàng)建或優(yōu)化索引:根據(jù)查詢條件創(chuàng)建合適的索引,特別是經(jīng)常用于WHERE子句的字段、Orderby 排序的字段、Join 連表查詢的字典、 group by的字段,并且如果查詢中經(jīng)常涉及多個(gè)字段,考慮創(chuàng)建聯(lián)合索引,使用聯(lián)合索引要符合最左匹配原則,不然會索引失效 -
限制結(jié)果集大小:使用LIMIT限制返回的行數(shù),特別是處理大數(shù)據(jù)量時(shí),這可以顯著提高查詢速度。 -
避免索引失效:比如不要用左模糊匹配、函數(shù)計(jì)算、表達(dá)式計(jì)算等等。 -
分頁優(yōu)化:針對 limit n,y 深分頁的查詢優(yōu)化,可以把Limit查詢轉(zhuǎn)換成某個(gè)位置的查詢:select * from tb_sku where id>20000 limit 10,該方案適用于主鍵自增的表, -
使用分批處理:處理大量數(shù)據(jù)時(shí),可以考慮分批加載數(shù)據(jù),減少內(nèi)存壓力和鎖的競爭。 -
合理使用緩存:對于頻繁查詢的結(jié)果,可以考慮使用緩存機(jī)制,減少數(shù)據(jù)庫的查詢壓力。 -
優(yōu)化數(shù)據(jù)庫表:如果單表的數(shù)據(jù)超過了千萬級別,考慮是否需要將大表拆分為小表,減輕單個(gè)表的查詢壓力。也可以將字段多的表分解成多個(gè)表,有些字段使用頻率高,有些低,數(shù)據(jù)量大時(shí),會由于使用頻率低的存在而變慢,可以考慮分開。
MySQL事務(wù)4大特性?如何保證?
-
原子性(Atomicity):一個(gè)事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會結(jié)束在中間某個(gè)環(huán)節(jié),而且事務(wù)在執(zhí)行過程中發(fā)生錯(cuò)誤,會被回滾到事務(wù)開始前的狀態(tài),就像這個(gè)事務(wù)從來沒有執(zhí)行過一樣,就好比買一件商品,購買成功時(shí),則給商家付了錢,商品到手;購買失敗時(shí),則商品在商家手中,消費(fèi)者的錢也沒花出去。 -
一致性(Consistency):是指事務(wù)操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉(zhuǎn)賬 200 元,分為兩個(gè)步驟,從 A 的賬戶扣除 200 元和對 B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結(jié)果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。 -
隔離性(Isolation):數(shù)據(jù)庫允許多個(gè)并發(fā)事務(wù)同時(shí)對其數(shù)據(jù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致,因?yàn)槎鄠€(gè)事務(wù)同時(shí)使用相同的數(shù)據(jù)時(shí),不會相互干擾,每個(gè)事務(wù)都有一個(gè)完整的數(shù)據(jù)空間,對其他并發(fā)事務(wù)是隔離的。也就是說,消費(fèi)者購買商品這個(gè)事務(wù),是不影響其他消費(fèi)者購買的。 -
持久性(Durability):事務(wù)處理結(jié)束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
InnoDB 引擎通過什么技術(shù)來保證事務(wù)的這四個(gè)特性的呢?
-
持久性是通過 redo log (重做日志)來保證的; -
原子性是通過 undo log(回滾日志) 來保證的; -
隔離性是通過 MVCC(多版本并發(fā)控制) 或鎖機(jī)制來保證的; -
一致性則是通過持久性+原子性+隔離性來保證;
MVCC如何實(shí)現(xiàn)的?
對于「讀提交」和「可重復(fù)讀」隔離級別的事務(wù)來說,它們是通過 Read View 來實(shí)現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時(shí)機(jī)不同,大家可以把 Read View 理解成一個(gè)數(shù)據(jù)快照,就像相機(jī)拍照那樣,定格某一時(shí)刻的風(fēng)景。
-
「讀提交」隔離級別是在「每個(gè)select語句執(zhí)行前」都會重新生成一個(gè) Read View; -
「可重復(fù)讀」隔離級別是執(zhí)行第一條select時(shí),生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View。
Read View 有四個(gè)重要的字段:
-
m_ids :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」的事務(wù) id 列表,注意是一個(gè)列表,“活躍事務(wù)”指的就是,啟動了但還沒提交的事務(wù)。 -
min_trx_id :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」中事務(wù) id 最小的事務(wù),也就是 m_ids 的最小值。 -
max_trx_id :這個(gè)并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時(shí)當(dāng)前數(shù)據(jù)庫中應(yīng)該給下一個(gè)事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值 + 1; -
creator_trx_id :指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。
對于使用 InnoDB 存儲引擎的數(shù)據(jù)庫表,它的聚簇索引記錄中都包含下面兩個(gè)隱藏列:
-
trx_id,當(dāng)一個(gè)事務(wù)對某條聚簇索引記錄進(jìn)行改動時(shí),就會把該事務(wù)的事務(wù) id 記錄在 trx_id 隱藏列里; -
roll_pointer,每次對某條聚簇索引記錄進(jìn)行改動時(shí),都會把舊版本的記錄寫入到 undo 日志中,然后這個(gè)隱藏列是個(gè)指針,指向每一個(gè)舊版本記錄,于是就可以通過它找到修改前的記錄。
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:一個(gè)事務(wù)去訪問記錄的時(shí)候,除了自己的更新記錄總是可見之外,還有這幾種情況:
-
如果記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)可見。 -
如果記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 后才啟動的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)不可見。 -
如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中: -
如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒提交事務(wù)),所以該版本的記錄對當(dāng)前事務(wù)不可見。 -
如果記錄的 trx_id 不在 m_ids列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對當(dāng)前事務(wù)可見。
這種通過「版本鏈」來控制并發(fā)事務(wù)訪問同一個(gè)記錄時(shí)的行為就叫 MVCC(多版本并發(fā)控制)。
MySQL 索引分類有哪些?
MySQL可以按照四個(gè)角度來分類索引。
-
按「數(shù)據(jù)結(jié)構(gòu)」分類:B+tree索引、Hash索引、Full-text索引。 -
按「物理存儲」分類:聚簇索引(主鍵索引)、二級索引(輔助索引)。 -
按「字段特性」分類:主鍵索引、唯一索引、普通索引、前綴索引。 -
按「字段個(gè)數(shù)」分類:單列索引、聯(lián)合索引。
MySQL事務(wù)的原子性怎么保證的
如果我們每次在事務(wù)執(zhí)行過程中,都記錄下回滾時(shí)需要的信息到一個(gè)日志里,那么在事務(wù)執(zhí)行中途發(fā)生了 MySQL 崩潰后,就不用擔(dān)心無法回滾到事務(wù)之前的數(shù)據(jù),我們可以通過這個(gè)日志回滾到事務(wù)之前的數(shù)據(jù)。實(shí)現(xiàn)這一機(jī)制就是 undo log(回滾日志),它保證了事務(wù)的 ACID 特性中的原子性(Atomicity)。
undo log 是一種用于撤銷回退的日志。在事務(wù)沒提交之前,MySQL 會先記錄更新前的數(shù)據(jù)到 undo log 日志文件里面,當(dāng)事務(wù)回滾時(shí),可以利用 undo log 來進(jìn)行回滾。如下圖:
每當(dāng) InnoDB 引擎對一條記錄進(jìn)行操作(修改、刪除、新增)時(shí),要把回滾時(shí)需要的信息都記錄到 undo log 里,比如:
-
在插入一條記錄時(shí),要把這條記錄的主鍵值記下來,這樣之后回滾時(shí)只需要把這個(gè)主鍵值對應(yīng)的記錄刪掉就好了; -
在刪除一條記錄時(shí),要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時(shí)再把由這些內(nèi)容組成的記錄插入到表中就好了; -
在更新一條記錄時(shí),要把被更新的列的舊值記下來,這樣之后回滾時(shí)再把這些列更新為舊值就好了。
在發(fā)生回滾時(shí),就讀取 undo log 里的數(shù)據(jù),然后做原先相反操作。比如當(dāng) delete 一條記錄時(shí),undo log 中會把記錄中的內(nèi)容都記下來,然后執(zhí)行回滾操作的時(shí)候,就讀取 undo log 里的數(shù)據(jù),然后進(jìn)行 insert 操作。
MySQL怎么解決慢查詢問題
-
分析查詢語句:使用EXPLAIN命令分析SQL執(zhí)行計(jì)劃,找出慢查詢的原因,比如是否使用了全表掃描,是否存在索引未被利用的情況等,并根據(jù)相應(yīng)情況對索引進(jìn)行適當(dāng)修改。 -
創(chuàng)建或優(yōu)化索引:根據(jù)查詢條件創(chuàng)建合適的索引,特別是經(jīng)常用于WHERE子句的字段。并且如果查詢中經(jīng)常涉及多個(gè)字段,考慮創(chuàng)建聯(lián)合索引。 -
優(yōu)化數(shù)據(jù)庫架構(gòu):考慮是否需要將大表拆分為小表,減輕單個(gè)表的查詢壓力。 -
使用緩存技術(shù):引入緩存層,如Redis或Memcached,存儲熱點(diǎn)數(shù)據(jù)和頻繁查詢的結(jié)果。實(shí)施緩存更新策略,如Cache Aside或Write Through,確保數(shù)據(jù)一致性。 -
查詢優(yōu)化:避免使用SELECT *,只查詢真正需要的列。使用覆蓋索引,即索引包含所有查詢的字段。盡量減少子查詢和臨時(shí)表的使用,轉(zhuǎn)而使用JOIN或窗口函數(shù)。 -
數(shù)據(jù)庫監(jiān)控與日志分析:定期查看數(shù)據(jù)庫的慢查詢?nèi)罩荆页雎樵兊哪J健? -
代碼層面優(yōu)化:檢查應(yīng)用程序代碼,避免N+1查詢問題,優(yōu)化數(shù)據(jù)加載邏輯。使用數(shù)據(jù)庫連接池,減少連接建立和斷開的開銷。
explain執(zhí)行計(jì)劃有哪些信息?
在MySQL中,EXPLAIN是用于分析和優(yōu)化SQL查詢的性能。當(dāng)你在查詢前加上EXPLAIN關(guān)鍵字,MySQL會返回查詢的執(zhí)行計(jì)劃,這可以幫助你理解MySQL優(yōu)化器如何執(zhí)行查詢,以及查詢的各個(gè)部分是如何被處理的。EXPLAIN 的基本語法如下:
EXPLAIN SELECT ...;
執(zhí)行后,我們可以得到如下圖所示的信息EXPLAIN可以揭示以下信息:
-
查詢中涉及的表及其訪問方式。 -
索引的使用情況。 -
查詢中表的連接順序。 -
數(shù)據(jù)讀取的類型,比如全表掃描(full table scan)還是使用索引(index scan)。 -
是否使用了臨時(shí)表或文件排序等。
接下來我們來具體看看EXPLAIN的參數(shù)有哪些:
-
possible_keys 字段表示可能用到的索引; -
key 字段表示實(shí)際用的索引,如果這一項(xiàng)為 NULL,說明沒有使用索引; -
key_len 表示索引的長度; -
rows 表示掃描的數(shù)據(jù)行數(shù)。 -
type 表示數(shù)據(jù)掃描類型,我們需要重點(diǎn)看這個(gè)。
type 字段就是描述了找到所需數(shù)據(jù)時(shí)使用的掃描方式是什么,常見掃描類型的執(zhí)行效率從低到高的順序?yàn)?/strong>:
-
All(全表掃描):在這些情況里,all 是最壞的情況,因?yàn)椴捎昧巳頀呙璧姆绞? -
index(全索引掃描):index 和 all 差不多,只不過 index 對索引表進(jìn)行全掃描,這樣做的好處是不再需要對數(shù)據(jù)進(jìn)行排序,但是開銷依然很大。所以,要盡量避免全表掃描和全索引掃描。 -
range(索引范圍掃描):range 表示采用了索引范圍掃描,一般在 where 子句中使用 < 、>、in、between 等關(guān)鍵詞,只檢索給定范圍的行,屬于范圍查找。從這一級別開始,索引的作用會越來越明顯,因此我們需要盡量讓 SQL 查詢可以使用到 range 這一級別及以上的 type 訪問方式。 -
ref(非唯一索引掃描):ref 類型表示采用了非唯一索引,或者是唯一索引的非唯一性前綴,返回?cái)?shù)據(jù)返回可能是多條。因?yàn)殡m然使用了索引,但該索引列的值并不唯一,有重復(fù)。這樣即使使用索引快速查找到了第一條數(shù)據(jù),仍然不能停止,要進(jìn)行目標(biāo)值附近的小范圍掃描。但它的好處是它并不需要掃全表,因?yàn)樗饕怯行虻模幢阌兄貜?fù)值,也是在一個(gè)非常小的范圍內(nèi)掃描。 -
eq_ref(唯一索引掃描):eq_ref 類型是使用主鍵或唯一索引時(shí)產(chǎn)生的訪問方式,通常使用在多表聯(lián)查中。比如,對兩張表進(jìn)行聯(lián)查,關(guān)聯(lián)條件是兩張表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 進(jìn)行執(zhí)行計(jì)劃查看的時(shí)候,type 就會顯示 eq_ref。 -
const(結(jié)果只有一條的主鍵或唯一索引掃描):const 類型表示使用了主鍵或者唯一索引與常量值進(jìn)行比較,比如 select name from product where id=1。
除了關(guān)注 type,我們也要關(guān)注 extra 顯示的結(jié)果。這里說幾個(gè)重要的參考指標(biāo):
-
Using filesort :當(dāng)查詢語句中包含 group by 操作,而且無法利用索引完成排序操作的時(shí)候, 這時(shí)不得不選擇相應(yīng)的排序算法進(jìn)行,甚至可能會通過文件排序,效率是很低的,所以要避免這種問題的出現(xiàn)。 -
Using temporary:使了用臨時(shí)表保存中間結(jié)果,MySQL 在對查詢結(jié)果排序時(shí)使用臨時(shí)表,常見于排序 order by 和分組查詢 group by。效率低,要避免這種問題的出現(xiàn)。 -
Using index:所需數(shù)據(jù)只需在索引即可全部獲得,不須要再到表中取數(shù)據(jù),也就是使用了覆蓋索引,避免了回表操作,效率不錯(cuò)。
框架
請求到SpringBoot具體處理函數(shù)的流程
-
請求接收:當(dāng)一個(gè)HTTP請求到達(dá)服務(wù)器時(shí),它會被服務(wù)器容器(如Tomcat、Jetty或Undertow)接收。服務(wù)器容器將請求轉(zhuǎn)發(fā)給Spring Boot的前端控制器 DispatcherServlet。 -
前端控制器處理: DispatcherServlet作為Spring MVC的核心組件,負(fù)責(zé)接收請求并進(jìn)行初步處理。它會委托HttpRequestHandlerMapping或HandlerMapping接口的實(shí)現(xiàn)類去尋找合適的處理器(即控制器方法)。 -
映射處理器: HandlerMapping接口的實(shí)現(xiàn)類(如RequestMappingHandlerMapping)會根據(jù)URL、HTTP方法等信息,查找與請求匹配的處理器方法。查找成功后,HandlerMapping會返回一個(gè)HandlerExecutionChain對象,包含處理器對象和攔截器列表。 -
處理器適配器: DispatcherServlet會調(diào)用HandlerAdapter接口的實(shí)現(xiàn)類來確定如何調(diào)用處理器方法。找到合適的HandlerAdapter后,它會調(diào)用HandlerAdapter的handle方法來執(zhí)行處理器方法。 -
參數(shù)綁定和類型轉(zhuǎn)換:在調(diào)用處理器方法前,參數(shù)綁定和類型轉(zhuǎn)換會自動進(jìn)行。這包括從請求中提取數(shù)據(jù)并將其轉(zhuǎn)換為處理器方法所需的參數(shù)類型。 WebDataBinder和HandlerMethodArgumentResolver參與此過程。 -
攔截器執(zhí)行:如果 HandlerExecutionChain中包含攔截器,則在調(diào)用處理器方法前后會執(zhí)行攔截器的preHandle和postHandle方法。 -
調(diào)用處理器方法:經(jīng)過上述步驟后,具體的處理器方法(控制器方法)將被執(zhí)行。控制器方法可能會返回一個(gè)模型和視圖,或者直接返回一個(gè)響應(yīng)體。 -
處理返回值:根據(jù)控制器方法的返回值類型, HandlerAdapter會選擇相應(yīng)的ViewResolver或ResponseBody處理器來處理返回值。如果是視圖,ViewResolver會解析視圖名稱并生成視圖對象。如果是響應(yīng)體,HttpMessageConverter會將返回的對象轉(zhuǎn)換為HTTP響應(yīng)體。 -
視圖渲染:如果有視圖對象,它會被渲染,將模型數(shù)據(jù)填充到視圖模板中,生成HTML或其他格式的響應(yīng)。 -
響應(yīng)發(fā)送:最終的響應(yīng)(HTML頁面、JSON數(shù)據(jù)等)將被封裝在HTTP響應(yīng)中發(fā)送回客戶端。 -
攔截器后處理:如果有攔截器, postHandle和afterCompletion方法將在響應(yīng)發(fā)送后被調(diào)用,以便進(jìn)行額外的后處理或資源清理。
算法
-
層序遍歷
點(diǎn)擊關(guān)注公眾號,閱讀更多精彩內(nèi)容

