<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          還得是美團(tuán)啊!依舊是校招大戶,給力!

          共 30352字,需瀏覽 61分鐘

           ·

          2024-08-02 17:00

          Python客棧設(shè)為“星標(biāo)?
          第一時(shí)間收到最新資訊

          大家好,我是小林。

          早上看到美團(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)。


          ?? ?

          ?

          考察的知識點(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ù)很可能被其他線程修改。synchronizedReentrantLock都是悲觀鎖的例子。樂觀鎖(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ī)制。

          反射具有以下特性:

          1. 運(yùn)行時(shí)類信息訪問:反射機(jī)制允許程序在運(yùn)行時(shí)獲取類的完整結(jié)構(gòu)信息,包括類名、包名、父類、實(shí)現(xiàn)的接口、構(gòu)造函數(shù)、方法和字段等。
          2. 動態(tài)對象創(chuàng)建:可以使用反射API動態(tài)地創(chuàng)建對象實(shí)例,即使在編譯時(shí)不知道具體的類名。這是通過Class類的newInstance()方法或Constructor對象的newInstance()方法實(shí)現(xiàn)的。
          3. 動態(tài)方法調(diào)用:可以在運(yùn)行時(shí)動態(tài)地調(diào)用對象的方法,包括私有方法。這通過Method類的invoke()方法實(shí)現(xiàn),允許你傳入對象實(shí)例和參數(shù)值來執(zhí)行方法。
          4. 訪問和修改字段值:反射還允許程序在運(yùn)行時(shí)訪問和修改對象的字段值,即使是私有的。這是通過Field類的get()和set()方法完成的。


          應(yīng)用場景:Java反射機(jī)制在現(xiàn)代軟件開發(fā)中,尤其是在企業(yè)級應(yīng)用和框架設(shè)計(jì)中扮演著重要角色,尤其是在我們平時(shí)用的spring框架中,很多地方都用到了反射,讓我們來看看Spring的IoC和AOP是如何使用反射技術(shù)的:


          1. 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ù)性和可測試性。

          1. 動態(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)用ThreadLocalget()方法時(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ù)的流程

          1. 請求接收:當(dāng)一個(gè)HTTP請求到達(dá)服務(wù)器時(shí),它會被服務(wù)器容器(如Tomcat、Jetty或Undertow)接收。服務(wù)器容器將請求轉(zhuǎn)發(fā)給Spring Boot的前端控制器DispatcherServlet
          2. 前端控制器處理:DispatcherServlet作為Spring MVC的核心組件,負(fù)責(zé)接收請求并進(jìn)行初步處理。它會委托HttpRequestHandlerMappingHandlerMapping接口的實(shí)現(xiàn)類去尋找合適的處理器(即控制器方法)。
          3. 映射處理器:HandlerMapping接口的實(shí)現(xiàn)類(如RequestMappingHandlerMapping)會根據(jù)URL、HTTP方法等信息,查找與請求匹配的處理器方法。查找成功后,HandlerMapping會返回一個(gè)HandlerExecutionChain對象,包含處理器對象和攔截器列表。
          4. 處理器適配器:DispatcherServlet會調(diào)用HandlerAdapter接口的實(shí)現(xiàn)類來確定如何調(diào)用處理器方法。找到合適的HandlerAdapter后,它會調(diào)用HandlerAdapterhandle方法來執(zhí)行處理器方法。
          5. 參數(shù)綁定和類型轉(zhuǎn)換:在調(diào)用處理器方法前,參數(shù)綁定和類型轉(zhuǎn)換會自動進(jìn)行。這包括從請求中提取數(shù)據(jù)并將其轉(zhuǎn)換為處理器方法所需的參數(shù)類型。WebDataBinderHandlerMethodArgumentResolver參與此過程。
          6. 攔截器執(zhí)行:如果HandlerExecutionChain中包含攔截器,則在調(diào)用處理器方法前后會執(zhí)行攔截器的preHandlepostHandle方法。
          7. 調(diào)用處理器方法:經(jīng)過上述步驟后,具體的處理器方法(控制器方法)將被執(zhí)行。控制器方法可能會返回一個(gè)模型和視圖,或者直接返回一個(gè)響應(yīng)體。
          8. 處理返回值:根據(jù)控制器方法的返回值類型,HandlerAdapter會選擇相應(yīng)的ViewResolverResponseBody處理器來處理返回值。如果是視圖,ViewResolver會解析視圖名稱并生成視圖對象。如果是響應(yīng)體,HttpMessageConverter會將返回的對象轉(zhuǎn)換為HTTP響應(yīng)體。
          9. 視圖渲染:如果有視圖對象,它會被渲染,將模型數(shù)據(jù)填充到視圖模板中,生成HTML或其他格式的響應(yīng)。
          10. 響應(yīng)發(fā)送:最終的響應(yīng)(HTML頁面、JSON數(shù)據(jù)等)將被封裝在HTTP響應(yīng)中發(fā)送回客戶端。
          11. 攔截器后處理:如果有攔截器,postHandleafterCompletion方法將在響應(yīng)發(fā)送后被調(diào)用,以便進(jìn)行額外的后處理或資源清理。

          算法

          • 層序遍歷

          往期回顧

          1、12個(gè)Python循環(huán)中的性能監(jiān)控與優(yōu)化工具和技巧
          2、一個(gè) Bug 改了三次,汗流浹背了。。
          3、Linux Mint 22“Wilma”正式發(fā)布
          4、不到2MB,很炸裂的神器!
          5、8年前就免費(fèi)的功能還在扣費(fèi)!運(yùn)營商的回應(yīng)令人無語
                


          點(diǎn)擊關(guān)注公眾號,閱讀更多精彩內(nèi)容

          瀏覽 156
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产乱轮视频 | 97视频自拍| 亚洲三级片视频 | 小嫩苞一区二区三区 | 成人免费做受 |